☰
大数运算课程设计:十进制与二进制高精度算法实现与避坑指南
2026/10/6 3:29:44 网站建设 项目流程

简介:这份数据结构课程设计资源聚焦大数运算的完整实现,面向计算机专业学生及需要处理超长数值的开发者。项目覆盖大数加法、减法、乘法、除法、乘方与取模六类核心操作,并同时支持十进制与二进制大数运算,可应用于密码学、高性能计算等场景,帮助读者理解数组存储、进位借位、快速幂与长除法等算法细节。资源包共35个文件,以17个txt验证数据、5个Python测试脚本、4个data数据文件及2个cpp源文件为主,另含头文件、可执行程序与工程配置,压缩包约22.24MB,目录结构便于对照源码与测试用例。目前已有1352人学习下载。通过阅读BigInteger核心实现与配套Python验证代码,读者可掌握大数运算的完整设计思路、算法优化策略及跨语言结果比对方法,适合作为课程设计参考或算法练习素材。

1. 大数运算课程设计:为什么 64 位整数一撞就碎,得自己造轮子

做数据结构课程设计,大数运算是被选得最多、也最容易翻车的一类题。C 语言里unsigned long long顶天 1.8×10¹⁹,Java 的long也就 9.2×10¹⁸,一旦题目要求算 100 的阶乘、2 的 1000 次方,或者两个 200 位十进制数相乘,内置类型直接溢出,结果变成负数或者垃圾值。这不是玄学,是位数不够。大数运算要解决的核心问题就一句话:把数字拆成一位一位存进数组或字符串,自己实现加减乘除、乘方、取模,并且同时兼容十进制和二进制两种进制。它适合正在做数据结构课设的本科生,也适合想搞明白高精度算法底层逻辑的开发者。下面我按自己带课设的经验,把选型、实现、参数和踩坑一次讲透。

2. 存储结构怎么选:十进制和二进制大数的底层表示

2.1 为什么不用字符串直接算,而要先想清楚存储

很多人第一反应是用字符串存大数,因为输入输出方便。字符串确实能存,但每次运算都要做字符到数字的转换,乘法和除法里反复取位、进位,代码会变得又长又慢。更常见的做法是用整型数组,每个元素存一位或者若干位。这里有个关键分叉:十进制大数和二进制大数的存储粒度不一样。

十进制大数,我一般用int数组,每个元素存 0 到 9 的一位数字,低位放在数组前面,也就是下标 0 存个位。这样进位方向是从低下标往高下标走,和手算一致。二进制大数则每个元素存 0 或 1,或者为了效率每个元素存 30 位、60 位,但课设阶段建议老老实实一位一存,先把逻辑跑通。

提示:低位在前、高位在后,是所有大数运算的默认约定。如果你反过来存,进位和借位方向全乱,后面除法会写到怀疑人生。

2.2 结构体定义与初始化:一份能同时吃十进制和二进制的骨架

下面这份 C 语言结构体是我课设里反复用过的版本,同时支持两种进制,靠一个base字段区分。

#include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_DIGITS 2000 // 支持约 2000 位十进制,二进制可到 6000 位左右 typedef struct { int digits[MAX_DIGITS]; // 低位在前,digits[0] 是个位/最低位 int len; // 当前有效位数 int base; // 10 表示十进制,2 表示二进制 int sign; // 1 正数,-1 负数,0 表示零 } BigNum; // 初始化:把字符串转成大数,自动识别进制 void initBigNum(BigNum *n, const char *str, int base) { memset(n->digits, 0, sizeof(n->digits)); n->len = 0; n->base = base; n->sign = 1; int start = 0; if (str[0] == '-') { n->sign = -1; start = 1; } else if (str[0] == '+') { start = 1; } int L = strlen(str); for (int i = L - 1; i >= start; i--) { char c = str[i]; int v; if (c >= '0' && c <= '9') v = c - '0'; else if (c >= 'A' && c <= 'F') v = c - 'A' + 10; else if (c >= 'a' && c <= 'f') v = c - 'a' + 10; else continue; n->digits[n->len++] = v; } // 去掉高位多余的零 while (n->len > 1 && n->digits[n->len - 1] == 0) n->len--; if (n->len == 1 && n->digits[0] == 0) n->sign = 0; }

