大数相乘算法杂谈

大数相乘算法杂谈

描述:求两个位数少于100位的大整数的乘积。
分析:由于整数过大,超出了基本类型的表示范围。因此采用字符串存储大整数,模拟整数乘法求出乘积。
  • 长乘法
  • 长乘法优化
  • Karatsuba乘法

一、长乘法

长乘法即我们在小学学习的竖式乘法,以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        )

算法分析:

  1. 将数5830按位依次与数23958233依次相乘,得到多次相乘结果;
  2. 将多次相乘结果依次错位相加得到最终结果。

到这一步,相信大家很快就会发现,这就是一个简单的二重循环。下面是用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;
}

三、Karatsuba乘法

前面的两种算法,无论时长乘法还是长乘法优化,本质都是各位依次相乘,算法的复杂度都是 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, 求二者乘积。

  1. x x x y y y写成 x = 1 0 n / 2 a + b x = 10^{n/2} a + b x=10n/2a+b y = 1 0 n / 2 c + d y = 10^{n/2} c + d y=10n/2c+d;
  2. 计算乘法 a × c a \times c a×c b × d b \times d b×d ( a + b ) × ( c + d ) (a + b) \times (c + d) (a+b)×(c+d);
  3. 乘积结果: p r o d u c t = a c 1 0 n + ( a d + b c ) 1 0 n / 2 + b d product = ac10^n + (ad + bc)10^{n/2} + bd product=ac10n+(ad+bc)10n/2+bd

下面是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;
}

你可能感兴趣的:(算法,算法,c++,数据结构,字符串)