字符串相乘:从暴力算法到规律优化
文章目录
- 字符串相乘:从暴力算法到规律优化
- 一、问题引入:为什么需要特殊处理?
- 二、朴素思路:模仿手工乘法
- 2.1 手工乘法的步骤
- 2.2 代码实现(初始版本)
- 2.3 初始版本的问题分析
- 三、优化思路:减少字符串操作
- 3.1 优化字符串加法
- 3.2 优化乘法中的补零操作
- 四、进一步优化:基于数学规律的算法
- 4.1 乘积长度的数学规律
- 4.2 位相乘的位置规律
- 4.3 优化版本代码
- 五、最终版本解析
- 5.1 核心思路
- 5.2 时间和空间复杂度
- 5.3 优势总结
- 六、测试用例验证
字符串相乘:从暴力算法到规律优化
题目链接
一、问题引入:为什么需要特殊处理?
LeetCode 第 43 题要求我们实现两个非负整数的相乘,其中输入和输出都以字符串形式表示,且不能使用内置的大数库或直接转换为整数。这个问题的核心挑战在于:
- 输入数字可能非常大(长度可达 200 位),远超任何内置整数类型的存储范围
- 必须手动模拟乘法运算的全过程
- 需要处理中间结果的进位和拼接
那如何从最朴素的思路开始,逐步推导出高效解法?
二、朴素思路:模仿手工乘法
2.1 手工乘法的步骤
回忆一下我们在纸上做乘法的过程:
- 用第一个数的每一位分别乘以第二个数
- 每完成一次乘法,根据当前位的位置补 0(例如个位乘完补 0 个 0,十位乘完补 1 个 0)
- 把所有中间结果相加,得到最终乘积
例如计算 “123” × “456”:
- 3 × 456 = 1368
- 2 × 456 = 912 → 补 1 个 0 → 9120
- 1 × 456 = 456 → 补 2 个 0 → 45600
- 总和:1368 + 9120 + 45600 = 56088
2.2 代码实现(初始版本)
基于这个思路,我们可以写出第一版代码:
#include <string>
#include <algorithm>
using namespace std;class Solution {
public:string multiply(string num1, string num2) {// 特殊情况:任何一方为0,结果为0if(num1 == "0" || num2 == "0") {return "0";}int len1 = num1.size();int len2 = num2.size();string res = "0"; // 初始结果为0// 用num1的每一位乘以num2for(int i = len1 - 1; i >= 0; i--) {string temp; // 存储当前位的乘法结果int carry = 0;int numi = num1[i] - '0'; // 转换为数字// 计算numi × num2for(int j = len2 - 1; j >= 0; j--) {int numj = num2[j] - '0';int product = numi * numj + carry;temp.push_back((product % 10) + '0'); // 存储当前位carry = product / 10; // 计算进位}// 处理剩余进位if(carry != 0) {temp.push_back(carry + '0');}// 反转得到正确的顺序(因为我们是从低位开始计算的)reverse(temp.begin(), temp.end());// 根据当前位的位置补0(第i位补len1-1-i个0)int zeros = len1 - 1 - i;while(zeros--) {temp.push_back('0');}// 累加当前结果到总结果res = addStrings(res, temp);}return res;}// 辅助函数:字符串加法string addStrings(string a, string b) {int lena = a.size() - 1;int lenb = b.size() - 1;string res;int carry = 0;// 从低位到高位相加while(lena >= 0 || lenb >= 0 || carry > 0) {int na = (lena >= 0) ? (a[lena--] - '0') : 0;int nb = (lenb >= 0) ? (b[lenb--] - '0') : 0;int sum = na + nb + carry;res.insert(0, 1, (sum % 10) + '0'); // 插入到结果头部carry = sum / 10; // 更新进位}return res;}
};
2.3 初始版本的问题分析
这个版本虽然能正确工作,但存在明显的效率问题:
- 字符串插入效率低:
res.insert(0, ...)
每次都需要移动所有字符,时间复杂度为 O (n) - 中间字符串频繁创建:每次乘法和加法都会创建新的字符串,增加了内存开销
- 整体时间复杂度高:假设两个字符串长度分别为 m 和 n,时间复杂度为 O (m×n + m²×n)(乘法 O (m×n),加法 O (m×n) 且需要加 m 次)
三、优化思路:减少字符串操作
3.1 优化字符串加法
字符串加法中,insert(0, ...)
操作效率低下,我们可以改为从后往前存储结果,最后再反转:
// 优化后的字符串加法
string addStrings(string a, string b) {int lena = a.size() - 1;int lenb = b.size() - 1;string res;int carry = 0;while(lena >= 0 || lenb >= 0 || carry > 0) {int na = (lena >= 0) ? (a[lena--] - '0') : 0;int nb = (lenb >= 0) ? (b[lenb--] - '0') : 0;int sum = na + nb + carry;res.push_back((sum % 10) + '0'); // 先存到尾部carry = sum / 10;}reverse(res.begin(), res.end()); // 最后反转return res;
}
这个优化将每次插入的 O (n) 操作变为 O (1)(尾部插入),最后反转一次 O (n),整体效率提升明显。
3.2 优化乘法中的补零操作
在乘法中,补零操作可以更高效:不需要实际插入字符,而是在加法时考虑偏移量。但为了保持逻辑清晰,我们暂时保留补零操作,后续会有更好的优化。
四、进一步优化:基于数学规律的算法
4.1 乘积长度的数学规律
观察可知:两个长度分别为 m 和 n 的数字相乘,结果的长度最多为 m + n(例如 999 × 99 = 98901,3 位 × 2 位 = 5 位)。
这个规律让我们可以预先分配一个固定长度的数组来存储结果,避免频繁的字符串操作。
4.2 位相乘的位置规律
对于 num1 [i](从右数第 i 位)和 num2 [j](从右数第 j 位)的乘积,其结果会影响最终结果的第 i+j 位和第 i+j+1 位(从右数,从 0 开始)。为什么会这样?那是因为两数相乘结果不会超过三位数,所以智慧影响两个位置!
例如 “123” × “456” 中:
- num1 [0] = ‘3’(个位),num2 [0] = ‘6’(个位)
- 乘积 3×6=18,影响结果的第 0+0=0 位(8)和第 0+0+1=1 位(1)
4.3 优化版本代码
string multiply(string num1, string num2) {if (num1 == "0" || num2 == "0") {return "0";}int m = num1.size(), n = num2.size();// 结果最多m+n位vector<int> resArr(m + n, 0);// 计算每一位的乘积并累加到结果数组for (int i = m - 1; i >= 0; i--) {int digit1 = num1[i] - '0';for (int j = n - 1; j >= 0; j--) {int digit2 = num2[j] - '0';int product = digit1 * digit2;// 计算当前乘积在结果数组中的位置int p1 = i + j, p2 = i + j + 1;// 累加当前乘积到结果数组int sum = product + resArr[p2];resArr[p2] = sum % 10; // 当前位resArr[p1] += sum / 10; // 进位累加到高位}}// 转换为字符串(跳过前导零)string res;for (int num : resArr) {// 跳过开头的0,但不能全跳(至少保留一个0)if (!(res.empty() && num == 0)) {res.push_back(num + '0');}}return res;
}
五、最终版本解析
5.1 核心思路
- 预分配结果数组:利用乘积长度规律,创建 m+n 大小的数组
- 位运算累加:直接在数组中累加每对位的乘积结果,避免中间字符串
- 最后转字符串:跳过前导零,将数组转换为最终字符串
5.2 时间和空间复杂度
- 时间复杂度:O (m×n),其中 m 和 n 分别是两个输入字符串的长度,我们只需要遍历一次所有位的组合
- 空间复杂度:O (m+n),用于存储结果数组
5.3 优势总结
- 效率极高:避免了所有字符串插入操作和中间结果的字符串存储
- 逻辑清晰:直接模拟了乘法的数学本质
- 内存友好:使用数组存储中间结果,内存开销稳定
六、测试用例验证
- 基础测试:num1 = “2”, num2 = “3”
- 结果数组大小为 2+1=3,初始为 [0,0,0]
- 计算 2×3=6,p1=0+0=0,p2=1
- sum=6+0=6 → resArr[1]=6, resArr[0]=0
- 转换为字符串:跳过前导零 → “6”
- 复杂测试:num1 = “123”, num2 = “456”
- 结果数组大小为 3+3=6
- 经过多轮计算后,数组变为 [0,5,6,0,8,8]
- 转换为字符串:“56088”