这段代码的逻辑很直白:从字符串末尾往前扫,因为末尾是个位。遇到负号先记符号。十六进制字符也顺手支持了,虽然标题只要求十进制和二进制,但多支持一个不费事。参数base决定后续运算时进位阈值是 10 还是 2。len始终维护有效位数,高位零全部砍掉,否则比较大小和除法会出错。

2.3 十进制与二进制共存的三个设计约束

同时支持两种进制,不是把base一改就完事。我踩过的坑集中在三点。第一,进位阈值必须跟着base走,十进制满 10 进 1,二进制满 2 进 1,写死 10 的话二进制加法直接错。第二,输出函数要按base决定打印字符,二进制只打印 0 和 1,十进制打印 0 到 9。第三,比较大小的时候,先比len,len相同再从高位往低位逐位比,这个逻辑和进制无关,可以复用。把这三条守住,后面加减乘除乘方取模都能共用一套底层。

3. 加减乘除四件套:从手算竖式到可复现代码

3.1 大数加法和减法:符号处理才是真正的难点

加法本身不难,难的是带符号。我的做法是先写一个无符号加法addAbs,只负责绝对值相加,符号另外判断。减法同理,先写subAbs保证大减小,如果实际是小减大,就交换再取负。

// 绝对值加法:c = |a| + |b| void addAbs(const BigNum *a, const BigNum *b, BigNum *c) { c->base = a->base; c->len = 0; int carry = 0; int maxLen = (a->len > b->len) ? a->len : b->len; for (int i = 0; i < maxLen || carry; i++) { int sum = carry; if (i < a->len) sum += a->digits[i]; if (i < b->len) sum += b->digits[i]; c->digits[c->len++] = sum % c->base; // 关键:按 base 取余 carry = sum / c->base; // 关键:按 base 进位 } c->sign = (c->len == 1 && c->digits[0] == 0) ? 0 : 1; } // 绝对值减法:要求 |a| >= |b|,结果 c = |a| - |b| void subAbs(const BigNum *a, const BigNum *b, BigNum *c) { c->base = a->base; c->len = 0; int borrow = 0; for (int i = 0; i < a->len; i++) { int diff = a->digits[i] - borrow - (i < b->len ? b->digits[i] : 0); if (diff < 0) { diff += c->base; borrow = 1; } else borrow = 0; c->digits[c->len++] = diff; } while (c->len > 1 && c->digits[c->len - 1] == 0) c->len--; c->sign = (c->len == 1 && c->digits[0] == 0) ? 0 : 1; }

注意sum % c->base和sum / c->base这两行,它们就是十进制和二进制共用的关键。base是 10 时满十进一,是 2 时满二进一,同一份代码不用改。减法里的diff += c->base同理,借位时借的是base,不是固定的 10。很多人二进制减法算错,就是这里写死了 10。

带符号的加减法,规则和初中数学一样:同号相加取相同符号,异号相减取绝对值大的符号。我一般写一个compareAbs先比绝对值大小,再分四种情况调用addAbs或subAbs。这块逻辑不复杂但分支多,建议单独写测试用例,把+5 + -3、-5 + 3、-5 + -3、5 + -5全跑一遍。

3.2 大数乘法:O(n²) 竖式乘法和它的参数边界

乘法用竖式,两层循环,c[i+j] += a[i] * b[j],最后统一处理进位。这是最稳的写法,复杂度 O(n²),2000 位乘 2000 位大概 400 万次内层操作,课设完全够用。

