数组形式的整形加法:从竖式思路到三种语言落地

发布时间:2026/10/8 4:01:39
数组形式的整形加法:从竖式思路到三种语言落地 前两天在改一段老代码接到一个看上去很基础的需求两个很大的整数分别以数组形式给出来要求把它们精确相加结果同样以数组形式返回。很多人第一反应是“直接转成整数加不就完了”但真在 JS、C 或者更长位数的场景里试过就会发现常规整数类型根本装不下。这个需求有个很形象的叫法——数组形式的整形加法。它本质上是把整数按位拆进数组用模拟竖式的方式逐位相加、逐位进位最后再拼接收尾。这篇文章就从思路到落地把这一整套逻辑完整拆开适合正在学数据结构和算法、刷题的同学也适合日常要处理大数精度问题的开发。1. 项目概述数组形式的整形加法到底在解决什么问题1.1 这个需求从哪里来数组形式的整形加法不是凭空想出来的概念它最常见的来源有三个。第一个是算法题场景。也就是“字符串相加”“链表相加”这类题目把数字按位拆开存放然后逐位计算。热词里出现的[1,2]、js怎么取出数组其实就是这个场景的典型问题接口或题目给你的是一堆数字字符你得先想办法把它们变成可计算的数值再完成加法。第二个是真实的工程需求。比如金融系统里处理超长金额、订单号、ID 号如果数值位数超过语言默认整数类型的精度上限直接相加就会出错。这一块在 JavaScript 里尤其常见因为Number类型是双精度浮点超过安全范围后的整数计算会丢精度结果根本不是你以为的那个数。第三个是底层课程里的高精度计算。 C 语言课程设计里经常要求自己写一个大数加法输入用字符串形式内部用字符数组保存再用-0把字符转换成数值。为什么 C 语言里多用字符数组而不是 int 数组因为键盘输入天然就是字符串读进来就是字符数组你要面对和处理的就是这个形态。这个需求本质上要解决的是当“纸面写不下的数字”出现时如何用数组这个基础容器把大数拆成一位一位的小数位来算。核心逻辑跟小学竖式一模一样只是从草稿纸搬到了内存里。1.2 “数组”与“整形”组合后的真实形态先解释一下“整形”这个词。编程语境下“整形”和“整型”都是指整数类型 integer大家平时写的时候常常混用我看到标题“数组形式的整形加法”第一反应就是“整型加法”不用太纠结写法理解成整数类型就行。这个需求里数组和整形的组合通常有两种典型形态。第一种是字符数组形态。比如[1,2,3]每个元素是一个数字字符。JSON 接口返回的数组经常是这种形态C 语言从终端读入的字符串本质上也是这种形态。处理这种数组第一步永远是把字符转成数值JS 用map(Number)C 语言用c - 0。第二种是整型数组形态。比如[1,2,3]每个元素直接就是一个数值。Python、Java 里处理这种数组更直接拿到手就可以参与算术运算不需要额外转换。除了这两种还有由字符数组衍生出来的复杂形态比如指针数组存放字符串。C 语言里写char *nums[] {123, 98};这就是一个数组里面每个元素是一个字符串的起始地址。访问nums[0][1]就能拿到第一个字符串的第二位字符。很多人在“指针数组存放字符串”上绕不过弯其实把它理解成“数组里每个元素又是一个字符串的开头”就清楚多了。顺带说一句用数组承载离散化数据是个很大的思路不只是加法。有人把 png 图片在线转成 rgb565 的 C 语言数组做嵌入式屏幕显示这是数组应用的另一个方向。回到加法我们只需要关注数字数组本身。搞清楚这几种形态和它们之间的转换方式后面写代码时就不会一头雾水。2. 核心思路拆解为什么不能直接把数组转成整数相加2.1 整数类型的精度上限和溢出很多人不理解为什么明明有整数类型还要绕一圈用数组答案很简单现有的整数类型装不下。拿 JavaScript 举例Number类型是 IEEE 754 双精度浮点数能安全表示的最大整数是2^53 - 1也就是9007199254740991。超过这个值相邻整数就没法区分了。你试着在控制台输入9007199254740992 1得到的结果仍然是9007199254740992加了个寂寞。这种精度丢失在做大数运算时非常致命。C 语言里也好不到哪去。int类型通常 32 位上限约 21 亿long long勉强到 19 位十进制数再大的数一样溢出。有些语言比如 Python 自带大整数但很多场景要求你手写实现比如自定义大数类或者算法题明确不让你依赖语言特性。这就像一张纸写不下很长的数字时你会拿多张纸分开记。数组就是一种“多张纸”的组织方式每个下标是一格每一格存一位数字位数再多也就是多占几个下标而已。这就是数组形式的整形加法的存在意义——用空间换精度。2.2 字符数组与整型数组的取舍既然要用数组第一步要决定的就是用字符数组还是整型数组。这两种方案各有适用场景我列个表对比一下。对比维度字符数组 [1,2,3]整型数组 [1,2,3]输入来源键盘/接口直接得到无需额外构造需要从字符或其他类型转换存储占用每个字符 1 字节相对紧凑每个数字占 4 或 8 字节占空间更大计算前处理需要-0转换才可运算可直接参与算术运算调试观感打印直观但容易忘转换数值直观转换环节少典型语言场景C 字符串、JS JSON 接口Python 列表、Java ArrayList实际工程里怎么选主要看数据从哪里来。JSON 接口返回的[1,2]你不可能先手动改成[1,2]再接收常见的做法是拿到手后整体转换一次。C 语言里键盘输入“123”内存里存的就是字符数组逐位相加前必须把字符转成数字。转换公式本身不难num c - 0因为 ASCII 码里数字字符0到9是连续排列的。难点在于很多人转换时写反变成0 - c结果全是负数输出一团糟。我的建议是不要一上来就纠结哪种形态更好先把输入是什么形态搞清楚再决定转换成本。如果输入已经是整型数组直接算如果是字符数组放心大胆用map(Number)或者c - 0转一遍这个开销完全可以接受。2.3 进位逻辑与方向选择数组加法的核心逻辑是模拟竖式。小学列竖式时从个位开始从右往左逐位相加满十进一。数组版本一模一样只是把“从右往左”落实成下标遍历。这里有一个方向问题非常关键且容易被忽略。数组本身有两种存放方向低位在前nums[0]是个位比如[3,2,1]表示数字 123。高位在前nums[0]是最高位比如[1,2,3]表示数字 123。如果数组是低位在前遍历顺序就是从 0 到 len-1如果是高位在前就要从 len-1 倒序遍历。不同题目、不同接口的定义不一样动手写代码前一定要先看清楚。我见过太多人因为没有确认方向把结果整个算反排查半天才发现问题出在遍历起点。方向确定后进位逻辑就固定了。每轮计算三个输入左数组当前位、右数组当前位、上一轮的进位。求和后sum % 10是当前位要写入的结果Math.floor(sum / 10)是传给下一轮的进位。循环条件是i 0 || j 0 || carry 0三个条件用“或”连接意思是只要还有一位没处理完或者还有进位没消化循环就不能停。这个条件是我反复强调的点漏掉最后一个carry 0最高位的进位就会凭空消失。3. 三种语言落地数组加法的完整实现3.1 JavaScript字符串数字数组的取出与转换先解决热词里那个最基础的问题[1,2]怎么取出数字。最常见的写法是map(Number)const arr [1, 2]; const nums arr.map(Number); // [1, 2]这里有一个经典陷阱很多新手一看就会中招如果写成arr.map(parseInt)结果会变成[1, NaN]。原因在于map会传给回调函数三个参数当前元素、当前下标、整个数组。parseInt的第二个参数恰好是进制 radix于是parseInt(2, 1)因为进制不合法直接返回NaN。以前很多公司的面试就爱考这个点。用map(Number)是安全的因为Number转换只关心第一个参数下标对它没有影响。不过为了严谨也可以写成arr.map((s) Number(s))避免后续维护的人顺手改成parseInt又踩坑。拿到数字数组之后实现逐位相加就很简单了。完整代码如下function arrayAdd(a, b) { const numA a.map(Number); const numB b.map(Number); let i numA.length - 1; let j numB.length - 1; let carry 0; const result []; while (i 0 || j 0 || carry 0) { const x i 0 ? numA[i] : 0; const y j 0 ? numB[j] : 0; const sum x y carry; result.push(sum % 10); carry Math.floor(sum / 10); i--; j--; } return result.reverse(); } console.log(arrayAdd([1, 2, 3], [9, 8])); // [2, 2, 1]为什么最后要reverse()因为循环从低位开始每算出一位就push到数组末尾得到的结果是倒序。以 123 98 为例个位 3811push 1进位 1十位 29112push 2进位 1百位 1012push 2。此时 result 是[1,2,2]反转后才是正确的[2,2,1]。还有一个细节Math.floor(sum / 10)不要写成sum / 10。JS 的除法直接得到浮点数如果进位是0.2、1.5这种值后续逻辑全乱。用Math.floor取整最干净或者用parseInt(sum / 10, 10)。3.2 C语言字符数组逐位运算与指针细节C 语言里没有字符串类型字符串就是字符数组结尾有一个\0。从键盘读入“123”内存里a[0]1、a[1]2、a[2]3、a[3]\0。逐位相加前必须把每个字符转成数值公式是a[i] - 0结果拼回字符时再用(sum % 10) 0。这两个转换各有一个常见错误忘记转换直接拿字符参与加法比如3的 ASCII 码是 51算出来的结果彻底乱套转换回来了结果数组却忘了补\0输出时后面跟一大串乱码。完整实现我贴出来#include stdio.h #include string.h void add_arrays(const char *a, const char *b, char *out) { int i strlen(a) - 1; int j strlen(b) - 1; int carry 0; int pos 0; while (i 0 || j 0 || carry 0) { int x i 0 ? a[i] - 0 : 0; int y j 0 ? b[j] - 0 : 0; int sum x y carry; out[pos] (sum % 10) 0; carry sum / 10; i--; j--; } out[pos] \0; // 结果反转用双指针 int left 0; int right pos - 1; while (left right) { char tmp out[left]; out[left] out[right]; out[right] tmp; left; right--; } } int main() { char result[100]; add_arrays(123, 98, result); printf(%s\n, result); // 221 return 0; }C 语言版本里字符串反转我建议用双指针一个指针指头部一个指尾部往中间走逐个交换字符。这样做不会引入额外内存时间复杂度也是最优的。有些人习惯每次用插入操作把字符放到头部那样每次都要移动后面所有字符效率很差。再提一下指针数组。如果多个数字字符串放在一起比如char *nums[] {123, 98};nums[0]指向第一个字符串nums[1]指向第二个。取第 k 位就是nums[0][k]。这也呼应了“数组指针移动指定位输出字符”的用法——本质上就是通过指针或下标定位字符再逐位处理。3.3 Python数组切片与简洁实现Python 里列表就是数组配合切片命令能写出非常简洁的加法。先列几个最常用的切片arr[::-1]反转整个列表arr[1:]去掉第一个元素arr[:-1]去掉最后一个元素arr[1:3]取出下标 1 到 2 的元素字符列表转数字列表一行列表推导式搞定[int(x) for x in arr]。这个写法比map(int, arr)更直观而且可以随时加过滤条件。完整实现如下def array_add(a, b): a [int(x) for x in a] b [int(x) for x in b] i, j len(a) - 1, len(b) - 1 carry 0 res [] while i 0 or j 0 or carry: x a[i] if i 0 else 0 y b[j] if j 0 else 0 s x y carry res.append(s % 10) carry s // 10 i - 1 j - 1 return res[::-1] print(array_add([1, 2, 3], [9, 8])) # [2, 2, 1]Python 的while条件里直接写or carry就行因为carry为 0 时等价于 False为 1 时等价于 True。carry s // 10是整数除法不会出现浮点数问题。最后返回res[::-1]这是 Python 切片反转的经典操作比res.reverse()更适合在返回语句中直接使用因为reverse()会原地反转并返回 None。4. 边界情况与典型问题排查4.1 长度不对齐与反转遗漏两个数组长度不一致是最常见的情况。比如 123 和 98一个是三位一个是两位最直观的错误是拿着数组下标从头开始对应相加这样十位和个位会对错。解决办法就是前面提到的低位对齐两个指针分别指向各自末尾从个位开始算某一边先走完了缺的位按 0 处理。我在实际代码里看到最多的错误还有反转遗漏。很多人算出了结果但输出是倒序。因为从低位开始遍历结果是按“个位、十位、百位”的顺序 append 的最后必须做一次整体反转。JS 用reverse()Python 用切片[::-1]C 语言用双指针手动交换。反转这一步常常被忘尤其是在逻辑写得很顺的时候最容易在最后关头出问题。4.2 连续进位与最高位溢出连续进位是数组加法里最考验细节的地方。举一个极端例子999 1。个位9 1 10结果位写 0进位 1十位9 0 1 10结果位写 0进位 1百位9 0 1 10结果位写 0进位 1循环结束进位还是 1最高位要追加一个 1最终结果是[1,0,0,0]也就是 1000。如果 while 循环条件漏写carry 0循环会在百位计算完就结束最高位的进位直接丢失结果是[0,0,0]错得离谱。这种连续进位的场景不是凭空想出来的算法题和实际大数计算里经常遇到。所以我把循环条件总结成一句口诀只要还有位没处理完或者还有进位没消化循环就不能停。三个条件用或连接一个都不能少。4.3 前导零、负数与“假数组”前导零的问题容易被忽略。如果输入是[0,0,1]它表示数字 1。逐位相加本身结果是对的但输出结果可能带着多余的前导零比如[0,0,1]。要不要去掉前导零取决于业务语义如果是纯数值规范输出应该去掉如果是固定宽度的编号比如补足四位显示前导零必须保留。动手前先确认需求不要在代码里硬编码。负数处理是另一个边界。数组加法的基础形态只适用于非负整数。如果遇到负数常规做法是把符号单独抽出来用一个标志位记录正负两个数组只存绝对值再根据符号组合决定是加还是减。减法的借位处理比加法复杂不少所以算法题如果不要求负数千万不要自己加戏如果明确了先把符号逻辑拆开再谈逐位运算。“假数组”这个坑在真实项目里很常见。JS 接口偶尔返回的不是真数组而是类数组对象比如{0: 1, 1: 2, length: 2}直接调用map会报错。稳妥做法是先判断Array.isArray(arr)或者用Array.from(arr)转成真数组再操作。这个问题在简单算法题里不会出现但放在真实接口开发里非常值得警惕。5. 常见问题速查与避坑清单5.1 高频问题排查表我把这类需求里反复出现的问题整理成一张速查表方便你排查时对照。问题现象根本原因解决办法结果数组多出逗号数组直接转字符串如[1,2,3]隐式转为1,2,3用join()拼接别用toString()arr.map(parseInt)出现 NaNparseInt把下标当进制参数改用map(Number)或显式回调C 语言输出乱码字符数组结尾没有补\0运算结束时给结果数组补\0结果整体倒序从低位遍历完忘了反转JS 用reverse()Python 用[::-1]C 双指针最高位丢失while 循环条件漏掉carry 0三个条件用或连接检查循环边界字符转数字出错写成0 - c牢记是c - 0数组长度不一对错位同一下标直接相加低位对齐缺位补 0前导零未处理没考虑数值语义确认需求后统一 trim 或保留这张表里最值得说的还是map(parseInt)那个问题。我见过不止一次写的时候完全没意识到调了半天发现单个元素变成了 NaN。排查的方法也很简单在转换后打印一次数组看看每个元素是不是符合预期。5.2 调试技巧与测试用例设计数组加法看起来简单坑全在边界上。我个人的习惯是写代码前先把测试用例列出来用例全过了再优化代码。必测的用例至少有这组0 0输入可能是全 0 数组也可能是空数组1 9单数位相加产生进位99 1两位连续进位999 1最高位溢出结果多一位两个长度不一致的大数字符数组形态比如[1,2]带前导零的输入调试时不要只看最终结果建议每一轮循环都打印i、j、x、y、sum、carry。对照竖式手动算一遍哪一轮出了问题一目了然。我自己调试这类代码时习惯先用最笨的竖式笔算再和程序输出对拍能快速定位是哪一位算错了。还有一个设计上的小技巧把“字符数组转数字数组”和“数组加法”拆成两个独立函数。转换出错和加法出错是两类问题拆开以后单测也能分开写。一旦结果不对先测转换再测加法定位速度快很多。项目开发中把这些代码写成可复用的工具函数也很有价值。同一个数组加法逻辑今天用在订单号处理上明天用在算法题里逻辑完全不变。抽出来以后只需要在调用处做好输入形态的适配就行。结尾我自己的一点体会做这类数组加法我踩过最深的一个坑是把输出方向的预期搞反。有一次接口返回的数组是高位在前我默认按低位在前处理结果所有超过 10 的相加结果全部倒序排查了大半天才反应过来。后来我养成一个习惯拿到题目或需求先把数据形态和方向用一行注释写在代码最上面再开始写循环。不要小看这一行注释它能帮你省下大量排查时间。另一个建议是不要觉得add函数太简单就不写测试。数组形式的整形加法几乎每个平台、每种语言都有一道对应的题无论是字符串相加还是高精度计算器核心逻辑都是同一套。只要把进位、对齐、反转这三件事练成肌肉记忆以后再遇到大数乘法、大数减法思路完全可以复用。最后再补充一个小细节JS 的reverse()会改变原数组如果后面还要用原数据记得先slice()拷贝一份再反转。C 语言手动双指针最稳也不会引入额外内存开销。希望这篇能帮你少走点弯路。