
简介一套面向高校理工科学生与科研人员的MATLAB大数计算工具箱覆盖大整数/大数的高精度运算、素性检验、因数分解、斐波那契数列、取模与商余计算等常见操作。资源共包含113个文件其中100个m脚本与函数构成核心代码2个mat文件提供测试数据7个png图片展示运行结果另有docx/html/rtf格式的程序说明文档整体压缩包仅2.78MB轻量易部署。代码采用模块化设计注释清晰、参数变量说明完整运行环境为Windows7及以上与MATLAB2014a及以上版本适合计算机、电子信息、数学、物理、机械、土木等专业学生用于毕业设计、课程设计或海外作业参考。目前已有76人学习浏览对需要快速理解并复用大数运算算法的入门者与开发者均有实用价值。1. 当 double 在 1e308 前投降MATLAB 大数计算工具箱怎么接管做数值计算的人迟早会碰到这一幕明明只是算个 50 的阶乘double 却悄悄变成了 Inf想求 2 的 1000 次方plot 出来的曲线直接断成几截。MATLAB 原生数据类型里int64 上限约 9.22e18double 顶到 1e308看起来很大可组合数、斯特林数、大质数判定一进来这点家底立刻见底。网上最常见的应对是调用符号数学工具箱的 vpa但符号计算的速度和内存开销在批量任务里非常难看。这套大数计算工具箱走的是另一条路它自己实现了一个 vpi 类把整数按十进制拆块存进数组再在这个结构上重写了 factor、isprime、fibonacci、nextprime 以及四则运算的专用版本。它的定位很清楚不接符号引擎用结构化数组和算法优化把大数运算变成纯数值循环。这篇文章不打算只贴 demo_vpi.m 的说明逐行复述而是把数据表示、判定逻辑、运算实现和实际调用场景拆开讲你拿它做课程设计也好改造成自己的精度引擎也好都能直接对齐。2. VPI 类的数据表示以及 factor / isprime 的质数判定逻辑2.1 decimal digits 数组为什么不直接用 int64 拼大数先看这个工具箱里最底层的东西vpi 对象长什么样。打开任意一个文件查看变量时你会发现它的核心属性就是一组DecimalDigits数组每位存 0 到 9 之间一个数字按从低位到高位的顺序排。比如数字 12345 在这个类里存成[5 4 3 2 1]。为什么不用几个 int64 拼成一个大整数MATLAB 的数组索引和内存连续性是它的强项但一旦涉及跨字节进位要么用typecast做位运算要么处理 64 位边界上的进位标志这两件事在脚本语言里都容易出错。十进制数组的代价是每位只存一格 int8内存占用是二进制表示的约 2.4 倍换来的是加减法可以完全照搬小学算术的逐位算法调试时可以直接disp这个数组看中间状态。工具箱里所有函数都围绕这个表示展开这是理解后面每个 m 文件的钥匙。内部结构长这样classdef vpi properties DecimalDigits int8(0); % 从低位到高位每位一个数字 Sign int8(1); % 1 正, -1 负 end methods function obj vpi(n) if nargin 0 return; end if isnumeric(n) isscalar(n) obj vpi_from_double(obj, n); else error(vpi:inputError, 只支持标量数值输入); end end end endvpi_from_double是私有转换函数用循环依次取模 10 得到每一位。虽然叫 DecimalDigits但声明成 int8 而不是 uint8是为了后面做减法时不额外处理位扩展。符号单独存一个 int8乘除法都先算绝对值最后统一修正符号这个设计让 times 和 quotient 不用在每步都检查正负。2.2 factor 的试除 Pollard Rho 混合策略拿到一个 vpi 大数第一件想做的事通常是分解质因数。工具箱里的 factor.m 没有傻乎乎地从头试除到尾它采用的是分段策略。小于 10^6 的数直接查预生成的质数表试除这个表在工具箱首次运行时会用埃拉托斯特尼筛法生成生成一次缓存到持久变量里。超过这个规模的数先用前几千个质数试除把最容易分解的小因子剥离掉剩下的残余如果检测到是质数就直接返回否则进入 Pollard Rho 循环。function [factors, exponents] factor(vpi_input) % 参数说明: % vpi_input : vpi 对象不能为 0 或 1 % factors : 质因子数组按升序排列 % exponents : 对应质因子的指数 factors vpi(1); exponents []; n vpi_input; if n 1 error(factor:invalidInput, 0 和 1 不做质因数分解); end % 第 1 步小质数试除最多跑 primes(1000) small_primes load_small_primes(); for p small_primes pv vpi(p); count 0; while mod(n, pv) 0 n n / pv; count count 1; end if count 0 factors factors * pv; exponents(end1) count; %#okAGROW end if n 1 return; end end % 第 2 步剩余项继续分解 while ~isprime(n) d pollard_rho(n); count 0; while mod(n, d) 0 n n / d; count count 1; end factors factors * d; exponents(end1) count; %#okAGROW end factors factors * n; exponents(end1) 1; end这段代码里有两个参数值得关注小质数集合的大小和pollard_rho里的迭代上限。我习惯把试除范围从 1000 提升到 5000因为 Pollard Rho 对大质因子效率高对 1000 到 5000 之间的小因子反而很浪费。另外pollard_rho里自带一个循环次数限制默认是 10000 步超过就直接放弃本次尝试重新选种子 f(x) x^2 c 里的 c。如果你的任务里整数中间有大质数平方项你会发现 c 的随机选择对耗时影响极大甚至超过算法本身的常数项。2.3 isprime 为什么用的是 Miller-Rabin 而不是朴素试除isprime.m 是判断大整数是否为质数。朴素试除法在 vpi 场景下不可行因为 n 可能是几百位的十进制数。工具箱的做法是先用 2、3、5、7 的小质数集合做快速排除——这些判断直接用 mod 实现代价极低绝大多数合数在这一步就被筛掉了。剩下的候选者走 Miller-Rabin 概率测试基底选前 12 个质数[2 3 5 7 11 13 17 19 23 29 31 37]。这组基底的错误率对于小于 2^64 的整数为零对于更大的整数误判概率低于 4 的负 12 次方在课程设计和工程场景完全够用。function result isprime(n) if ~isa(n, vpi) n vpi(n); end % 边界处理 small_primes [2 3 5 7 11 13 17 19 23 29 31 37]; % 先检查是否被小质数整除 for p small_primes if n p result true; return; end if mod(n, vpi(p)) 0 result false; return; end end % 提因子: n - 1 d * 2^s n_minus_1 n - 1; s 0; d n_minus_1; while d 1 mod(d, vpi(2)) 0 s s 1; d d / 2; end result miller_rabin(n, d, s, small_primes); end关键在于mod 运算本身是 vpi 类的方法它已经从 double 运算里解放出来了。所以这个函数跑得快不快很大程度上取决于底层大数取模的除法实现效率。这就是为什么工具箱的作者要把 quotient 和 mod 单独写成独立文件——没有好的除法isprime 的速度会从秒级退化到分钟级。3. times、quotient 和 mod 的实现思路与参数调整3.1 大数乘法从 O(n^2) 到分块优化的选择vpi 里的 times.m 重写了*运算符。最直接的实现是双重循环模拟竖式乘法外层遍历乘数每一位内层遍历被乘数每一位累加进结果数组。这个算法的时间复杂度是 O(mn)其中 m 和 n 分别是两个操作数的十进制位数。位数在几百以内时这个朴素版本反而是最快、最稳定的因为它的常数因子小内存访问线性无递归开销。工具箱内部做的优化是把内层循环改成矩阵运算用result(i1:ilen_b) result(i1:ilen_b) a_i * b一次处理一整个乘数字段。function c times(a, b) % 大数乘法入口支持 vpi * vpi 和 vpi * double a_digits a.DecimalDigits; b_digits b.DecimalDigits; len_a numel(a_digits); len_b numel(b_digits); % 短数做外层循环减少列切片次数 if len_a len_b a_digits b_digits; b_digits a.DecimalDigits; len_a numel(a_digits); len_b numel(b_digits); end result_digits zeros(1, len_a len_b 1, int32); for i 1:len_a ai a_digits(i); if ai 0 continue; end % 核心乘加: 对应位置的逐位乘积累加 tmp ai .* b_digits; result_digits(i:ilen_b-1) result_digits(i:ilen_b-1) tmp; end % 统一进位避免每层循环都进位 carry 0; for k 1:numel(result_digits) s result_digits(k) carry; result_digits(k) mod(s, 10); carry floor(s / 10); end % 去掉前导 0 c vpi.empty; c.DecimalDigits int8(result_digits(find(result_digits ~ 0, 1, last):-1:1)); end注意最后一步的进位逻辑为了性能内层循环不立刻处理进位而是把所有中间结果积累在 result_digits 里最后统一做一次进位扫描。这样做的原因很实际逐位进位的分支判断在 MATLAB 循环里非常昂贵尤其是大数相乘动辄上千次迭代统一进位的总耗时只有前者的五分之一左右。这个取舍在任何大数库里都成立Python 的 int 底层也是这么干的只是 MATLAB 里需要你自己写。使用这段代码时有个参数要调中间数组的数据类型。我这里用了 int32如果输入位数超大且值接近 9 位数字组合int32 可能溢出可以把数据类型改成 int64但内存占用会翻倍。我的习惯是位数总和超过 3000 再切 int64否则 int32 足够。3.2 quotient 和 mod一次除法同时算商和余数大数除法是大数计算里最难写对、也最影响整体性能的一个环节。工具箱里的 quotient.m 和 mod.m 其实是同一次长除法计算的两个出口quotient 返回商mod 返回余数。内部实现的公共函数做一次长除法然后按调用方需求丢弃一半结果。商和余数分开出两个文件是为了在 isprime 和 factor 里单独调用时不必承担存储另一个结果的额外开销。function [q, r] long_divide(a, b) % a 为被除数b 为除数 % q 为商r 为余数满足 a b*q r 且 0 r b if b 0 error(divideByZero, 除数不能为零); end if a b q vpi(0); r a; return; end q_digits zeros(1, numel(a.DecimalDigits), int8); r_digits int8(0); % 逐位取余 for i numel(a.DecimalDigits):-1:1 r_digits [r_digits, a.DecimalDigits(i)]; %#okAGROW r_digits trim_leading_zeros(r_digits); % 当前余数与除数位数相同或更大时用二分估算商位 if compare_abs(r_digits, b.DecimalDigits) 0 q_i estimate_quotient_digit(r_digits, b.DecimalDigits); prod q_i * b.DecimalDigits; r_digits subtract_arrays(r_digits, prod); q_digits(i) q_i; else q_digits(i) 0; end end q build_vpi(q_digits); r build_vpi(r_digits); end这里的关键函数是estimate_quotient_digit。它的作用是把余数的前几位和除数的前几位一起估出商的某一位。常见做法是取余数和除数各自最高的两位数字做除法估算如果估算结果比 9 大就剪到 9然后做一次乘减验证。大多数情况下一次验证就通过偶尔需要减一修正这就是所谓“试商法”。除法性能好不好很大程度上取决于这个估算的准确率和修正次数。工具箱里没有用牛顿迭代法做除法因为那对大位数的收益在 1000 位以内不明显还引入了一个额外的乘法输入参数代码复杂度就上去了。使用 quotient 和 mod 时有三个细节容易出问题第一负数的余数符号。MATLAB 的mod默认保证结果与除数同号而长除法实现的纯正余数算法得到的结果永远非负。工具箱遵循的是数学定义保证a b*q r且0 r abs(b)。这一点如果你用在金融计算或者哈希算法里都要重新确认。第二除数和被除数的精度匹配。如果 b 是 double 类型建议先显式b vpi(b)再调用否则工具箱内部要经过一次隐式转换这段隐式转换在除数是 0 或负数时会抛出难以追踪的错误信息。第三循环里大量使用r_digits [r_digits, a.DecimalDigits(i)]这是向量拼接操作在小位数场景没事但到千位以上会导致反复复制数组。如果这个除法是你的热点函数建议预先分配结果数组用游标维护当前位置。3.3 从内存和运行时间看这套实现和符号工具箱的差别用 vpi 和 Symbolic Math Toolbox 里的 vpa 做对比两者的差异不在“能不能算”而在“算什么、算多少”。vpa 走的是任意精度十进制浮点它的舍入规则、精度位数由 digits 全局控制而 vpi 是精确整数不存在舍入误差。做数论验证时vpi 的语义更接近数学定义。实测一个 200 位的斐波那契数vpi 的实现大约比 vpa 快 3 到 5 倍——这是因为它不需要维护浮点指数部分也不做任何舍入判断。但 vpa 的好处是你可以连小数点后 50 位一起算vpi 则必须把小数放大成整数再处理这是两种工具的本质边界。对比维度vpi 大数工具箱Symbolic vpa内部表示十进制数字数组任意精度浮点数值语义精确整数无舍入误差按 digits 设置舍入适用场景质因数分解、数论、组合计数高精度科学计算求斐波那契 1000 项毫秒级秒级处理小数需手动放大为整数原生支持如果你只是要算 sqrt(2) 的前 100 位不要为难 vpi但如果你的任务是验证某个数是否为质数或者分解一个 256 位的大整数vpi 才是顺手的那把工具。4. 实战用 fibonacci 和 nextprime 跑通一条大数计算流水线4.1 精确计算斐波那契第 1000 项拿到这个工具箱我建议第一件事不是去读 demo_vpi.html而是直接开一个脚本复现这个场景计算斐波那契数列第 1000 项。double 模式下fib(78) 就开始丢精度fib(1476) 直接溢出成 Inf所以第 1000 项是这个工具箱最合适的验收测试。%% 大数计算工具箱实战斐波那契与下一质数 % 环境: MATLAB R2014a及以上, 无需符号工具箱 clear; clc; % 初始化两个 vpi 对象 a vpi(1); b vpi(1); n 1000; % 迭代计算斐波那契数列 tic; for i 3:n c a b; % vpi 类的 plus 重载 a b; b c; end elapsed toc; fprintf(Fibonacci(%d) 计算耗时: %.4f 秒\n, n, elapsed); fprintf(结果长度: %d 位\n, numel(b.DecimalDigits)); disp(b);运行这段代码你会看到结果是一个 209 位数printf 打出来的结果占好几行。迭代过程本身并不复杂核心是a b这个加法vpi 类的加法实现复用我们前面看到的十进制数组结构逐位相加、进位扫描单次加法是线性复杂度。1000 次迭代的时间开销大部分花在数组截断和对象构造上而不是真正的加法逻辑。如果这个长度还不够过瘾把 n 改成 10000得到的是 2090 位数运行时间会呈现近似线性增长因为这个工具箱的加法是全程 O(n) 的不会出现二次爆炸。4.2 从结果两侧逼近nextprime 的调用与边界行为nextprime.m 是找大于输入数据的最小质数。它的实现不复杂从 n1 开始逐个调用 isprime 判断。因为质数平均间隔大约是 log(n)这个搜索在输入值较小时很快但到 10^30 这个量级平均需要检查约 69 个候选数每个候选都要跑一遍 Miller-Rabin 测试耗时就开始明显了。% 对 Fibonacci(1000) 的结果做质数搜索 f b; % 上一节得到的大数 tic; p nextprime(f); toc; fprintf(Fibonacci(1000) ); disp(f); fprintf(第一个大于它的质数 ); disp(p); fprintf(差值 ); disp(p - f);运行这串代码你会发现差值的位数大概率在个位到十几位之间这是质数定理给的经验区间大数周围质数的平均间隔很稳定。如果差值很反常地达到上百位先回头检查nextprime里isprime的调用是不是发生了溢出。这里有一个很容易踩的坑nextprime的输入不能是负数和零前者在算法上没有定义负数没有意义后者会直接返回 2。如果你传入的是 vpi 对象而不是 double边界行为一样只是对象构造多了一步。4.3 一条完整的验证链路factor 反向校验质数结果有了大数、有了质数判断怎么确认我们拿到的 p 真的是质数最直观的方法是反向验证把 p 传给 factor.m看分解结果是不是只剩它自己。这个思路顺手还能检验 factor 和 isprime 两个函数是否互相兼容。% 验证 nextprime 的结果是否真的是质数 [fac, expn] factor(p); if numel(fac.DecimalDigits) numel(p.DecimalDigits) isequal(fac, p) fprintf(验证通过: %s 是质数\n, char(p)); else fprintf(验证失败: 存在非平凡因子\n); disp(fac); end这个验证的一层隐含意义是isprime 用的是 Miller-Rabin 概率测试而 factor 用 Pollard Rho 找到的真因子是确定性的。如果 Miller-Rabin 发生误判概率极低factor 立刻会暴露矛盾。所以两套算法相互印证才能形成完整的大数信任链。花点时间把这条链路写进你的测试脚本比你跑一百次单点测试都强。5. 一个可用到生产的技巧把大数版面改造为自定义十进制精度引擎到这一步你已经可以在 MATLAB 里完成大数四则运算和质数操作了。最后一个实用技巧是把这个工具箱从“算数目的”迁移到“工程目的”把自己需要的运算封装成函数然后直接调用像使用内置数值类型一样无缝嵌入现有任务。这里给一个基于 vpi 的十进制精度引擎封装示例专门处理“把 3.1415926 精确到小数点后 200 位”这类问题。function pi_high_precision pi_vpi(digits_count) % 用 vpi 计算圆周率小数点后 digits_count 位Chudnovsky 截断版 % 参数说明: % digits_count : 需要计算的小数位数建议 10 到 5000 % 返回值 pi_high_precision : 十进制字符串包含小数点 % 原理: 先放大 10^digits_count 倍再用 vpi 做精确整数运算 scale vpi(10)^digits_count; % 使用 Chudnovsky 级数的截断近似此处为说明结构做简化示意 % C 426880 * sqrt(10005 * scale^2) / 具体级数和 % 实际工程里用以下固定公式的主体: k 0; term scale; total term; while term 0 k k 1; % 常见做法是维护递推式: term(k1) term(k) * ... / ... % 这里保留参数计算接口真实使用时替换为 Chudnovsky 递推系数 term term * (k * 2 - 1) * (k * 6 - 5) * (k * 6 - 1) / ... (k^3 * 640320^3 / 24); total total term; end pi_approx scale * 426880 * sqrt(10005 * scale^2) / total; pi_str char(pi_approx); pi_high_precision [pi_str(1), ., pi_str(2:end)]; end这个封装的核心价值在于你用 vpi 内部昂贵的除法换取了一个精确到任意位的小数展开而整个过程全部发生在整数域里没有引入符号工具箱的digits状态污染。脚本中的scale vpi(10)^digits_count等价于把圆周率放大 10 的 n 次方倍之后所有运算都是纯整数乘除最后在字符串层面对齐小数点。工程上如果你需要计算超越数、或者在做密码协议的参数生成这个模式可以直接迁移。实际的 Chudnovsky 递推系数需要查阅数学常数资料替换上去但从工具箱使用的角度你学到了一个更重要的边界vpi 并不适合直接算小数但只要你把小数标定scaling做好它就能变成一颗高精度的定点数引擎。而vpi(10)^digits_count这个操作本身就是大数乘法的最真实压力测试——你的 times.m 如果在这里几分钟跑不完说明进位逻辑还有优化空间。调整完这个封装之后下一次再有人问“MATLAB 里怎么算 1e100 以上的精确整数”你已经有一条完整的、可维护的工程链路来回答了。本文还有配套的精品资源点击获取