void mulBig(const BigNum *a, const BigNum *b, BigNum *c) { c->base = a->base; c->len = a->len + b->len; c->sign = (a->sign == 0 || b->sign == 0) ? 0 : a->sign * b->sign; memset(c->digits, 0, sizeof(c->digits)); for (int i = 0; i < a->len; i++) { for (int j = 0; j < b->len; j++) { c->digits[i + j] += a->digits[i] * b->digits[j]; } } // 统一进位 int carry = 0; for (int i = 0; i < c->len; i++) { int tmp = c->digits[i] + carry; c->digits[i] = tmp % c->base; carry = tmp / c->base; } while (c->len > 1 && c->digits[c->len - 1] == 0) c->len--; if (c->len == 1 && c->digits[0] == 0) c->sign = 0; }

参数上要注意c->len初始设为a->len + b->len,这是乘积位数的上界。进位循环必须覆盖整个c->len,因为内层累加后c->digits[i]可能远大于base,比如十进制下最大是 9×9×2000,不统一进位会溢出。二进制下每个元素最大是 1×1×位数,同样要进位。这个统一进位的写法比边乘边进位更不容易错,代价是多一次遍历。

3.3 大数除法:试商法是唯一靠谱的路

除法是四件套里最难的。我试过二分试商,也试过逐位试商,最后发现课设阶段最稳的是「逐位试商」:从被除数最高位开始,每次拉一位下来,用减法不断试出当前位的商。

// 无符号除法:q = a / b, r = a % b void divModAbs(const BigNum *a, const BigNum *b, BigNum *q, BigNum *r) { q->base = a->base; q->len = a->len; q->sign = 1; memset(q->digits, 0, sizeof(q->digits)); r->base = a->base; r->len = 0; r->sign = 0; memset(r->digits, 0, sizeof(r->digits)); for (int i = a->len - 1; i >= 0; i--) { // 余数左移一位(乘以 base),加上当前位 for (int k = r->len; k > 0; k--) r->digits[k] = r->digits[k - 1]; r->digits[0] = a->digits[i]; r->len++; while (r->len > 1 && r->digits[r->len - 1] == 0) r->len--; // 试商:用减法数出这一位商 int cnt = 0; while (compareAbs(r, b) >= 0) { BigNum tmp; subAbs(r, b, &tmp); *r = tmp; cnt++; } q->digits[i] = cnt; } while (q->len > 1 && q->digits[q->len - 1] == 0) q->len--; if (q->len == 1 && q->digits[0] == 0) q->sign = 0; }

这段代码里compareAbs是比较绝对值的辅助函数,返回 1、0、-1。试商用减法循环,十进制下每位最多减 9 次,二进制下最多减 1 次,所以二进制除法反而更快。参数上要注意余数r在每轮开始前要左移一位,左移就是所有位往高位挪一格,低位补当前被除数的位。这个左移在十进制下相当于乘 10,二进制下相当于乘 2,和base一致。

注意:除法里q->digits[i] = cnt直接赋值,因为试商结果一定小于base。如果你发现商位大于等于base,说明试商循环写错了,检查compareAbs的边界。

3.4 乘方和取模:复用乘法与除法的组合拳

乘方就是快速幂,把指数转成二进制,不断平方。取模就是除法取余数。这两个都是前面四件套的组合,不需要新算法。

// 快速幂:c = a^e,结果可能很大,课设里一般配合取模使用 void powBig(const BigNum *a, int e, BigNum *c) { BigNum base = *a; initBigNum(c, "1", a->base); while (e > 0) { if (e & 1) { BigNum tmp; mulBig(c, &base, &tmp); *c = tmp; } e >>= 1; if (e > 0) { BigNum tmp; mulBig(&base, &base, &tmp); base = tmp; } } } // 取模:r = a % m void modBig(const BigNum *a, const BigNum *m, BigNum *r) { BigNum q; divModAbs(a, m, &q, r); r->sign = a->sign; // 余数符号跟随被除数 }

快速幂里e & 1判断当前二进制位是否为 1,是就乘进结果,然后指数右移一位,底数平方。这个算法把 O(e) 次乘法降到 O(log e) 次,算 2 的 1000 次方只需要约 10 次乘法。取模直接调除法,余数符号跟随被除数,这是 C 语言%的语义,保持一致。参数上注意powBig的指数用int就够,课设里指数很少超过 10000,真要超大指数可以把指数也做成大数,但那是另一个难度了。

4. 避坑与排查:大数运算课设里最容易翻车的 5 个点

4.1 现象:二进制加法结果比预期多一位,或者十进制减法出现负数位

原因:进位或借位阈值写死了 10。二进制加法里sum % 10和sum / 10会让结果错乱,因为二进制满 2 就该进位。减法里diff += 10在二进制下借多了。

解决:所有涉及进位、借位、取余、整除的地方,统一用base字段,不要出现字面量 10。写完后用base=2跑一组1011 + 0110验证。

4.2 现象:乘法结果高位全是零,或者长度不对

原因:c->len初始化成a->len + b->len后,没有在最后砍掉高位零。或者进位循环只跑到a->len + b->len - 1,漏了最高位的进位。

解决:进位循环范围用c->len,循环结束后用while砍高位零。如果最高位进位没处理,结果会少一位,比如99 * 99 = 9801变成980。

4.3 现象:除法死循环,程序卡住

原因:试商循环里compareAbs(r, b) >= 0一直成立,因为减法结果没有正确更新r,或者subAbs要求|a| >= |b|但传参反了。

解决:在试商循环里每次减完把tmp赋回r,确保r真的变小。另外检查compareAbs的实现,先比len,len相同再从高位往低位比,不要从低位比。

4.4 现象:取模结果符号不对,或者余数大于模数

原因:余数符号没有跟随被除数,或者除法里余数没有最终规范化。

解决:modBig最后加一行r->sign = a->sign。如果余数绝对值大于等于模数绝对值,说明试商没试够,检查试商循环条件。

4.5 现象:同时跑十进制和二进制时,输出乱码或位数错乱

原因:输出函数没有按base分支,或者初始化时base字段没传对。

解决:输出函数里判断n->base == 10打印'0'+digit,n->base == 2只打印'0'或'1'。初始化时显式传base,不要靠全局变量。

5. 验证与进阶:用对拍和边界用例把大数运算钉死

课设验收的时候,老师不会只看你跑通一个例子。我一般会准备三组验证。第一组是边界用例:0 加 0、0 减 0、1 乘 0、0 除以非零、非零除以 1、负数乘负数、二进制全 1 加 1。第二组是对拍:用 Python 的int做参照,随机生成 100 组大数,把 C 程序的结果和 Python 结果逐位比对。第三组是性能边界:2000 位十进制乘法跑 100 次,看耗时是否在可接受范围。

对拍脚本可以这样写,用 Python 生成测试数据并调用编译好的 C 程序:

import random, subprocess def gen_big(base, length): if base == 10: return ''.join(random.choice('0123456789') for _ in range(length)) else: return ''.join(random.choice('01') for _ in range(length)) for _ in range(100): base = random.choice([10, 2]) a = gen_big(base, random.randint(1, 50)) b = gen_big(base, random.randint(1, 50)) # 调用 C 程序,传入 a b base,拿回结果 result = subprocess.run(['./bignum', a, b, str(base)], capture_output=True, text=True) c_result = result.stdout.strip() # Python 按进制解析后计算,再转回字符串比对 py_result = str(int(a, base) + int(b, base)) if base == 2: py_result = bin(int(a, 2) + int(b, 2))[2:] assert c_result == py_result, f"FAIL: {a} + {b} base {base}" print("all pass")

这个脚本的关键是int(a, base)按指定进制解析,二进制结果用bin()转回字符串时去掉0b前缀。对拍能抓出 90% 的进位和符号 bug,比手算靠谱得多。

进阶一点,如果你想让除法更快,可以把试商从逐次减法改成二分试商,在0到base-1之间二分找商,十进制下最多 4 次比较,比最多 9 次减法快一倍。二进制下没区别,因为商只能是 0 或 1。另一个技巧是压位,十进制下每个数组元素存 4 位数字,乘法内层循环减少 4 倍,但进位和输出要额外处理,课设里看时间够不够再决定。

我自己的习惯是,每写完一个运算函数,先不急着写下一个,而是用对拍脚本单独跑这个函数 100 组随机数据,全过了再往下走。大数运算的 bug 会传染,加法错了乘法必错,除法错了取模必错。把每个环节钉死,最后组合起来才稳。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询