资讯详情

资讯详情

大数运算实现详解:从数组存储到快速幂的完整设计

简介这份数据结构课程设计资源聚焦大数运算的完整实现面向计算机专业学生及需要理解高精度计算的开发者解决普通整型无法处理超长数值的痛点。项目覆盖大数加法、减法、乘法、除法、乘方与取模六类核心操作并同时支持十进制与二进制大数运算涉及进位借位处理、竖式乘法、长除法、快速幂及边乘边取模等算法思路在密码学、分布式计算等场景中具有实用价值。资源包共35个文件以17个txt测试数据与结果文件、5个Python验证脚本、2个C源码及头文件为主另含可执行文件与工程配置压缩包约22.24MB便于对照验证算法正确性。目前已有1353人学习下载。读者可获得完整源代码、多组输入输出样例及Python对照脚本用于理解大数运算细节、验证结果并完成课程设计报告。1. 大数运算为什么long long也会溢出而字符串却能算到底写过大数运算的人多半有过这种体验明明用了 64 位整数算个阶乘到 21 就翻车unsigned long long撑到 20 也到头了。这不是编译器的问题而是定长类型的天花板——64 位能表示的状态只有 2^64 种而 100 的阶乘有 158 位十进制数字物理上就塞不进去。大数运算要解决的核心矛盾就一句话当数值的位数超过机器字长时如何用软件模拟出一套「无限位」的算术系统。这份数据结构课程设计资源把大数加法、减法、乘法、除法、乘方、取模六种运算全部实现并且同时支持十进制和二进制两种进制。它值得搞懂的地方不在于「能算大数」这个结论而在于它逼你回答一个底层问题人类做竖式笔算的那套规则怎么翻译成计算机能执行的循环和进位逻辑。搞懂这套映射你再看任意精度库、密码学里的模幂运算、甚至浮点数的舍入误差都会有一种「原来底层就是这么回事」的通透感。接下来从存储结构开始一层层拆到除法试商和快速幂的实现细节。2. 存储结构为什么用数组存数字而不是字符串2.1 大数的物理表示低位在前还是高位在前大数在内存里最常见的表示是一个整型数组每个元素存一位十进制或若干位二进制下的 limb。关键设计决策是下标方向数组下标 0 存最低位还是最高位。绝大多数实现选择低位在前原因是进位传播的方向——加法从最低位开始进位往高位走如果低位在下标 0进位就是i1循环自然从左往右推进如果高位在前进位方向就和下标方向相反每次进位都要往前插代码会别扭很多。// 大数的核心结构低位存在下标 0 typedef struct { int *digits; // digits[0] 是个位digits[1] 是十位依此类推 int len; // 当前有效位数 int cap; // 已分配的容量 int sign; // 符号1 正-1 负0 表示零 int base; // 进制基数10 或 2 } BigNum;base字段是这份设计支持双进制的关键。同一套加减乘除逻辑只要把进位阈值从 10 换成 2就能在二进制模式下复用。sign单独存符号而不是用补码是因为大数没有固定位宽补码表示需要先确定位宽反而把问题复杂化了。len和cap分离是动态数组的标准做法避免每次运算都重新分配。2.2 为什么不用字符串直接算字符串存大数看起来更直观12345一个字符一位但每次取某一位都要做c - 0的转换乘法里嵌套循环访问频繁这个转换开销会累积。更麻烦的是字符串不可变中间结果需要频繁拼接内存分配次数爆炸。用整型数组每一位本身就是可运算的数值乘法和进位直接做算术省掉了字符与数字之间的来回转换。代价是内存占用比字符串高一个 int 至少 4 字节存一位十进制数字利用率只有约 1/8但换来的运算速度提升在课程设计这个量级上完全值得。提示如果追求极致空间效率可以一个 int 存 9 位十进制因为 10^9 在 32 位 int 范围内这就是所谓「压位」技巧后面乘法章节会提到它的取舍。3. 加减法进位与借位是怎么在数组上传播的3.1 加法的进位传播与符号处理加法是所有运算的基础逻辑最直接从下标 0 开始逐位相加加上来自低位的进位结果对 base 取模留在当前位商作为新进位传给下一位。循环结束后如果进位还是 1说明结果多了一位需要扩容。BigNum* bn_add(BigNum *a, BigNum *b) { // 同号相加绝对值相加符号取 a 的符号 if (a-sign b-sign) { BigNum *r bn_alloc(max(a-len, b-len) 1, a-base); int carry 0; for (int i 0; i r-cap; i) { int da (i a-len) ? a-digits[i] : 0; // 越界位补 0 int db (i b-len) ? b-digits[i] : 0; int sum da db carry; r-digits[i] sum % a-base; // 当前位 carry sum / a-base; // 进位传给下一位 } r-sign a-sign; bn_trim(r); // 去掉高位多余的 0修正 len return r; } // 异号相加等价于大减小交给减法处理 return bn_sub_signed(a, b); }bn_trim这一步容易被忽略但很关键加法结果的高位可能全是 0比如100 (-100)走减法路径后如果不修正len后续比较大小和除法都会出错。carry用sum / base而不是硬编码/10正是为了二进制模式下自动切换成/2。3.2 减法的借位与大小比较减法比加法多一个前置判断必须先比较两个数的绝对值大小确定结果符号再用大减小。如果直接小减大借位会一路传播到最高位之外产生负数位逻辑就崩了。// 比较绝对值大小返回 1 表示 |a| |b|-1 表示小于0 表示相等 int bn_cmp_abs(BigNum *a, BigNum *b) { if (a-len ! b-len) return a-len b-len ? 1 : -1; for (int i a-len - 1; i 0; i--) { // 从最高位往下比 if (a-digits[i] ! b-digits[i]) return a-digits[i] b-digits[i] ? 1 : -1; } return 0; }比较必须从最高位开始因为高位权重更大。len不同时直接比len能省掉逐位扫描这是常见的优化。确定大小后减法循环里当da - db - borrow 0时当前位加baseborrow置 1等价于向高位借了一个 base。这套借位逻辑和加法进位是对称的理解了加法减法只是把「进位」换成「借位」、把「加」换成「减」。4. 乘法O(n²) 竖式乘法与压位优化4.1 双重循环的竖式乘法乘法是加减法的组合放大结果第ij位由a的第i位和b的第j位相乘贡献所有贡献累加后再统一处理进位。这就是小学竖式乘法的程序化表达。BigNum* bn_mul(BigNum *a, BigNum *b) { BigNum *r bn_alloc(a-len b-len, a-base); // 结果最多这么多位 for (int i 0; i a-len; i) { int carry 0; for (int j 0; j b-len; j) { // 当前位 原有累积值 本次乘积 进位 int cur r-digits[i j] a-digits[i] * b-digits[j] carry; r-digits[i j] cur % a-base; carry cur / a-base; } r-digits[i b-len] carry; // 处理行末进位 } r-sign a-sign * b-sign; bn_trim(r); return r; }注意r-digits[ij]是累加而不是直接赋值因为不同的(i,j)组合会落到同一个下标上。carry在每行结束后要单独加到i b-len位置这是竖式乘法里「错位相加」的体现。时间复杂度 O(n²)n 是位数对于课程设计里几百上千位的数完全够用。4.2 压位用空间换时间的经典取舍如果一位一位乘1000 位乘 1000 位要做 100 万次内层循环。压位技巧是把 base 从 10 提到 10^9一个 int 存 9 位十进制位数瞬间降到约 1/9循环次数降到约 1/81。代价是进位和取模的常数变大且输出时需要补前导零。// 压位模式下base 1000000000 // 输出时每个 limb 要补足 9 位最高位除外 void bn_print(BigNum *a) { if (a-sign 0) printf(-); printf(%d, a-digits[a-len - 1]); // 最高位不补零 for (int i a-len - 2; i 0; i--) printf(%09d, a-digits[i]); // 其余补足 9 位 }%09d这个格式串是压位输出的关键少了它1000000001会被打印成11。压位和二进制模式有冲突——二进制下天然按位存压位意义不大所以这份设计里压位主要用在十进制模式。这是典型的「优化有适用边界」的例子不是所有场景都该压位。5. 除法与取模试商法为什么是最难写对的部分5.1 长除法的试商逻辑除法是六种运算里最容易写错的。核心是模拟长除法从被除数最高位开始逐位拉下来和当前余数拼成临时被除数然后「试商」——猜一个商位使得除数 × 商位 ≤ 临时被除数。试商从base-1往下试效率太低常见优化是用临时被除数的最高两位除以除数的最高位来估算再回退修正。// 核心估算商位然后回退到正确值 int estimate_quotient(BigNum *rem, BigNum *div) { if (bn_cmp_abs(rem, div) 0) return 0; // 用最高位估算最多回退几次 int q (rem-len div-len) ? (rem-digits[rem-len-1] * rem-base rem-digits[rem-len-2]) / div-digits[div-len-1] : 0; if (q rem-base) q rem-base - 1; // 估算值封顶 // 回退保证 div * q rem while (q 0 bn_cmp_abs(bn_mul_small(div, q), rem) 0) q--; return q; }估算值可能偏大所以要用while回退。回退次数在正常情况下是常数级这是长除法能跑到 O(n²) 而不是 O(n³) 的原因。bn_mul_small是除数乘一个单位数比完整乘法快得多。5.2 取模与除法的关系取模本质是除法的副产品a mod b a - (a / b) * b。所以实现了除法取模几乎免费。但要注意符号约定——-7 mod 3在不同语言里结果不同C 是 -1Python 是 2。这份设计里取模结果符号跟随被除数和 C 语言一致这是需要明确的接口约定否则调用方会踩坑。BigNum* bn_mod(BigNum *a, BigNum *b) { BigNum *q bn_div(a, b); // 先求商 BigNum *prod bn_mul(q, b); // 商 × 除数 BigNum *r bn_sub(a, prod); // 被除数 - 乘积 余数 return r; // 符号跟随 a }6. 乘方与快速幂把 O(n) 次乘法压到 O(log n)6.1 朴素乘方与它的瓶颈乘方最直白的实现是循环 n 次乘法a^n做 n-1 次乘法。当 n 是几百时还能忍n 是几万时直接卡死。问题在于每次乘法都是 O(位数²)n 次乘法就是 O(n × 位数²)指数一大就爆炸。6.2 快速幂的二进制分解快速幂利用指数的二进制分解a^13 a^(1101₂) a^8 × a^4 × a^1。从低位到高位扫描指数底数不断自乘a → a² → a⁴ → a⁸遇到指数当前位是 1 就把当前底数乘进结果。乘法次数从 n 降到 log n。BigNum* bn_pow(BigNum *base, long long exp) { BigNum *result bn_from_int(1, base-base); // 结果初始化为 1 BigNum *b bn_copy(base); while (exp 0) { if (exp 1) // 当前二进制位是 1 result bn_mul(result, b); b bn_mul(b, b); // 底数自乘对应位权翻倍 exp 1; // 指数右移一位 } return result; }exp 1判断最低位exp 1右移这是二进制分解的标准写法。b bn_mul(b, b)让底数按a, a², a⁴, a⁸...增长正好对应二进制各位的权重。这个技巧和取模结合就是模幂运算是密码学里 RSA 的核心理解了大数快速幂再看模幂就是多一步取模而已。注意快速幂里b bn_mul(b, b)会产生大量中间大数内存峰值比朴素乘方高位数增长极快100 的 100 次方有 201 位1000 的 1000 次方有 3001 位分配容量时要留够。7. 避坑与排查大数运算里那些让人熬夜的细节7.1 高位零没清理导致比较和除法出错现象100 - 100得到的结果打印是空或者0但和真正的0比较却说不相等。原因减法后len没修正高位残留 0bn_cmp_abs先比len长度不同直接判不等。解决每次运算结束调用bn_trim从最高位往下扫描遇到非零位就停把len修正到那里如果全是 0len置 1 且sign置 0。7.2 二进制模式下进位阈值写死成 10现象二进制大数加法结果完全错误1 1得到2而不是10。原因进位逻辑里硬编码了% 10和/ 10没读base字段。解决所有进位、取模、借位操作统一用base变量base在创建时根据模式设为 10 或 2。这是「魔法数字」的典型翻车现场。7.3 除法试商估算偏大导致死循环现象除法跑着跑着卡住或者商位出现负数。原因估算公式在除数最高位很小时比如除数是1000...估出的商偏大回退循环条件写错q减到 0 还在减。解决回退循环加q 0保护估算值先封顶到base - 1并且每次回退后重新比较确保div × q ≤ rem严格成立。7.4 乘方指数用 int 导致大指数溢出现象算2^100时指数传进去变成负数或奇怪的值。原因指数参数用了int但调用方传的是更大的数或者指数在中间计算里溢出。解决指数用long long并且在快速幂入口检查指数非负如果要做大指数指数本身也是大数需要另一套基于指数二进制的迭代不能直接用long long。7.5 内存泄漏每个中间结果都要释放现象跑一批运算后内存持续上涨最后 OOM。原因大数运算里bn_mul、bn_add每次都malloc新结构调用方忘了free中间结果。解决约定「谁分配谁释放」或者引入引用计数课程设计里更简单的做法是每个函数返回新对象调用方用完立即释放快速幂里b bn_mul(b, b)这种自赋值要先释放旧的b。8. 设计取舍为什么这套实现值得逐行读一遍大数运算的实现方式有很多种这份课程设计选了最「笨」但最透明的一条路数组存位、竖式模拟、O(n²) 乘法、试商除法。它没有用 Karatsuba 分治乘法没有用牛顿迭代求倒数来做除法也没有用 FFT 加速。这不是能力不够而是教学场景下的正确取舍——每一种高级算法都建立在基础算法之上如果连竖式乘法的进位传播都没亲手写过看 Karatsuba 的分治合并只会一头雾水。我一般会这么理解这套代码的价值它是一面镜子照出你对「进位」「借位」「试商」这些词的理解是不是真的落地了。很多人能背出快速幂的公式但让他处理exp为 0、base为 0、结果为负这些边界就开始含糊。这份实现把这些边界都摊开在代码里bn_trim、sign处理、base参数化每一处都是踩过坑才会加的。验证这套实现是否真的理解到位有个简单办法把base从 10 改成 16看加法、乘法、除法是否还能正确工作。如果能说明进位逻辑真正参数化了如果不能说明哪里还藏着硬编码的 10。再进一步把乘法换成 Karatsuba对比位数在几百和几千时的耗时曲线你会直观看到 O(n²) 和 O(n^1.585) 的分岔点在哪里。这种「改一个参数看系统反应」的验证习惯比读十遍代码都管用。从那以后我每次拿到一个号称「支持任意精度」的实现都会先翻它的进位逻辑和 trim 函数看这两处有没有偷懒——这两处干净整个实现的可信度就立住了。希望这份资源能帮你把大数运算从「知道有这么回事」变成「能讲清楚每一位是怎么进上去的」。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →