← 目录 / 算法文档 · 模块一 数学基础 / 1.10 高精度减法

1.10 高精度减法

和加法一样用数组模拟竖式运算,但这次要处理的是"借位",还要先弄清楚两个数到底谁比较大。

本页目录
① 核心思路:比较大小与借位

回忆一下小学怎么算"个位不够减"的减法,比如 36 - 19:个位 6 - 9 不够减,就向十位借 1 当 10 用。高精度减法沿用了上一节(1.9 加法)"数组倒序存储 + 逐位运算"的整体框架,但比加法多出两个新问题:

⚠️
两个新麻烦:
1️⃣ 结果可能是负数——如果被减数比减数小(比如 15 - 489),单纯按位相减会出现"不够减"的情况,需要提前判断好两数的大小关系;
2️⃣ 某一位不够减时要向高位"借位"——这和加法的"进位"刚好相反,进位是"往高位送 1",借位是"从高位借 1 用"。

为了避免结果出现负数,高精度减法通常约定"用大数减小数",如果被减数本来就比减数小,就先交换两者、算出差值后再在结果前面添上负号。判断两个高精度数谁大谁小,规则很直观:

489
vs
1500
📏
两条比较规则,按顺序判断:
1️⃣ 先比位数——位数多的数一定更大(没有前导 0 的情况下)。上面例子里 1500 是 4 位,489 是 3 位,1500 直接胜出,不需要再比第二条;
2️⃣ 位数相同则从最高位开始逐位比较——哪个数先出现更大的数字,哪个数就更大(一旦分出胜负立刻停止比较)。

确定了"大数 - 小数"之后,就可以从个位开始逐位相减。如果某一位不够减,就向前一位(更高的那一位)借 1 当 10 用,同时把那一位自己减 1(因为被借走了)。

热身:个位不够减,怎么借位
借出 1
10
·
0
十位
借入 1
313
8
5
个位
13 - 8 = 5 → 个位不够减,向十位借 1,结果是 5
个位 3 - 8 不够减,向十位借 1 当 10 用,变成 13 - 8 = 5;十位原本是 1,借出去 1 之后变成 0,再和减数的十位(这里没有,按 0 算)相减,还是 0。最终结果去掉多余的高位 0,就是 5。高精度减法要做的,就是把这个"不够减就向高位借 1"的动作,用循环重复很多次。
② 完整代码实现

先写一个独立的比较函数 IsLess,再串联成完整流程:读入两数 → 比较大小并在必要时交换 → 按位相减(带借位)→ 去掉前导 0 → 按符号输出。

C++ · 高精度减法(完整可运行程序)
1#include <iostream>
2#include <vector>
3using namespace std;
4
5bool IsLess(vector<int>& a, vector<int>& b)
6{
7 if (a.size() != b.size())
8 {
9 return a.size() < b.size(); // 位数不同:位数多的更大
10 }
11 for (int i = (int)a.size() - 1; i >= 0; i--) // 位数相同:从最高位开始比
12 {
13 if (a[i] != b[i]) // 找到第一个不同的位
14 {
15 return a[i] < b[i];
16 }
17 }
18 return false; // 每一位都相同,说明 a 等于 b
19}
20
21int main()
22{
23 string s1, s2;
24 cin >> s1 >> s2; // 读入两个数(字符串形式)
25
26 int len1 = s1.size(), len2 = s2.size();
27 vector<int> a(len1, 0), b(len2, 0); // 各自按实际位数开数组
28
29 for (int i = 0; i < len1; i++) // 把 s1 倒序拆进数组 a(下标0存个位)
30 {
31 a[i] = s1[len1 - 1 - i] - '0';
32 }
33 for (int i = 0; i < len2; i++) // s2 同样倒序拆进数组 b
34 {
35 b[i] = s2[len2 - 1 - i] - '0';
36 }
37
38 bool isNegative = false; // 先假设结果不是负数
39 if (IsLess(a, b)) // a 比 b 小,说明结果是负数
40 {
41 swap(a, b); // 交换后保证 a 永远是较大的数,才能用"大数-小数"的方式模拟
42 isNegative = true; // 记下来,最后输出的时候要加负号
43 }
44 b.resize(a.size(), 0); // 把 b 用 0 补到和 a 一样长,避免下面按下标访问时越界
45
46 vector<int> c(a.size(), 0); // 结果数组,和 a 一样长(减法结果不会比 a 位数更多)
47 for (int i = 0; i < (int)a.size(); i++) // 从个位(下标0)开始逐位相减
48 {
49 a[i] -= b[i]; // 本位直接相减,结果先暂存回 a[i]
50 if (a[i] < 0) // 减出负数,说明这一位不够减,需要借位
51 {
52 a[i] += 10; // 向高位借1当10用,本位加回10
53 a[i + 1] -= 1; // 高一位被借走1,要相应减1
54 }
55 c[i] = a[i]; // 这一位最终结果存进 c
56 }
57
58 int len = (int)c.size();
59 while (len > 1 && c[len - 1] == 0) // 去掉结果最高位多余的 0(比如 352-350=002,要变成 2)
60 {
61 len--;
62 }
63
64 if (isNegative && !(len == 1 && c[0] == 0)) // 结果是负数,且不是 0(0 不用加负号)
65 {
66 cout << "-";
67 }
68 for (int i = len - 1; i >= 0; i--) // 倒着输出,从最高位到个位
69 {
70 cout << c[i];
71 }
72 cout << endl;
73
74 return 0;
75}
运行示例
输入:6023 1458
输出:4565
输入:15 489
输出:-474 (被减数比减数小,结果带负号)
🧰
这里 vector 帮了两个忙:一是 ab 长度可以按各自输入的实际位数分配,不需要先猜一个固定大小;二是交换较大数之后,直接用 b.resize(a.size(), 0) 把较短的那个数用 0 补到和 a 一样长,之后 a[i] - b[i] 就能安全地按位访问,不用担心下标越界——这一步如果用普通数组,因为数组本身已经有固定大小、天然"自带"补 0,反而不用特地处理;但换成 vector 后,必须自己动手 resize 补齐,这是使用 vector 时容易漏掉的一个细节。
✏️
动手试一试:除了上面两组,再试试这几组输入检验自己的理解:489 489(结果应该是 0,注意不能输出"-0")、1000 1(十位、百位、千位连续借位)、两个 20 位以上的大数相减。如果结果不对,回到本节的追踪表格,逐行核对 a[i]b[i] 在每一轮循环里到底是什么值,通常问题就出在某一步的借位没有正确处理。
③ 竖式减法的思路:借位(详解)

