120 lines
4.7 KiB
Markdown
120 lines
4.7 KiB
Markdown
# **C++ 高精度减法讲义**
|
||
|
||
#### 1. 思想
|
||
|
||
既然高精度加法可以通过“反转 -> 补零 -> 逐位相加”的流程简化计算,减法是否也可以呢?
|
||
|
||
答案是肯定的!通过**完全相同的预处理步骤**,我们同样可以将减法问题转化为一个从左到右的、下标对齐的简单循环。
|
||
|
||
**产生了新的问题:**
|
||
|
||
我们已经知道如何用字符串来模拟大数的加法。减法同样可以模拟,但比加法多了三个问题:
|
||
|
||
* **预处理**:减法必须先判断大小,确定结果的符号。
|
||
* **计算**:将进位(`carry`) 逻辑替换为借位(`borrow`) 逻辑。
|
||
* **后处理**:减法的结果可能产生前导零 (例如 `123-120=003`),需要特殊处理。
|
||
|
||
#### 2. 算法步骤详解
|
||
|
||
**判断符号 (这是减法独有的)**
|
||
|
||
* 在计算前,必须比较 `a` 和 `b` 的大小。
|
||
* 创建一个 `bool negative = false;` 标志位。
|
||
* 如果 `b` 比 `a` 大,说明结果是负数。我们记下 `negative = true;`,然后交换 `a` 和 `b`。
|
||
* **目的**:通过这一步,我们保证了接下来的计算永远是 `大数 - 小数`,极大地简化了借位逻辑。
|
||
|
||
**反转与补零 (与加法完全一致)**
|
||
|
||
* 将 `a` 和 `b` (已经是大数和小数了) 同时反转。
|
||
* 在较短的字符串**末尾**补 `0`,使其与较长的等长。
|
||
* **目的**:让原本的“个位、十位、百位...”对齐到字符串的索引 `0, 1, 2...`,从而可以使用一个简单的 `for` 循环从左到右处理。
|
||
|
||
**核心循环——逐位相减**
|
||
这是算法的核心,我们将在这里实现借位逻辑。
|
||
|
||
* 创建一个 `int borrow = 0;` 用于记录借位。
|
||
* 循环从 `i = 0` 开始:
|
||
1. **取数计算**:`diff = (a[i]-'0') - (b[i]-'0') - borrow;`
|
||
* `a[i]` 是被减数,`b[i]` 是减数。
|
||
* `borrow` 是来自**更低位**的借位请求。如果上一轮(`i-1`)发生了借位,`borrow` 就是 `1`,意味着当前位的 `a[i]` 已经被借走了一个 `1`。
|
||
2. **判断并处理借位**:
|
||
* 如果 `diff < 0`,说明当前位不够减。
|
||
* `diff += 10;` (向更高位借来的 `1` 在本位体现为 `10`)。
|
||
* `borrow = 1;` (设置借位标记,告诉**下一轮循环**,它的 `a[i+1]` 被借走了)。
|
||
* 如果 `diff >= 0`,说明够减。
|
||
* `borrow = 0;` (重置借位标记)。
|
||
3. **拼接结果**:`ans += to_string(diff);`
|
||
|
||
**去除前导零 (在反转前操作)**
|
||
|
||
* 我们得到的 `ans` 是一个倒序的结果。例如,`123 - 120` 的计算结果 `3`,在 `ans` 中会是 `"300"`。
|
||
* 如果直接反转,会得到 `"003"`。
|
||
* 所以,我们必须在**最终反转之前**,去除 `ans` **末尾**多余的 `0`。
|
||
* **注意**:要保留最后一位 `0` (例如 `10 - 10` 的结果是 `0`),所以循环条件是 `ans.size() > 1`。
|
||
|
||
**最终反转与添加符号**
|
||
|
||
* 将清理好的 `ans` 字符串反转回来,得到正确的顺序。
|
||
* 检查设置的 `negative` 标志位。如果为 `true`,在结果的最前面加上 `"-"`。
|
||
|
||
#### 3. 完整代码实现
|
||
|
||
```cpp
|
||
// 比较函数,判断 a 是否大于 b
|
||
bool isGreater(string a, string b) {
|
||
if (a.length() != b.length()) {
|
||
return a.length() > b.length();
|
||
}
|
||
return a > b; // 长度相同时,直接按字典序比较
|
||
}
|
||
|
||
string sub(string a, string b) {
|
||
// 判断符号,并确保 a >= b
|
||
bool negative = false;
|
||
if (isGreater(b, a)) {
|
||
negative = true;
|
||
swap(a, b);
|
||
}
|
||
|
||
// 将两个输入字符串都反转
|
||
reverse(a.begin(), a.end());
|
||
reverse(b.begin(), a.end());
|
||
|
||
// 在较短的字符串末尾补 '0',使其与较长的等长
|
||
while (b.size() < a.size()) b += '0';
|
||
|
||
string ans = "";
|
||
int borrow = 0;
|
||
|
||
// 核心循环,逐位相减
|
||
for (int i = 0; i < a.size(); i++) {
|
||
// 计算当前位的差值(考虑上一位的借位)
|
||
int diff = (a[i] - '0') - (b[i] - '0') - borrow;
|
||
|
||
// 判断是否需要向更高位借位
|
||
if (diff < 0) {
|
||
// 当前位不够减,需要借位
|
||
diff += 10; // 借来的 1 在本位变成了 10
|
||
borrow = 1; // 产生了借位,标记下一轮计算要被借走
|
||
} else {
|
||
// 当前位够减,无需借位
|
||
borrow = 0; // 重置借位标记
|
||
}
|
||
// 将当前位的结果拼接到 ans 末尾
|
||
ans += to_string(diff);
|
||
}
|
||
|
||
// 在反转前,把倒序结果末尾的 0 去掉 (处理前导零)
|
||
while (ans.size() > 1 && ans.back() == '0') {
|
||
ans.pop_back();
|
||
}
|
||
|
||
// 将最终结果反转回来
|
||
reverse(ans.begin(), ans.end());
|
||
|
||
// 如果结果为负,添加负号
|
||
if (negative) ans = "-" + ans;
|
||
|
||
return ans;
|
||
}
|
||
``` |