文章目录

高精度
你有没有遇到过这样的问题?
计算 50 的阶乘时,用 long long 存储结果,得到的却是一串毫无意义的负数?
玩二游时,计算角色最终伤害值,明明手动算的是 2999.99 点,游戏面板却总喜欢取整直接按照 3000.00 点算?
再往大了说,天体物理中计算行星质量可能涉及上百位数字,金融行业的汇率转换、大额资金清算容不得一丝误差。
这些场景里,C++ 原生的 int/long long/float/double 显然"不够用"了:
- 整数类型有固定存储范围,超出就会"溢出",存不下超大数;
- 浮点类型因二进制存储特性,无法精确表示部分十进制小数,计算时会产生偏差。
那该如何解决这些问题?答案就是「高精度」。
什么是高精度?
高精度,简单说就是针对原生类型无法覆盖的"范围超限"(如大数)或"精度丢失"(如部分小数)问题,通过自定义方式(如数组/字符串模拟十进制)实现数字的精确存储、计算和传输,精准匹配实际需求,避免因原生类型的存储缺陷导致的误差和 bug。
它的核心思路特别好理解:就像我们手算加减法那样------把数字按位对齐,从最低位开始运算,遇到满十进一、不够减借一的情况手动处理。高精度本质就是让计算机"复现手算十进制的过程":用数组或字符串存储数字的每一位(包括小数点后),手动模拟对齐、运算、进位/借位规则,从而绕开原生类型的存储和精度限制,得到绝对精确的结果。
具体来说,高精度主要解决两类问题:
- 大数高精度:突破原生整数的存储范围,比如计算上百位的大数乘法、超大数阶乘,或是航天、物理领域的超大数值计算;
- 小数高精度:解决浮点类型的精度丢失问题,比如电商金额统计、金融汇率转换、税务计算等需要"分毫不差"的场景。
无论是大数还是小数,实现思路都是一致的:用数组/字符串存十进制每一位,模拟手算逻辑------这正是高精度能绕开原生类型缺陷的关键。
高精度的实现方式
高精度数的已知值与计算结果有两种核心存储形式:
- 存储在字符串中
- 存储在数组(或vector)中
这两种方法本质都是通过离散存储每一位数字模拟十进制表示,以规避普通数据类型的溢出限制。其中字符串更适配输入输出、临时存储场景(也可直接用于计算),数组/vector则更适合频繁计算或需反复参与运算的存储场景,是高精度运算的核心载体。
力扣高精度练习题
| 题型 | 核心考点 | 典型题目 |
|---|---|---|
| 大数加法(非负) | 位对齐、进位处理、前导零去除 | 415. 字符串相加 |
| 大数减法(非负) | 借位处理、被减数≥减数判断、前导零去除 | 415. 字符串相加 可以扩展为减法 |
| 大数乘法(非负) | 乘积位数预估、逐位相乘+进位累加、前导零去除 | 43. 字符串相乘 |
| 大数除法(非负) | 长除法模拟、试商优化、商/余数前导零处理 | 29. 两数相除(原生类型模拟高精度) |
| 小数高精度运算 | 小数点对齐、补零、末尾零去除 | 没有单一模板题 |
| 混合运算/复杂场景 | 结合栈、括号、优先级处理 | 224. 基本计算器、227. 基本计算器 II |
高精度运算原理详解
加减乘运算的核心逻辑高度统一(均围绕 "逐位运算 + 进位 / 累积"),因此我就以「二进制求和」为代表,拆解加减乘类高精度运算的通用思路。而除法运算逻辑与加减乘差异较大(核心是 "逐位商 + 余数迭代"),需单独拆解原理与实现。
1. 力扣 67 .二进制求和
在高精度场景中,二进制求和与十进制高精度加法的核心逻辑完全一致 ------ 本质都是「按位对齐、逐位运算、处理进位」,区别仅在于进制规则(二进制满 2 进 1,十进制满 10 进 1)。下面以力扣 67 题为例,详细拆解高精度二进制求和的实现原理和步骤。
题目描述
题目链接:力扣 67 .二进制求和
题目描述:

