C++ 高精度计算的讲解 模拟 力扣67.二进制求和 题解 每日一题

文章目录

高精度

你有没有遇到过这样的问题?

计算 50 的阶乘时,用 long long 存储结果,得到的却是一串毫无意义的负数?

玩二游时,计算角色最终伤害值,明明手动算的是 2999.99 点,游戏面板却总喜欢取整直接按照 3000.00 点算?

再往大了说,天体物理中计算行星质量可能涉及上百位数字,金融行业的汇率转换、大额资金清算容不得一丝误差。

这些场景里,C++ 原生的 int/long long/float/double 显然"不够用"了:

  • 整数类型有固定存储范围,超出就会"溢出",存不下超大数;
  • 浮点类型因二进制存储特性,无法精确表示部分十进制小数,计算时会产生偏差。

那该如何解决这些问题?答案就是「高精度」。

什么是高精度?

高精度,简单说就是针对原生类型无法覆盖的"范围超限"(如大数)或"精度丢失"(如部分小数)问题,通过自定义方式(如数组/字符串模拟十进制)实现数字的精确存储、计算和传输,精准匹配实际需求,避免因原生类型的存储缺陷导致的误差和 bug

它的核心思路特别好理解:就像我们手算加减法那样------把数字按位对齐,从最低位开始运算,遇到满十进一、不够减借一的情况手动处理。高精度本质就是让计算机"复现手算十进制的过程":用数组或字符串存储数字的每一位(包括小数点后),手动模拟对齐、运算、进位/借位规则,从而绕开原生类型的存储和精度限制,得到绝对精确的结果。

具体来说,高精度主要解决两类问题:

  • 大数高精度:突破原生整数的存储范围,比如计算上百位的大数乘法、超大数阶乘,或是航天、物理领域的超大数值计算;
  • 小数高精度:解决浮点类型的精度丢失问题,比如电商金额统计、金融汇率转换、税务计算等需要"分毫不差"的场景。

无论是大数还是小数,实现思路都是一致的:用数组/字符串存十进制每一位,模拟手算逻辑------这正是高精度能绕开原生类型缺陷的关键。

高精度的实现方式

高精度数的已知值与计算结果有两种核心存储形式:

  1. 存储在字符串中
  2. 存储在数组(或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)
约束:

  1. 被除数和除数均为非空字符串,仅由数字 '0'-'9' 组成
  2. 除数不为 "0",且字符串不含前导零(非 "0" 时)

算法原理

前置依赖:

由于除数和被除数均为大数,需先实现两个核心辅助函数:

  1. 大数比较函数 :判断两个大数字符串 ab 的大小,返回 1(a > b)、0(a == b)、-1(a < b)
  2. 大数减法函数 :假设 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,直到片段小于除数为止,最终计数就是当前位商。
    • 操作:
      1. 初始化计数 count = 0
      2. 循环:只要「当前余数片段」≥ 除数,就执行 片段 = 片段 - 除数(调用大数减法函数),同时 count++
      3. 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,然后补下一位再算。
    • 操作:
      1. 当前位商为0;
      2. 若商字符串还未开始记录有效数字(即存在前导零),则跳过(不存入商字符串);
      3. 若商字符串已记录有效数字(如之前已存入'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 带着小花🌸来啦!🌸奖励🌸坚持攻克重难点的你!复杂问题拆解后也能轻松应对,下一道题虽然是基础题,但细节决定成败,跟着思路一步步梳理,一定能写出简洁高效的代码~ 如果这篇本文章对你有帮助别忘了点赞,收藏哦~,关注这个博主后续刷题时直接找到他随时回顾知识点,也欢迎在评论区分享你的解题感悟,我们下道题不见不散!

相关推荐
夏乌_Wx1 小时前
练题100天——DAY19:含退格的字符串+有序数组的平方
算法
Ayanami_Reii1 小时前
进阶数据结构应用-线段树扫描线
数据结构·算法·线段树·树状数组·离散化·fenwick tree·线段树扫描线
leoufung1 小时前
LeetCode 98 Validate Binary Search Tree 深度解析
算法·leetcode·职场和发展
水木姚姚1 小时前
C++ begin
开发语言·c++·算法
浅川.251 小时前
xtuoj 素数个数
数据结构·算法
jyyyx的算法博客1 小时前
LeetCode 面试题 16.18. 模式匹配
算法·leetcode
uuuuuuu1 小时前
数组中的排序问题
算法
Stream1 小时前
加密与签名技术之密钥派生与密码学随机数
后端·算法
Stream1 小时前
加密与签名技术之哈希算法
后端·算法