【C++】大数相加算法详解:从字符串加法到内存布局的思考

引言

在编程面试和实际开发中,处理大数相加是一个经典问题。由于计算机整数类型的限制,当数字超过特定范围时,我们需要使用字符串来表示数字并进行计算。本文将详细解析一个高效的大数相加算法,并探讨其背后的内存管理原理。

目录

引言

问题背景

算法实现

算法步骤详解

[1. 初始化](#1. 初始化)

[2. 逐位相加](#2. 逐位相加)

[3. 处理不同长度的数字](#3. 处理不同长度的数字)

[4. 处理最终进位](#4. 处理最终进位)

内存布局思考

[1. 字符串存储](#1. 字符串存储)

[2. 内存分配策略](#2. 内存分配策略)

[3. 字符与数字转换](#3. 字符与数字转换)

性能分析

[时间复杂度:O(max(m, n))](#时间复杂度:O(max(m, n)))

[空间复杂度:O(max(m, n) + 1)](#空间复杂度:O(max(m, n) + 1))

边界情况处理

扩展应用

[1. 二进制加法](#1. 二进制加法)

[2. 链表表示的数字相加](#2. 链表表示的数字相加)

实际应用场景

总结


问题背景

假设我们需要计算两个非常大的整数相加,这两个整数已经以字符串形式给出:

  • num1 = "12345678901234567890"

  • num2 = "98765432109876543210"

这些数字已经超出了普通整数类型的表示范围,因此我们需要实现一个字符串加法算法。

算法实现

cpp

复制代码
class Solution {
public:
    string addStrings(string num1, string num2) {
        string str;                    // 存储结果
        int end1 = num1.size() - 1;   // num1的末尾索引
        int end2 = num2.size() - 1;   // num2的末尾索引
        int next = 0;                 // 进位标志
        
        // 从个位开始逐位相加
        while(end1 >= 0 || end2 >= 0) {
            // 获取当前位的数字,如果已经处理完则用0代替
            int val1 = end1 >= 0 ? num1[end1] - '0' : 0;
            int val2 = end2 >= 0 ? num2[end2] - '0' : 0;
            
            // 当前位的和(包括进位)
            int ret = val1 + val2 + next;
            
            // 计算新的进位和当前位的结果
            next = ret / 10;     // 进位
            ret = ret % 10;      // 当前位的结果
            
            // 将当前位的结果转换为字符并添加到结果字符串
            str += (ret + '0');
            
            // 移动到下一位(更高位)
            end1--;
            end2--;
        }
        
        // 如果最后还有进位,添加一个'1'
        if(next == 1) {
            str += '1';
        }
        
        // 反转字符串,因为我们是先从个位开始计算的
        reverse(str.begin(), str.end());
        
        return str;
    }
};

算法步骤详解

1. 初始化

  • end1end2 分别指向两个数字字符串的最后一个字符(个位)

  • next 用于记录进位,初始为0

  • str 用于存储结果,初始为空

2. 逐位相加

text

复制代码
示例:num1 = "123", num2 = "456"

第一轮:个位相加
val1 = 3, val2 = 6, next = 0
ret = 3 + 6 + 0 = 9
next = 9 / 10 = 0
ret = 9 % 10 = 9
str = "9"

第二轮:十位相加
val1 = 2, val2 = 5, next = 0
ret = 2 + 5 + 0 = 7
next = 0
ret = 7
str = "97"

第三轮:百位相加
val1 = 1, val2 = 4, next = 0
ret = 1 + 4 + 0 = 5
next = 0
ret = 5
str = "975"

反转后得到:"579"

3. 处理不同长度的数字

text

复制代码
示例:num1 = "999", num2 = "1"

第一轮:个位
3 + 1 + 0 = 4, next = 0, str = "4"

第二轮:十位
9 + 0 + 0 = 9, next = 0, str = "94"

第三轮:百位
9 + 0 + 0 = 9, next = 0, str = "994"

结果:"1000"

等等,这个结果是错误的!让我重新计算:

实际上应该是:

个位:9 + 1 = 10,进位1,当前位0,str = "0"

十位:9 + 0 + 1 = 10,进位1,当前位0,str = "00"

百位:9 + 0 + 1 = 10,进位1,当前位0,str = "000"

最后还有进位:str = "0001"

反转后:"1000"

这个算法已经正确处理了这种情况,因为我们的循环条件是while(end1 >= 0 || end2 >= 0),所以当num2处理完后,会继续处理num1。

4. 处理最终进位

text

复制代码
示例:num1 = "999", num2 = "1"
最终循环结束后,next = 1
所以会添加一个'1':str = "0001"
反转后:"1000"

内存布局思考

虽然这个算法本身不涉及复杂的内存操作,但我们可以思考其中涉及的内存管理:

1. 字符串存储

  • 输入字符串num1num2存储在内存中(栈或堆,取决于调用方式)

  • 结果字符串str在栈上创建,但实际字符数据在堆上分配(C++ string的内部实现)

2. 内存分配策略

cpp

复制代码
string str;  // 初始为空
str += (ret + '0');  // 每次追加字符,可能触发内存重新分配

优化建议:我们可以预先分配足够的内存,避免多次重新分配:

cpp

复制代码
int maxLen = max(num1.size(), num2.size()) + 1;  // +1用于可能的进位
string str;
str.reserve(maxLen);  // 预先分配内存

3. 字符与数字转换

  • num1[end1] - '0':将字符数字转换为整数值

  • ret + '0':将整数值转换为字符数字

性能分析

时间复杂度:O(max(m, n))

  • m 和 n 分别是两个输入字符串的长度

  • 我们需要遍历两个字符串的所有位

空间复杂度:O(max(m, n) + 1)

  • 结果字符串的长度最多为 max(m, n) + 1(考虑进位)

边界情况处理

  1. 空字符串:虽然题目通常不会给空字符串,但我们可以处理

  2. 前导零:输入可能有前导零,但我们的算法能正确处理

  3. 非常大的数字:算法可以处理任意长度的数字

  4. 全零的情况:算法能正确处理

扩展应用

1. 二进制加法

同样的思路可以用于二进制加法:

cpp

复制代码
class Solution {
public:
    string addBinary(string a, string b) {
        string result;
        int i = a.length() - 1;
        int j = b.length() - 1;
        int carry = 0;
        
        while(i >= 0 || j >= 0 || carry) {
            int sum = carry;
            if(i >= 0) sum += a[i--] - '0';
            if(j >= 0) sum += b[j--] - '0';
            
            result.push_back((sum % 2) + '0');
            carry = sum / 2;
        }
        
        reverse(result.begin(), result.end());
        return result;
    }
};

2. 链表表示的数字相加

cpp

复制代码
struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(nullptr) {}
};

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode* dummy = new ListNode(0);
        ListNode* curr = dummy;
        int carry = 0;
        
        while(l1 != nullptr || l2 != nullptr || carry != 0) {
            int sum = carry;
            if(l1 != nullptr) {
                sum += l1->val;
                l1 = l1->next;
            }
            if(l2 != nullptr) {
                sum += l2->val;
                l2 = l2->next;
            }
            
            carry = sum / 10;
            curr->next = new ListNode(sum % 10);
            curr = curr->next;
        }
        
        return dummy->next;
    }
};

实际应用场景

  1. 金融计算:处理非常大的金额

  2. 加密算法:大数运算在加密中很常见

  3. 科学计算:处理超出标准数据类型范围的数字

  4. 数据库:某些数据库系统需要处理超出普通整数范围的ID

总结

大数相加算法展示了几个重要的编程概念:

  1. 模拟人工计算:算法模拟了我们在纸上进行加法计算的过程

  2. 边界处理:正确处理不同长度的数字和进位

  3. 字符与数字转换:利用ASCII码进行字符和数字的相互转换

  4. 内存管理:虽然简单,但涉及到字符串的内存分配

这个算法虽然简单,但体现了解决复杂问题的基本思路:将大问题分解为小步骤,逐步解决。同时,它也提醒我们在处理输入时要注意边界情况和异常处理。

关键要点

  • 从最低位开始计算

  • 使用进位变量记录进位

  • 处理不同长度的数字

  • 最后检查是否还有进位

  • 反转结果字符串以获得正确的顺序

掌握这种基本的算法思想,可以帮助我们解决许多类似的问题,如大数相减、相乘、相除等。

相关推荐
“抚琴”的人2 小时前
C#上位机工厂模式
开发语言·c#
巨大八爪鱼2 小时前
C语言纯软件计算任意多项式CRC7、CRC8、CRC16和CRC32的代码
c语言·开发语言·stm32·crc
C+-C资深大佬2 小时前
C++ 数据类型转换是如何实现的?
开发语言·c++·算法
cwplh2 小时前
DP 优化二:斜率优化 DP
算法·动态规划
love530love2 小时前
彻底解决 ComfyUI Mixlab 插件 Whisper.available False 的报错
人工智能·windows·python·whisper·win_comfyui
木千2 小时前
Qt全屏显示时自定义任务栏
开发语言·qt
习惯就好zz2 小时前
[实战笔记] 从 Qt 5.12.9 跨越到 Qt 6.x 完美迁移指南 (Windows + VS)
windows·qt·msvc·qt5·qt6·迁移
Hcoco_me3 小时前
大模型面试题90:half2,float4这种优化 与 pack优化的底层原理是什么?
人工智能·算法·机器学习·langchain·vllm
浅念-3 小时前
链表经典面试题目
c语言·数据结构·经验分享·笔记·学习·算法