示例 1:
输入:a = "11", b = "1"
输出:"100"
示例 2:输入:a = "1010", b = "1011"
输出:"10101"
提示:1 <= a.length, b.length <= 104
a 和 b 仅由字符 '0' 或 '1' 组成
字符串如果不是 "0" ,就不含前导零
算法原理
1.按位对齐:二进制字符串的低位在右侧,因此从两个字符串的末尾(下标 size-1)开始,从右向左逐位处理(模拟手动加法的从右往左计算逻辑)。
2.逐位运算:对当前位的两个数字(若某字符串已遍历完,则该位补 0)加上上一轮的进位,计算当前位的和。
3.处理进位:二进制加法满 2 进 1,因此:
- 当前位结果 = 总和 % 2(取余得到当前位值)
- 新进位 = 总和 / 2(整除得到下一轮进位)
4.终止条件:两个字符串的所有位都遍历完毕,且进位为 0(若进位不为 0,需将最后的进位补到结果中)。
5.结果反转:由于计算时是从低位到高位存储,最终需要反转字符串得到正确的高位在前的结果。
代码实现
cpp
class Solution {
public:
string addBinary(string a, string b) {
int first = a.size() - 1, second = b.size() - 1, up = 0; // 双指针指向末尾,up为进位
string ret;
// 只要有一个字符串未遍历完,或有进位,就继续计算
while(first >= 0 || second >= 0 || up) {
// 计算当前位总和:a的当前位(无则补0) + b的当前位(无则补0) + 进位
up += (first >= 0 ? a[first--] - '0' : 0);
up += (second >= 0 ? b[second--] - '0' : 0);
// 当前位结果存入ret(先存低位,后续反转)
ret += (up % 2) + '0';
// 更新进位(满2进1)
up /= 2;
}
// 反转得到高位在前的正确结果
reverse(ret.begin(), ret.end());
return ret;
}
};
通用思路迁移说明
- 迁移到十进制高精度加法 :仅需将"进位规则"从"满2进1"改为"满10进1"(
carry % 10存当前位,carry /= 10更新进位)。 - 迁移到高精度减法:核心是"逐位减+借位",替换"逐位加+进位",流程仍为"双指针遍历→逐位运算→处理借位→结果整理"。
- 迁移到高精度乘法:外层遍历被乘数(从低位到高位),内层遍历乘数(从低位到高位),逐位计算乘积并累积到对应位置,最后处理进位,流程仍遵循"逐位运算→累积传递→结果整理"。
因此,我们就用二进制求和作为加减乘类高精度运算的"模板题",掌握其"按位处理+传递值(进位/累积)+结果整理"的核心,即可举一反三。
2.大数除法原理
与加减乘不同,大数除法的核心逻辑是 "逐位求商 + 余数迭代"------ 无法通过简单的 "逐位运算 + 进位" 实现,需模拟手动除法的 "取高位片段→求商→减积→补位" 流程,且需依赖 "大数比较""大数减法" 等辅助操作,因此单独作为一类高精度运算讲解。
场景说明
适用场景:被除数和除数均为超出 int/long long 范围的"大数"(如 100 位以上的数字),需以字符串形式存储和计算,最终返回商和余数的字符串结果(满足 被除数 = 除数 × 商 + 余数,且余数 < 除数)。
示例 1:
输入:被除数 = "123456789",除数 = "123"
输出:商 = "1003713",余数 = "90"(验证:123×1003713 + 90 = 123456789)
示例 2:输入:被除数 = "1000000000000000000000",除数 = "999"
输出:商 = "1001001001001001001001",余数 = "1"(验证:999×1001001001001001001001 + 1 = 1000000000000000000000)
约束:
- 被除数和除数均为非空字符串,仅由数字 '0'-'9' 组成
- 除数不为 "0",且字符串不含前导零(非 "0" 时)
算法原理
前置依赖:
由于除数和被除数均为大数,需先实现两个核心辅助函数:
- 大数比较函数 :判断两个大数字符串
a和b的大小,返回 1(a > b)、0(a == b)、-1(a < b) - 大数减法函数 :假设
a >= b,返回a - b的字符串结果(自动去除前导零)
核心步骤:
1.边界处理:
- 若被除数 < 除数:商为 "0",余数为被除数本身
- 若除数为 "1":商为被除数,余数为 "0"
2.初始化:
- 双指针(或单指针)从被除数的最高位开始遍历,逐位将数字并入「当前余数片段」
- 商字符串用于存储结果(高位在前),标记位
hasLeadingZero用于处理商的前导零
3.逐位求商
这一步是大数除法的核心,完全模拟手动除法中"取高位片段→算当前商→更余数"的过程,每一步都对应手动计算的直观逻辑,具体拆解如下:
(1)补位:将被除数当前位并入「当前余数片段」
手动除法中,我们会从被除数最高位开始,依次取1位、2位...直到片段能被除数整除或比较,这里的"当前余数片段"就是这个"待计算片段"。
- 操作:遍历被除数时,每次取当前位的数字(如被除数是"123456",当前遍历到'4'),追加到「当前余数片段」的末尾(若之前片段是"123",则变为"1234")。
- 目的:逐步构建足够大的"待计算片段",使其能与除数进行比较和减法运算。
(2)去前导零:保证片段格式有效
由于被除数可能存在高位补位(如前一步片段是"0",补下一位后变成"04"),前导零会影响后续比较(如"04"和"123"比较时,长度判断会出错),因此必须去除:
- 操作:用
find_first_not_of('0')找到片段中第一个非零字符的位置,截取从该位置到末尾的子串;若片段全为零(如"0000"),则重置为"0"。 - 示例:片段"00123"→去除前导零后为"123";片段"0000"→重置为"0"。
- 目的:确保「当前余数片段」是无前导零的有效数字字符串,避免比较逻辑出错。
(3)比较片段与除数,计算当前位商
这一步完全对应手动除法中"算当前位商"的操作,核心是通过"减法计数"确定商的当前位:
-
情况1:「当前余数片段」≥ 除数(如片段"123",除数"12")
- 逻辑:手动计算时,我们会想"123里有多少个12?"------ 这里通过循环减法实现:每次用片段减去除数,计数加1,直到片段小于除数为止,最终计数就是当前位商。
- 操作:
- 初始化计数
count = 0; - 循环:只要「当前余数片段」≥ 除数,就执行
片段 = 片段 - 除数(调用大数减法函数),同时count++; - 将
count转换为字符(如count=3→ '3'),存入商字符串。
- 初始化计数
- 示例:片段"123",除数"12":123-12=111(count=1)→ 111-12=99(count=2)→ ... → 最终 count=10(12×10=120),片段变为"3",当前位商为'10'?不,注意:商的每一位只能是0-9的数字 ,这里循环减法的次数一定在0-9之间(因为片段是"被除数逐位补成的",长度最多比除数多1位,因此最多能减9次除数)。
更正示例:片段"456",除数"123":456-123=333(count=1)→ 333-123=210(count=2)→ 210-123=87(count=3),此时87<123,循环停止,count=3,当前位商为'3',片段更新为"87"。
-
情况2:「当前余数片段」< 除数(如片段"87",除数"123")
- 逻辑:手动计算时,若当前片段不够除,就商0,然后补下一位再算。
- 操作:
- 当前位商为0;
- 若商字符串还未开始记录有效数字(即存在前导零),则跳过(不存入商字符串);
- 若商字符串已记录有效数字(如之前已存入'1'、'0'),则将'0'存入商字符串(避免商出现"13"变成"103"的错误)。
- 示例:商字符串已有"100"(前三位商),当前片段"8" < 除数"123",则存入'0',商变为"1000"。
4.终止条件:遍历完被除数的所有位,此时「当前余数片段」即为最终余数
5.结果整理:
- 若商字符串为空(全为前导零),则商为 "0"
- 最终余数去除前导零(若为空则补 "0")
代码实现
辅助函数实现
cpp
// 大数比较:a和b为无前导零的数字字符串,返回1(a>b)、0(a==b)、-1(a<b)
int compareBigNumber(string a, string b) {
if (a.size() != b.size()) return a.size() > b.size() ? 1 : -1;
for (int i = 0; i < a.size(); i++) {
if (a[i] != b[i]) return a[i] > b[i] ? 1 : -1;
}
return 0;
}
// 大数减法:假设a >= b(无前导零),返回a - b的字符串结果(去除前导零)
string subtractBigNumber(string a, string b) {
string res;
int i = a.size() - 1, j = b.size() - 1;
int borrow = 0; // 借位
while (i >= 0) {
int digitA = a[i] - '0';
int digitB = j >= 0 ? (b[j] - '0') : 0;
int diff = digitA - digitB - borrow;
if (diff < 0) {
diff += 10; // 借位补10
borrow = 1;
} else {
borrow = 0;
}
res += (diff + '0');
i--;
j--;
}
// 反转并去除前导零
reverse(res.begin(), res.end());
size_t pos = res.find_first_not_of('0');
return pos == string::npos ? "0" : res.substr(pos);
}
大数除法核心代码实现
cpp
// 大数除法:被除数dividend、除数divisor均为无前导零的数字字符串,返回pair<商, 余数>
pair<string, string> bigNumberDivide(string dividend, string divisor) {
string quotient; // 商结果
string remainder; // 当前余数片段
bool hasLeadingZero = true; // 标记商的前导零
// 边界情况1:除数为0(非法输入,此处抛出异常)
if (divisor == "0") throw invalid_argument("除数不能为0");
// 边界情况2:被除数 < 除数,商为0,余数为被除数
if (compareBigNumber(dividend, divisor) < 0) {
return {"0", dividend};
}
// 边界情况3:除数为1,商为被除数,余数为0
if (divisor == "1") {
return {dividend, "0"};
}
// 核心流程:逐位处理被除数,迭代更新余数和商
for (char c : dividend) {
// 1. 余数片段补当前位
remainder += c;
// 去除余数片段的前导零(避免"00123"这类无效格式)
size_t pos = remainder.find_first_not_of('0');
if (pos != string::npos) {
remainder = remainder.substr(pos);
} else {
remainder = "0"; // 余数片段全为0时,重置为"0"
}
// 2. 计算当前位商:统计余数片段能减多少次除数
int count = 0;
while (compareBigNumber(remainder, divisor) >= 0) {
remainder = subtractBigNumber(remainder, divisor);
count++;
}
// 3. 存储当前位商(跳过前导零)
if (count != 0 || !hasLeadingZero) {
quotient += (count + '0');
hasLeadingZero = false;
}
}
// 结果整理:商为空时补"0",余数去除前导零
if (quotient.empty()) quotient = "0";
size_t remPos = remainder.find_first_not_of('0');
if (remPos != string::npos) {
remainder = remainder.substr(remPos);
} else {
remainder = "0";
}
return {quotient, remainder};
}
下题预告
接下来,我们开启「栈」的专项训练!1047.1047. 删除字符串中的所有相邻重复项,栈作为数据结构中的"实用派",核心场景覆盖括号匹配、逆波兰表达式求值、单调优化等,每道题都会聚焦「什么时候压栈、什么时候弹栈、栈中存什么」的核心逻辑,从基础模拟到进阶技巧逐步推进~
Doro 带着小花🌸来啦!🌸奖励🌸坚持攻克重难点的你!复杂问题拆解后也能轻松应对,下一道题虽然是基础题,但细节决定成败,跟着思路一步步梳理,一定能写出简洁高效的代码~ 如果这篇本文章对你有帮助别忘了点赞,收藏哦~,关注这个博主后续刷题时直接找到他随时回顾知识点,也欢迎在评论区分享你的解题感悟,我们下道题不见不散!
