长乘法即我们在小学学习的竖式乘法,以23958233*5830为例
23958233
× 5830
———————————————
00000000 ( = 23,958,233 × 0)
71874699 ( = 23,958,233 × 30)
191665864 ( = 23,958,233 × 800)
+ 119791165 ( = 23,958,233 × 5,000)
———————————————
139676498390 ( = 139,676,498,390 )
算法分析:
到这一步,相信大家很快就会发现,这就是一个简单的二重循环。下面是用c++实现的代码:
// 预先定义的宏,方便字符与数字转换
#define CTOI(a) (int(a-'0'))
#define ITOC(a) (char('0'+a))
#define ISZERO(a) (a.compare("0") == 0 || a.empty())
// 输入:两个大数num1和num2
// 输出:两个大数的乘积
string bigNumberMultiply(const string& num1, const string& num2)
{
int len1 = (int)num1.size();
int len2 = (int)num2.size();
if (len1 > len2)
{
return bigNumberMultiply(num2, num1);
}
string product(len1+len2, '0'); //存储最终的乘积结果
string midProduct(len2 + 1, '0'); //存储一次相乘的结果
for (int i = len1 - 1, ret = 0; i >= 0; --i)
{
for (int j = len2 - 1, carry = 0; j >= 0; --j)
{
ret = (CTOI(num1[i]) * CTOI(num2[j])) + carry;
carry = ret / 10;
midProduct[len2 - 1 - j] = ITOC(ret % 10);
}
midProduct[len2] = ITOC(ret / 10);
ret = 0;
for (int k = 0, carry = 0; k < len2 + 1; ++k)
{
int ret = (CTOI(product[len1 - 1 - i + k]) + CTOI(midProduct[k])) + carry;
product[len1 - 1 - i + k] = ITOC(ret % 10);
carry = ret / 10;
}
product[len1 + len2 - i] = ITOC(ret / 10);
}
//去除乘积头部多余的0
while (product.back() == '0' && product.size() > 1)
{
product.pop_back();
}
//逆序得到结果
std::reverse(product.begin(), product.end());
return product;
}
观察上面的代码,可以发现,每一趟外层循环都包含两次二层循环,刚好是算法分析中的两步,依次相乘得到中间结果midProduct,累加到product上。细心的小伙伴肯定已经再想了,有没有办法将这两步合成一步?如果我们有办法直接将依次相乘结果直接放到product上,省去了midProduct这个中间结果,是不是就可以了。接下来,我们来看第二种方法。
我们观察方法一中的竖乘法,会发现每一趟竖乘的midProduct对应最终product的位置是固定的。如果我们直接把每一趟的结果直接存储在最终结果中,就可以省去了方法一中的步骤二,但同时product上的每一位存储的可能不是个位数,因此需要我们在竖乘结束之后对product进行进位操作。下面时优化之后的代码:
// 输入:两个大数num1和num2
// 输出:两个大数的乘积
string bigNumberMultiplyPlus(const string& num1, const string& num2)
{
int len1 = (int)num1.size();
int len2 = (int)num2.size();
if (len1 > len2)
{
return bigNumberMultiplyPlus(num2, num1);
}
//由于每一位的存储结果可能会超过255,必须使用int数组来存储结果
vector<int> midProduct(len1 + len2, 0);
for (int i = len1 - 1; i >= 0; --i)
{
for (int j = len2 - 1; j >= 0; --j)
midProduct[len1 + len2 - i - j - 2] += CTOI(num1[i]) * CTOI(num2[j]);
}
//处理最终结果的进位
for (int i = 0, carry = 0; i < (int)midProduct.size(); ++i)
{
midProduct[i] = midProduct[i] + carry;
carry = midProduct[i] / 10;
midProduct[i] %= 10;
}
//处理数字头部多余的0
while (midProduct.back() == 0 && midProduct.size() > 1)
{
midProduct.pop_back();
}
int productLen = (int)midProduct.size();
string product(productLen, '0');
for (int i = productLen - 1; i >= 0; --i)
{
product[productLen - i - 1] = ITOC(midProduct[i]);
}
return product;
}
前面的两种算法,无论时长乘法还是长乘法优化,本质都是各位依次相乘,算法的复杂度都是 O ( n 2 ) \pmb O(n^2) OOO(n2)。在1960年Karatsuba提出了一种分治算法,减少了长乘法中乘法的次数,使得复杂度降到了 O ( n l o g 2 3 ) \pmb O(n^{log_2^3}) OOO(nlog23),被认为是快速乘法理论的起点。我们以二位数乘法 12 × 12 12 \times 12 12×12为例,长乘法需要四次乘法 2 × 2 2 \times 2 2×2、 2 × 1 2 \times 1 2×1、 2 × 1 2 \times 1 2×1、 1 × 1 1 \times 1 1×1,采用了Karatsuba算法共计算了三次乘法 1 × 1 1 \times 1 1×1、 2 × 2 2 \times 2 2×2、 3 × 3 3 \times 3 3×3,因此该算法比长乘法快的多。
下面是Karatsuba算法的具体步骤:
现有两个乘数 x x x和 y y y, 求二者乘积。
下面是c++实现的Karatsuba乘法,由于没有考虑string类型的一些函数耗时,实际的运行效率是比前二者低的。
// 简单乘法,一个数和个位数相乘求积;
string multiply(const string& num1, char num2)
{
if (num2 == '0' || ISZERO(num1))
{
return "0";
}
int len = num1.size();
string product(len, '0');
int carry = 0;
for (int i = len - 1; i >= 0; --i)
{
int ret = CTOI(num1[i]) * CTOI(num2) + carry;
product[len-i-1] = ITOC(ret % 10);
carry = ret / 10;
}
if (carry != 0)
product.push_back(ITOC(carry));
reverse(product.begin(), product.end());
return product;
}
// 字符串整数相加
string add(const string& num1, const string& num2)
{
if (num1.size() < num2.size())
return add(num2, num1);
int len1 = (int)num1.size();
int len2 = (int)num2.size();
string sum(len1, '0');
int carry = 0;
for (int i = 0; i < len2; ++i)
{
int tmp = CTOI(num1[len1 - i - 1]) + CTOI(num2[len2 - i - 1]) + carry;
carry = tmp / 10;
sum[i] = ITOC(tmp % 10);
}
for (int i = len2; i < len1; ++i)
{
int tmp = CTOI(num1[len1 - i - 1]) + carry;
sum[i] = ITOC(tmp % 10);
carry = tmp / 10;
}
if (carry != 0)
sum.push_back(ITOC(carry));
reverse(sum.begin(), sum.end());
return sum;
}
// 字符串整数相减
// 暂认为num1>num2且num1和num2均为正数
string sub(const string& num1, const string& num2)
{
int len1 = num1.size();
int len2 = num2.size();
string diff(len1, '0');
int carry = 0;
for (int i = 0; i < len2; ++i)
{
int tmp = CTOI(num1[len1 - i - 1]) - CTOI(num2[len2 - i - 1]) + carry;
if (tmp < 0)
{
diff[i] = ITOC(tmp + 10);
carry = -1;
}
else
{
diff[i] = ITOC(tmp);
carry = 0;
}
}
for (int i = len2; i < len1; ++i)
{
int tmp = CTOI(num1[len1 - i - 1]) + carry;
if (tmp < 0)
{
diff[i] = ITOC(tmp + 10);
carry = -1;
}
else
{
diff[i] = ITOC(tmp);
carry = 0;
}
}
while (diff.back() == '0' && diff.size() > 1)
{
diff.pop_back();
}
reverse(diff.begin(), diff.end());
return diff;
}
// Karatsuba乘法
string karatsuba(const string& num1, const string& num2)
{
// 递归的简单情况
if (ISZERO(num1) || ISZERO(num2))
{
return "0";
}
if (num1.size() == 1 )
{
return multiply(num2, num1[0]);
}
if (num2.size() == 1)
{
return multiply(num1, num2[0]);
}
int len1 = num1.size();
int len2 = num2.size();
int half = ((std::max)(len1, len2) + 1) / 2;
// 步骤一:计算a, b, c, d
string a = (len1 > half) ? num1.substr(0, len1 - half) : "0";
string b = (len1 > half) ? num1.substr(len1 - half, half) : num1;
string c = (len2 > half) ? num2.substr(0, len2 - half) : "0";
string d = (len2 > half) ? num2.substr(len2 - half, half) : num2;
// 步骤二:计算a*c、b*d、(a+b)*(c+d)-a*c-b*d
string t0 = karatsuba(a,c);
string t1 = karatsuba(b,d);
string t2 = karatsuba(add(a, b), add(c, d));
string t3 = sub(sub(t2, t1), t0);
// 步骤三:计算乘积结果
string nOffset(2 * half, '0');
string nHalfOffset(half, '0');
string product = add(add((t0 + nOffset), (t3 + nHalfOffset)), t1);
return product;
}