上面完整代码里第 47~56 行的循环,就是下面要详细拆解的逐位相减与借位过程。回忆一下前面的热身例子(13-8=5):不够减就向高位借 1 当 10 用。下面用一个三位数的例子,把这个过程走一遍完整的逐位借位:

竖式演示:352 − 129 = 223
·
3
1
2
百位
借出 1
54
2
2
十位
借入 1
212
9
3
个位
从个位开始:个位 2 - 9 不够减,向十位借 1 当 10 用,本位变成 2+10=12,算出 12-9=3;十位借出 1 之后从 5 变成 4,再算 4-2=2(够减,不用再向百位借);百位没有被借位影响,直接 3-1=2。三位拼起来是 223,正好等于 352-129
🔗
如果十位自己也不够减怎么办?上面这个例子只借了一次位,十位借出 1 后还有富余(4 ≥ 2)。但如果十位借出 1 之后反而变得不够减(比如原本是 504 - 89,十位是 0,借出 1 后变成 -1),就要继续向百位借,借位可以像这样一路向更高位传递,直到借到足够为止——这也是为什么代码里的借位逻辑要放在循环里,每一位都要重新判断一次是否需要借位,而不是只判断一次。

光说"可以继续借位"还是有点抽象,下面就用一个真正需要连续借位三次的例子——6023 - 1458——把循环每一轮的状态都列出来:

i位置a[i] − b[i] 的计算c[i]对更高一位的影响
0个位3 − 8 = −5,不够减5(−5+10)借1:a[1] 从 2 变成 2−1=1
1十位1 − 5 = −4,不够减6(−4+10)借1:a[2] 从 0 变成 0−1=−1
2百位−1 − 4 = −5,不够减5(−5+10)借1:a[3] 从 6 变成 6−1=5
3千位5 − 1 = 4,够减4不需要借位

关键在于第 1、2 行:十位和百位在被计算之前,都已经因为上一轮的借位而先被扣掉了 1(十位从 2 变成 1,百位从 0 变成 -1),代码里正是靠 a[i+1] -= 1 这一行悄悄完成了这种"预先扣款",等循环走到 i=1i=2 时,a[i] 已经是修改过的新值了。把 c 从高位(下标 3)到低位(下标 0)读出来就是 4 5 6 5,也就是 4565,正好等于 6023 - 1458

④ 常见陷阱

把本节内容汇总成几条最容易踩坑的规则:

没有提前判断大小就直接相减:如果不确认被减数≥减数就直接按位相减,借位会一路借到最高位之外,得到完全错误的结果。务必先比较,让"大数减小数",符号单独在最后处理(如果原本是小数减大数,结果记得添加负号)。
忘记去掉结果的前导 0:减法很容易产生结果比原来位数少的情况(比如 504-500=4,但存储的数组高位是 0,0,4),如果不去掉多余的高位 0,直接按数组存储位数输出会变成"004",还带着两个多余的 0。
比较函数只比了位数,忘记位数相同的情况:判断大小的两条规则缺一不可——位数不同直接看位数,但位数相同时必须老老实实从最高位开始逐位比较,不能偷懒假设"位数相同就相等"。
忘记特判"结果为 0"时不能带负号:如果两数相减结果恰好是 0(比如 489 - 489),即使一开始判断出"需要交换、结果为负",也不应该在最终输出前打印负号——"负0"不符合正常的数字表达习惯,输出前必须加上这个特判。
🏆
接下来:高精度乘法(1.11 节)不再需要处理借位或大小比较,但会引入一个新概念——两个数各自某一位相乘的结果,要按"位置相加"的规则落到正确的结果位上,思路和加减法略有不同。