引言
在计算机科学中,处理超出标准数据类型范围的大数是一个常见问题。我们已经学习了大数相加,现在将探索更复杂的大数相乘算法。本文将详细分析一个完整的字符串乘法实现,探讨其原理、优化方案和实际应用。
目录
[1. 整体思路:模拟竖式乘法](#1. 整体思路:模拟竖式乘法)
[2. 函数分解](#2. 函数分解)
[(1) addStrings - 大数相加](#(1) addStrings - 大数相加)
[(2) Single_Multiply - 大数乘以一位数](#(2) Single_Multiply - 大数乘以一位数)
[(3) multiply - 主函数](#(3) multiply - 主函数)
[3. 算法示例](#3. 算法示例)
[1. 密码学](#1. 密码学)
[2. 科学计算](#2. 科学计算)
[3. 金融系统](#3. 金融系统)
[4. 分布式系统](#4. 分布式系统)
[1. 忘记处理乘数为0的情况](#1. 忘记处理乘数为0的情况)
[2. 前导零处理不当](#2. 前导零处理不当)
[3. 进位处理错误](#3. 进位处理错误)
[4. 索引计算错误](#4. 索引计算错误)
[1. 大数阶乘](#1. 大数阶乘)
[2. 大数幂运算](#2. 大数幂运算)
[3. 大数除法](#3. 大数除法)
[1. 使用整数数组代替字符串](#1. 使用整数数组代替字符串)
[2. 并行计算优化](#2. 并行计算优化)
问题描述
给定两个以字符串形式表示的非负整数 num1 和 num2,返回它们的乘积,也以字符串形式表示。
注意:
-
不能使用任何内置的大数库或直接将输入转换为整数
-
字符串中不包含前导零,除了数字0本身
-
1 ≤ num1.length, num2.length ≤ 200
示例 1:
text
输入:num1 = "2", num2 = "3"
输出:"6"
示例 2:
text
输入:num1 = "123", num2 = "456"
输出:"56088"
完整算法实现
cpp
class Solution {
public:
// 大数相加函数
string addStrings(string num1, string num2) {
string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
// 进位
int next = 0;
while (end1 >= 0 || end2 >= 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--;
}
if (next == 1) {
str += '1';
}
// 区间左闭右开
reverse(str.begin(), str.end());
return str;
}
// 大数乘以一位数函数
string Single_Multiply(string str, int num) {
int next = 0;
string result;
for (int i = str.size() - 1; i >= 0; i--) {
int ret = (str[i] - '0') * num + next;
next = ret / 10;
ret = ret % 10;
result += ret + '0';
}
if (next != 0) {
result += next + '0';
}
reverse(result.begin(), result.end());
return result;
}
// 大数相乘主函数
string multiply(string num1, string num2) {
// 处理乘数为0的情况
if(num1 == "0" || num2 == "0") {
return "0";
}
string result;
int n = num2.size() - 1;
// 模拟竖式乘法
for (int i = n; i >= 0; i--) {
// 计算num1与num2当前位的乘积
string ret = Single_Multiply(num1, num2[i] - '0');
// 根据位数补0(相当于乘以10的幂)
int count = n - i;
while (count--) {
ret += '0';
}
// 累加部分乘积
result = addStrings(result, ret);
}
return result;
}
};
算法详解
1. 整体思路:模拟竖式乘法
该算法模拟了我们手工进行多位数乘法的过程:
-
将乘法分解为多个"大数乘以一位数"的问题
-
对每个部分乘积进行适当的移位(补0)
-
将所有部分乘积相加得到最终结果
2. 函数分解
(1) addStrings - 大数相加
-
从个位开始逐位相加
-
处理进位
-
反转结果字符串
(2) Single_Multiply - 大数乘以一位数
-
从个位开始逐位相乘
-
处理乘积累加和进位
-
反转结果字符串
(3) multiply - 主函数
-
处理乘数为0的特殊情况
-
遍历第二个数的每一位
-
调用
Single_Multiply计算部分乘积 -
根据位数补0
-
累加所有部分乘积
3. 算法示例
text
计算:123 × 456
步骤1:123 × 6 = 738
步骤2:123 × 5 = 615,补1个0 → 6150
步骤3:123 × 4 = 492,补2个0 → 49200
累加:
738
6150
+ 49200
--------
56088
时间复杂度分析
原始算法复杂度
-
Single_Multiply函数:O(n),其中n是num1的长度 -
addStrings函数:O(max(m, n)),其中m和n是两个加数的长度 -
multiply函数:-
循环执行m次(m是num2的长度)
-
每次循环:调用Single_Multiply (O(n)) + 补0 (O(1)) + 调用addStrings (O(k)),k是当前结果的长度
-
总时间复杂度:O(m × (n + k)) ≈ O(m × n × m) = O(n × m²)(最坏情况)
-
复杂度优化空间
原始算法的时间复杂度较高,主要是每次循环都要进行字符串加法。我们可以使用数组来优化。
优化算法
优化1:使用数组存储中间结果
cpp
class Solution {
public:
string multiply(string num1, string num2) {
if (num1 == "0" || num2 == "0") return "0";
int m = num1.size(), n = num2.size();
vector<int> res(m + n, 0); // 乘积的最大长度为 m+n
// 逐位相乘
for (int i = m - 1; i >= 0; i--) {
int x = num1[i] - '0';
for (int j = n - 1; j >= 0; j--) {
int y = num2[j] - '0';
int sum = res[i + j + 1] + x * y;
res[i + j + 1] = sum % 10; // 当前位
res[i + j] += sum / 10; // 进位
}
}
// 转换为字符串,跳过前导0
string result;
for (int i = 0; i < res.size(); i++) {
if (result.empty() && res[i] == 0) continue; // 跳过前导0
result += (res[i] + '0');
}
return result.empty() ? "0" : result;
}
};
优化2:Karatsuba快速乘法算法
对于非常大的数,可以使用Karatsuba算法,时间复杂度为O(n^log₂³) ≈ O(n^1.585):
cpp
// Karatsuba算法实现大数相乘
class Karatsuba {
private:
// 辅助函数:字符串相加
string add(string a, string b) {
// ... 实现同上
}
// 辅助函数:字符串相减(保证a>=b)
string subtract(string a, string b) {
// ... 实现需要
}
// 去除前导0
string trimZeros(string s) {
int i = 0;
while (i < s.size() && s[i] == '0') i++;
return i == s.size() ? "0" : s.substr(i);
}
public:
string multiply(string x, string y) {
// 转换为等长
int n = max(x.size(), y.size());
while (x.size() < n) x = "0" + x;
while (y.size() < n) y = "0" + y;
// 基准情况
if (n == 1) {
int prod = (x[0] - '0') * (y[0] - '0');
return to_string(prod);
}
int m = n / 2;
// 分割x和y
string x1 = x.substr(0, n - m);
string x0 = x.substr(n - m);
string y1 = y.substr(0, n - m);
string y0 = y.substr(n - m);
// 递归计算
string z2 = multiply(x1, y1);
string z0 = multiply(x0, y0);
string z1 = multiply(add(x1, x0), add(y1, y0));
z1 = subtract(subtract(z1, z2), z0);
// 组合结果
for (int i = 0; i < 2 * m; i++) z2 += "0";
for (int i = 0; i < m; i++) z1 += "0";
string result = add(add(z2, z1), z0);
return trimZeros(result);
}
};
性能对比
| 算法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 模拟竖式乘法 | O(m×n) | O(m+n) | 中小规模大数相乘 |
| 原始字符串方法 | O(n×m²) | O(m+n) | 教学理解 |
| Karatsuba算法 | O(n^log₂³) ≈ O(n^1.585) | O(n) | 大规模大数相乘 |
| FFT算法 | O(n log n) | O(n) | 超大规模大数相乘 |
实际应用场景
1. 密码学
-
RSA加密算法中的大数运算
-
椭圆曲线密码学
-
大素数生成和验证
2. 科学计算
-
高精度数值计算
-
物理模拟
-
天文学中的大数计算
3. 金融系统
-
高精度货币计算
-
利率计算
-
风险评估模型
4. 分布式系统
-
一致性哈希
-
分布式ID生成
-
数据分片计算
边界条件与测试
全面测试用例
cpp
void testMultiply() {
Solution solution;
// 基础测试
assert(solution.multiply("2", "3") == "6");
assert(solution.multiply("123", "456") == "56088");
// 边界测试
assert(solution.multiply("0", "123") == "0");
assert(solution.multiply("123", "0") == "0");
assert(solution.multiply("0", "0") == "0");
// 大数测试
assert(solution.multiply("999", "999") == "998001");
assert(solution.multiply("123456789", "987654321") == "121932631112635269");
// 前导零处理(输入不会包含前导零,但结果可能)
assert(solution.multiply("10", "10") == "100");
assert(solution.multiply("99", "99") == "9801");
// 一位数测试
assert(solution.multiply("5", "7") == "35");
assert(solution.multiply("9", "9") == "81");
// 长字符串测试
string num1(100, '9'); // 100个9
string num2(100, '9'); // 100个9
string expected = string(199, '9') + "0" + string(199, '0').substr(1);
// 实际上 999...9 × 999...9 = 999...98000...01
cout << "所有测试通过!" << endl;
}
常见错误与注意事项
1. 忘记处理乘数为0的情况
cpp
// 错误示例:没有检查0
string multiply(string num1, string num2) {
// 如果num1或num2为"0",会进行不必要的计算
// 最终结果可能正确,但效率低下
}
2. 前导零处理不当
cpp
// 错误示例:结果包含前导零
string result = "00123"; // 应该为"123"
3. 进位处理错误
cpp
// 错误示例:进位可能超过一位
int sum = x * y + carry;
carry = sum / 10; // 正确
result += (sum % 10) + '0';
4. 索引计算错误
cpp
// 错误示例:数组索引计算错误
// 两个n位数相乘,结果最多为2n位
vector<int> res(m + n, 0); // 正确
vector<int> res(m + n - 1, 0); // 错误:可能不够存储
扩展应用
1. 大数阶乘
cpp
string factorial(int n) {
string result = "1";
for (int i = 2; i <= n; i++) {
result = multiply(result, to_string(i));
}
return result;
}
2. 大数幂运算
cpp
string power(string base, int exponent) {
if (exponent == 0) return "1";
if (exponent == 1) return base;
string result = "1";
while (exponent > 0) {
if (exponent % 2 == 1) {
result = multiply(result, base);
}
base = multiply(base, base);
exponent /= 2;
}
return result;
}
3. 大数除法
cpp
pair<string, string> divide(string dividend, string divisor) {
if (divisor == "0") {
throw runtime_error("Division by zero");
}
string quotient = "0";
string remainder = dividend;
while (compare(remainder, divisor) >= 0) {
// 使用二分法加速
string low = "1", high = remainder;
string mid, product;
while (compare(low, high) <= 0) {
mid = divideByTwo(add(low, high));
product = multiply(divisor, mid);
if (compare(product, remainder) <= 0) {
quotient = add(quotient, mid);
remainder = subtract(remainder, product);
break;
} else {
high = subtract(mid, "1");
}
}
}
return {quotient, remainder};
}
性能优化技巧
1. 使用整数数组代替字符串
cpp
// 使用vector<int>存储大数,每个元素存储多位数字
// 例如:使用基数10000,每个元素存储0-9999
class BigInt {
private:
vector<int> digits;
static const int BASE = 10000;
static const int BASE_DIGITS = 4;
public:
// 乘法实现
BigInt multiply(const BigInt& other) const {
vector<long long> res(digits.size() + other.digits.size(), 0);
for (size_t i = 0; i < digits.size(); i++) {
long long carry = 0;
for (size_t j = 0; j < other.digits.size() || carry > 0; j++) {
long long cur = res[i + j] + carry;
if (j < other.digits.size()) {
cur += (long long)digits[i] * other.digits[j];
}
res[i + j] = cur % BASE;
carry = cur / BASE;
}
}
// 转换为BigInt
BigInt result;
result.digits.resize(res.size());
for (size_t i = 0; i < res.size(); i++) {
result.digits[i] = res[i];
}
result.normalize();
return result;
}
};
2. 并行计算优化
cpp
// 使用多线程并行计算部分乘积
string parallelMultiply(const string& num1, const string& num2) {
int m = num1.size(), n = num2.size();
vector<vector<int>> partialProducts(n, vector<int>(m + n, 0));
// 并行计算每个部分乘积
#pragma omp parallel for
for (int j = 0; j < n; j++) {
int y = num2[n-1-j] - '0';
int carry = 0;
for (int i = 0; i < m; i++) {
int x = num1[m-1-i] - '0';
int product = x * y + carry;
partialProducts[j][i+j] = product % 10;
carry = product / 10;
}
if (carry > 0) {
partialProducts[j][m+j] = carry;
}
}
// 合并所有部分乘积
vector<int> result(m + n, 0);
int carry = 0;
for (int i = 0; i < m + n; i++) {
int sum = carry;
for (int j = 0; j < n; j++) {
sum += partialProducts[j][i];
}
result[i] = sum % 10;
carry = sum / 10;
}
// 转换为字符串
string resStr;
int idx = m + n - 1;
while (idx > 0 && result[idx] == 0) idx--;
for (; idx >= 0; idx--) {
resStr += result[idx] + '0';
}
return resStr;
}
总结
大数相乘算法是计算机科学中的一个基础但重要的问题。通过本文的分析,我们学习了:
-
模拟手工计算:将复杂问题分解为多个简单步骤
-
算法优化:从简单实现到高效算法的演进
-
边界处理:处理特殊情况如乘数为0、前导零等
-
性能分析:理解不同算法的时间复杂度和空间复杂度
关键要点:
-
大数相乘可以通过模拟竖式乘法实现
-
使用数组存储中间结果比字符串操作更高效
-
对于非常大的数,Karatsuba或FFT算法是更好的选择
-
正确处理边界情况和进位是算法的关键
掌握大数相乘算法不仅有助于解决类似问题,还能提高对算法设计、性能优化和数学思维的理解。在实际应用中,根据数据规模和性能要求选择合适的算法是关键。