和加法一样用数组模拟竖式运算,但这次要处理的是"借位",还要先弄清楚两个数到底谁比较大。
回忆一下小学怎么算"个位不够减"的减法,比如 36 - 19:个位 6 - 9 不够减,就向十位借 1 当 10 用。高精度减法沿用了上一节(1.9 加法)"数组倒序存储 + 逐位运算"的整体框架,但比加法多出两个新问题:
15 - 489),单纯按位相减会出现"不够减"的情况,需要提前判断好两数的大小关系;为了避免结果出现负数,高精度减法通常约定"用大数减小数",如果被减数本来就比减数小,就先交换两者、算出差值后再在结果前面添上负号。判断两个高精度数谁大谁小,规则很直观:
1500 是 4 位,489 是 3 位,1500 直接胜出,不需要再比第二条;确定了"大数 - 小数"之后,就可以从个位开始逐位相减。如果某一位不够减,就向前一位(更高的那一位)借 1 当 10 用,同时把那一位自己减 1(因为被借走了)。
13 - 8 = 5 → 个位不够减,向十位借 1,结果是 53 - 8 不够减,向十位借 1 当 10 用,变成 13 - 8 = 5;十位原本是 1,借出去 1 之后变成 0,再和减数的十位(这里没有,按 0 算)相减,还是 0。最终结果去掉多余的高位 0,就是 5。高精度减法要做的,就是把这个"不够减就向高位借 1"的动作,用循环重复很多次。先写一个独立的比较函数 IsLess,再串联成完整流程:读入两数 → 比较大小并在必要时交换 → 按位相减(带借位)→ 去掉前导 0 → 按符号输出。
| 1 | #include <iostream> |
| 2 | #include <vector> |
| 3 | using namespace std; |
| 4 | |
| 5 | bool 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 | |
| 21 | int 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 | } |
a、b 长度可以按各自输入的实际位数分配,不需要先猜一个固定大小;二是交换较大数之后,直接用 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 用。下面用一个三位数的例子,把这个过程走一遍完整的逐位借位:
2 - 9 不够减,向十位借 1 当 10 用,本位变成 2+10=12,算出 12-9=3;十位借出 1 之后从 5 变成 4,再算 4-2=2(够减,不用再向百位借);百位没有被借位影响,直接 3-1=2。三位拼起来是 223,正好等于 352-129。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=1、i=2 时,a[i] 已经是修改过的新值了。把 c 从高位(下标 3)到低位(下标 0)读出来就是 4 5 6 5,也就是 4565,正好等于 6023 - 1458。
把本节内容汇总成几条最容易踩坑的规则:
504-500=4,但存储的数组高位是 0,0,4),如果不去掉多余的高位 0,直接按数组存储位数输出会变成"004",还带着两个多余的 0。489 - 489),即使一开始判断出"需要交换、结果为负",也不应该在最终输出前打印负号——"负0"不符合正常的数字表达习惯,输出前必须加上这个特判。