简介:这份数据结构课程设计资源聚焦大数运算的完整实现,面向计算机专业学生及需要理解高精度计算的开发者,解决普通整型无法处理超长数值的编程难题。项目覆盖大数加法、减法、乘法、除法、乘方与取模六类核心操作,并同时支持十进制与二进制两种进制的大数运算,涉及进位借位处理、竖式乘法、长除法模拟、快速幂分治以及乘法过程中取模防溢出等关键算法思路,在密码学、分布式计算与高性能计算场景中均有实际应用价值。资源包共35个文件,以17个txt验证数据与结果文件、5个Python对照脚本、4个data测试数据、2个cpp源文件及配套头文件、目标文件与可执行程序为主,压缩包约22.24MB,目录结构清晰,便于按模块查阅与调试。目前已有1352人学习下载,读者可借助源代码、验证数据与Python参考实现,对照理解大数运算的底层细节,完成课程设计并提升算法设计与数据结构应用能力。
1. 大数运算课程设计:为什么90%的人卡在除法与取模
课程设计选题里,「大数运算」几乎是数据结构课最经典的题目之一。题目要求很明确:实现大数加法、减法、乘法、除法、乘方、取模,同时支持十进制和二进制。看起来只是把小学竖式搬到代码里,但真正动手后你会发现,加减乘还能靠模拟竖式硬写,除法和取模才是分水岭——90%的人在这里翻车。原因很简单:除法不是一次遍历能解决的,它需要「试商」,而试商策略直接决定你是O(n²)还是O(n³)。更麻烦的是,题目要求同时支持十进制和二进制,意味着你不能把数字直接塞进int或long long,必须自己设计存储结构。这篇笔记就按一线实现的顺序,把存储选型、加减乘除、乘方取模、进制切换、避坑排查全部拆开,让你能照着复现一套能跑、能过答辩、能扛住边界用例的完整方案。适合正在做数据结构课设的本科生,也适合想补一补大数底层实现的开发者。
2. 存储结构选型:十进制和二进制怎么共用一套骨架
2.1 为什么不用字符串直接算
最直觉的做法是用std::string存数字,逐位转int再运算。但字符串有两个硬伤:一是每次运算都要反复做char - '0',常数大;二是乘法和除法需要频繁随机访问和进位传播,字符串的不可变性会让代码里塞满临时对象。常见做法是转成vector<int>或vector<long long>,每个元素存一位或若干位。这里有个关键决策:十进制和二进制要不要共用同一套存储?
我的选择是共用vector<int>,但每个元素存「一个进制位」。十进制时每位0~9,二进制时每位0~1。这样加减乘除的核心逻辑完全一致,只需要在进位阈值和输出格式上做区分。代价是二进制下空间利用率低(一个int只存0或1),但课设规模下完全可接受,换来的是代码量减少一半。
struct BigInt { vector<int> digits; // 低位在前,digits[0]是个位 int base; // 10 或 2 bool negative; // 符号位,仅十进制减法/除法用 BigInt(int b = 10) : base(b), negative(false) {} };低位在前是血泪经验:进位从低位向高位传播,push_back比insert(begin())快得多,而且除法试商时从高位开始,反转遍历即可。base字段让同一份加减乘代码服务两种进制。negative只在十进制减法出现「小减大」时置位,二进制课设通常只做无符号,但加上符号位更完整。
2.2 十进制与二进制的输入输出转换
输入时按字符串读入,逐字符转数字。十进制直接c - '0',二进制c - '0'同样适用,因为字符'0'和'1'的差值就是0和1。输出时从高位到低位打印,十进制注意去掉前导零,二进制同理。
BigInt fromString(const string& s, int base) { BigInt num(base); for (int i = s.size() - 1; i >= 0; --i) { if (s[i] == '-') { num.negative = true; continue; } num.digits.push_back(s[i] - '0'); // 低位在前 } num.trim(); // 去掉高位多余的0 return num; } string toString(const BigInt& num) { if (num.digits.empty()) return "0"; string s; if (num.negative) s += '-'; for (int i = num.digits.size() - 1; i >= 0; --i) s += char('0' + num.digits[i]); return s; }trim()负责删除最高位的零,否则1000 - 1000会得到0000而不是0。这个函数在每次运算后都要调用,是避免输出异常的第一道防线。二进制输入输出不需要特殊处理,因为base字段已经隔离了差异。注意输入可能带前导零,trim同样能处理。
2.3 进位与借位的统一处理框架
加法和减法的核心是进位/借位传播。十进制进位阈值是10,二进制是2,用base统一。减法需要处理借位,当某位不够减时向高位借base。
BigInt add(const BigInt& a, const BigInt& b) { BigInt res(a.base); int carry = 0, n = max(a.digits.size(), b.digits.size()); for (int i = 0; i < n || carry; ++i) { int sum = carry; if (i < a.digits.size()) sum += a.digits[i]; if (i < b.digits.size()) sum += b.digits[i]; res.digits.push_back(sum % a.base); carry = sum / a.base; } res.trim(); return res; }这段代码的关键是循环条件i < n || carry,确保最高位进位不会丢失。减法类似,但需要先比较大小决定符号,再用「大减小」避免负数借位混乱。二进制下base=2,sum % 2和sum / 2自动完成二进制进位,不需要额外分支。这套框架是后面乘除的基础,务必先跑通。
3. 加减乘除逐个落地:从竖式模拟到试商优化
3.1 大数加法与减法:符号处理和借位边界
加法在上一节已经给出核心。减法要复杂一些,因为涉及符号。我的策略是:先比较绝对值大小,用大的减小的,如果原被减数小,结果加负号。比较函数从高位往低位比,位数不同直接按位数判断。
int cmpAbs(const BigInt& a, const BigInt& b) { if (a.digits.size() != b.digits.size()) return a.digits.size() > b.digits.size() ? 1 : -1; for (int i = a.digits.size() - 1; i >= 0; --i) if (a.digits[i] != b.digits[i]) return a.digits[i] > b.digits[i] ? 1 : -1; return 0; } BigInt sub(const BigInt& a, const BigInt& b) { if (cmpAbs(a, b) < 0) { BigInt r = sub(b, a); r.negative = !r.negative; return r; } BigInt res(a.base); int borrow = 0; for (int i = 0; i < a.digits.size(); ++i) { int diff = a.digits[i] - borrow - (i < b.digits.size() ? b.digits[i] : 0); if (diff < 0) { diff += a.base; borrow = 1; } else borrow = 0; res.digits.push_back(diff); } res.trim(); return res; }借位边界容易翻车的地方是:当a比b长,但高位被借位后变成负数,比如1000 - 1。循环里i只走到a.digits.size(),借位在最后一位处理后自然结束,因为a的最高位足够大。但如果a和b位数相同且a略大,借位可能传播到最高位之外,此时borrow为0,不会出问题。二进制下base=2,diff += 2完成借位,逻辑一致。
3.2 大数乘法:O(n²)竖式与进位累加
乘法用双层循环模拟竖式,res[i+j] += a[i] * b[j],最后统一处理进位。这是最稳的写法,不容易出错。
BigInt mul(const BigInt& a, const BigInt& b) { BigInt res(a.base); res.digits.resize(a.digits.size() + b.digits.size(), 0); for (int i = 0; i < a.digits.size(); ++i) { for (int j = 0; j < b.digits.size(); ++j) { res.digits[i + j] += a.digits[i] * b.digits[j]; res.digits[i + j + 1] += res.digits[i + j] / a.base; res.digits[i + j] %= a.base; } } res.trim(); return res; }注意进位是「边乘边进」,而不是最后统一进。这样写的好处是res.digits[i+j]始终小于base,不会溢出。二进制下a[i]*b[j]最大为1,进位更简单。乘法结果是a.size()+b.size()位,预分配避免push_back扩容。如果课设要求性能,可以提一句Karatsuba,但课设规模下O(n²)足够。
3.3 大数除法与取模:试商法的三种实现对比
除法是课设的核心难点。常见做法有三种:一是重复减法,太慢;二是二分试商,稳定但常数大;三是牛顿迭代,复杂且不适合课设。我一般用「逐位试商」:从高位到低位,每次取被除数的一段,用二分或线性试探找到最大商位。
// 返回 {商, 余数} pair<BigInt, BigInt> divmod(const BigInt& a, const BigInt& b) { BigInt q(a.base), r(a.base); for (int i = a.digits.size() - 1; i >= 0; --i) { r.digits.insert(r.digits.begin(), a.digits[i]); // 余数左移一位 r.trim(); int lo = 0, hi = a.base - 1, best = 0; while (lo <= hi) { int mid = (lo + hi) / 2; BigInt t = mul(b, fromInt(mid, a.base)); if (cmpAbs(t, r) <= 0) { best = mid; lo = mid + 1; } else hi = mid - 1; } q.digits.insert(q.digits.begin(), best); r = sub(r, mul(b, fromInt(best, a.base))); } q.trim(); r.trim(); return {q, r}; }fromInt把整数转成BigInt。二分试商的范围是0到base-1,十进制最多试4次,二进制最多试1次,效率可接受。取模就是divmod的余数。注意r.digits.insert(begin())是O(n)操作,整体复杂度O(n²log base),课设够用。如果要求更高,可以用「估商法」:用被除数前两位除以除数首位估算商位,再修正,但实现复杂,容易出边界bug。
3.4 大数乘方与取模:快速幂的递归与迭代写法
乘方用快速幂,把指数二进制分解,底数不断平方。取模乘方在每步乘法后取模,防止结果爆炸。
BigInt pow(BigInt base, int exp) { BigInt res(1, base.base); // 1 while (exp > 0) { if (exp & 1) res = mul(res, base); base = mul(base, base); exp >>= 1; } return res; } BigInt powMod(BigInt base, BigInt exp, const BigInt& mod) { BigInt res(1, base.base); base = divmod(base, mod).second; while (!exp.digits.empty()) { if (exp.digits[0] & 1) res = divmod(mul(res, base), mod).second; base = divmod(mul(base, base), mod).second; exp = divmod(exp, fromInt(2, exp.base)).first; } return res; }pow的指数用int,课设规模够用;如果指数也是大数,需要把exp转成二进制逐位处理。powMod里指数是BigInt,每次除以2取商,直到为0。注意base先对mod取模,避免底数过大。二进制下base=2,快速幂同样适用,因为乘法已经支持二进制。
4. 避坑与排查:大数运算课设最常见的5个翻车点
4.1 前导零导致比较和输出异常
现象:1000 - 1000输出0000,或者比较两个数时0010和10被判为不等。原因是运算后没有清理高位零。解决:每次运算结束调用trim(),比较前也确保两边都trim过。trim实现是从高位遍历,删除连续的0,但至少保留一位。
4.2 除法试商时余数左移顺序错误
现象:除法结果偏小或偏大,余数不对。原因是r.digits.insert(begin(), a.digits[i])把新位插到了最低位,而正确做法是余数整体左移一位后把新位放到最低位。insert(begin())等价于左移,但要注意digits是低位在前,所以begin()是最低位,插入后原来的位都向高位移动,逻辑正确。如果写成push_back就变成加到最高位,结果全错。
4.3 二进制与十进制混用时base字段丢失
现象:十进制数转二进制后运算结果按十进制进位,或者反过来。原因是构造BigInt时没有传递base,默认用了10。解决:所有运算函数返回结果时用BigInt res(a.base),fromInt也要传base。混用场景下,先统一转成同一进制再运算。
4.4 乘方指数为0或负数时返回错误
现象:pow(x, 0)返回0而不是1,或者负数指数死循环。原因是快速幂初始res没设为1,或者指数用int时负数右移行为未定义。解决:res初始化为BigInt(1, base),指数为0直接返回1;负数指数在课设中通常不支持,可以报错或转成倒数(但大数倒数需要分数表示,不建议)。
4.5 取模运算中模数为0或负数
现象:程序崩溃或结果无意义。原因是divmod中除数为0时试商循环异常。解决:在divmod入口判断b是否为零,是则抛出异常或返回错误码。模数为负数时,先取绝对值,结果符号按被除数处理,课设一般只做正数取模。
5. 进阶技巧:用二进制大数验证十进制结果的正确性
课设答辩时老师常问「你怎么证明你的结果是对的」。一个很实用的技巧是:用二进制大数独立实现一套运算,然后和十进制结果交叉验证。因为二进制下进位阈值是2,试商范围是0~1,逻辑更简单,不容易出bug。如果两套结果一致,基本可以确认正确。
具体做法:写一个toBinary和fromBinary,把十进制数转成二进制BigInt,用同一套加减乘除函数运算,再转回十进制比较。注意转换本身也要用大数除法:十进制转二进制就是不断除以2取余。
BigInt decToBin(const BigInt& dec) { BigInt n = dec, two(2, 10), bin(2); while (!n.digits.empty()) { auto qr = divmod(n, two); bin.digits.push_back(qr.second.digits.empty() ? 0 : qr.second.digits[0]); n = qr.first; } bin.trim(); return bin; }验证时选几组边界用例:0、1、10^18、两个大数相乘、除法余数为0和不为0的情况。如果二进制和十进制结果一致,答辩时可以直接展示这个交叉验证过程,比空口说「我测过了」有说服力得多。
另一个技巧是给除法加「验算」:商 * 除数 + 余数 == 被除数,每次除法后自动验算,不通过就打印中间状态。这个习惯帮我省了很多调试时间。课设代码不用追求极致性能,但正确性必须可验证。我一般会在main里跑一组随机测试,用long long能表示的范围做对照,超出范围再用二进制交叉验证。这套组合拳下来,除法和取模的边界基本不会翻车。希望帮到你。
本文还有配套的精品资源,点击获取