题目:
Given two non-negative integers num1
and num2
represented as strings, return the product of num1
and num2
, also represented as a string.
Example 1:
Input: num1 = "2", num2 = "3"
Output: "6"
Example 2:
Input: num1 = "123", num2 = "456"
Output: "56088"
Note:
- The length of both
num1
andnum2
is < 110. - Both
num1
andnum2
contain only digits0-9
. - Both
num1
andnum2
do not contain any leading zero, except the number 0 itself. - You must not use any built-in BigInteger library or convert the inputs to integer directly.
题目连接:https://leetcode.com/problems/multiply-strings/
题意
以字符串的形式输入两个非负整数,然后再以字符串的形式输出这两个整数的成绩
分析
Leetcode第二题,Difficulty:Medium。
- 做题之前首先要注意到上面的Note,两个整数的位数小于110位,意味着常规的long long类型是无法满足的
- 和大数加法一样,自然而然要做分解做乘法的
- 乘法想比加法要更复杂一些,有多种思路可以实现,下面注意介绍
首先想到大数运算,使用python可以说是相当的方便,所以下面先来介绍python的实现
Python3
我们使用python3都知道,python3是没有数值范围越界的,因此 本题变得异常简单。
Code
class Solution:
def multiply(self, num1: 'str', num2: 'str') -> 'str':
return str(int(num1) * int(num2))
首先使用int() 把两个字符串转换为整数形式,然后计算两个整数的乘积,对后使用str()把结果转换为字符串形式,输出即可。
结果:
Runtime: 40 ms, faster than 99.59% of Python3 online submissions for Multiply Strings.
Memory Usage: 12.6 MB, less than 100.00% of Python3 online submissions for Multiply Strings.
使用python3 我们可以看到运行时间为40ms,那么我们如何优化呢? 接下来使用C++来介绍本篇的重点:
C++版
对于普通的的乘法,我们知道m位数和n位数相乘 得到的结果位数在[m+n-1,m+n]之间,因此第一种思路: 模拟手工算法
即 ab*cd, 我们知道首先是d与a和b相乘然后记录进位情况,然后再c与a和b相乘 并进行相同的操作。所以 这里我们可以使用deque(双端队列)来存储每一位数之间相乘的结果,然后再来统一处理进位的情况。(之所以使用deque,是因为 两数相乘得到的结果位数不定,因此我们要有应对多出进位的情况 即再结果的最前面插入进位,所以使用双端队列相比更方便一些)
Code
class Solution {
public:
string multiply(string num1, string num2) {
// 用来保存最后返回的结果
string ans="";
// 双端队列 首先初始化大小为num1.size()+num2.size()-1 数值为0, 即两数相乘结果位数最小的情况(0除外) 如果有进位可以再采用队列头插法。
deque<int> res(num1.size()+num2.size()-1,0);
for(int i=0;i<num1.size();++i){
for(int j=0;j<num2.size();++j){
res[i+j] += (num1[i]-'0')*(num2[j]-'0');
}
}
int addflag = 0;
for(int i=res.size()-1;i>=0;--i){
int tmp = res[i] + addflag;
res[i] = tmp%10;
addflag = tmp/10;
}
while(addflag){
res.push_front(addflag%10);
addflag/=10;
}
//处理结果为0的特殊情况,比如11111*0 结果是0 而不是00000
while(res.front()==0 && res.size()>1){
res.pop_front();
}
for(auto a:res){
stringstream ss;
ss<<a;
ans += ss.str();
}
return ans;
}
};
结果:
Runtime: 20 ms, faster than 31.36% of C++ online submissions for Multiply Strings.
Memory Usage: 11 MB, less than 100.00% of C++ online submissions for Multiply Strings.
通过以上 可以看到 运行时间有上面python解决方案的40ms降至20ms,但faster than只有31.36%,因此 接下来继续寻找最优解。
(以下题解方法来自leetcode43 Discuss, 略作改动)
通过回顾上面的思路可以看出 字符串逐位相加之后再统一做进位处理,这样相当于多了一次不必要的遍历过程,因为我们完全可以在第一次遍历乘法的时候顺带就把进位考虑进来,代码如下:
Code
class Solution {
public:
string multiply(string num1, string num2) {
// 首先反转字符串,低位在前
reverse(num1.begin(), num1.end());
reverse(num2.begin(), num2.end());
vector<int> res(num1.size()+num2.size()+1,0);
string ans = "";
for(int i=0;i<num1.size();++i){
int n1 = num1[i]-'0',addflag1=0; // addflag 用来标记进位的情况
for(int j=0;j<num2.size();++j){
int n2 = num2[j]-'0';
int tmp = n1*n2+addflag1;
addflag1 = (res[i+j] + tmp)/10;
res[i+j] = (res[j+i]+tmp)%10;
if(j== num2.size()-1)
res[i+j+1] += (addflag1);
}
}
for(int i=res.size()-1; i>=0; --i){
ans += res[i]+'0';
}
// 删除头部0元素
ans.erase(0,ans.find_first_not_of('0'));
return ans.size()?ans:"0";
}
};
结果:
Runtime: 12 ms, faster than 100.00% of C++ online submissions for Multiply Strings.
Memory Usage: 10.6 MB, less than 100.00% of C++ online submissions for Multiply Strings.
分割线
以上都是模拟乘法的手动运算,那么接下来使用一种分治的思想来解决这道题目
使用分治 首先要理解以下两点:
(1)分治法基本思想是将一个规模为n的问题分解为k个规模较小的子问题,这些子问题相互独立且与原问题相同。
(2)递归的解这些子问题,然后将各子问题的解合并得到原问题的解。
分治法的重点是一个相对比较复杂的大问题是否能够分解成若干个同等类型的小问题,然后把小问题逐个解决 最终组合成大问题的解。
以本题大数相乘为例:给出两个大整数num1,num2,假设他们都为n位数(长度不等 补零即可),然后把num1、num2均等分为两份,如下图:
这样就会有:
num1 = (a1 * 10^n/2)+a0
num2 = (b1 * 10^n/2)+b0
num1 * num2 = a1*b1*10^n + (a1*b0+a0*b1)*10^n/2 + a0*b0
// 可以令 c0 = a0*b0 c1 = a1*b0+a0*b1 c2=a1*b1
num1 * num2 = c2*10^n + c1*10^n/2 + c0
这样即完成了一次划分,可以看到 划分之后num1、num2的计算长度减半。 另外可以看到这里除了乘法之外还用到了加法(加法也需要使用大数加法,因为n 分一半也有可能是大数 例如n为100位整数)
完成分治策略之后,需要终止条件,否则将会死循环。
这里采用一种比较懒惰的方法,通过使用long long可以发现 能够正确处理10位数的乘法,所以 简单粗暴地以此为结束条件,如下:
上面理清之后,代码实现就很容易了
Code
class Solution {
public:
string strProcess(string str, int n){
string head(n,'0');
return head+str;
}
string Add(string num1, string num2){
int addflag=0;
string ans = "";
// i j 分别表示num1 num2 数字的位数
int i=num1.size(), j = num2.size();
while(i>0 && j>0){
int tmp = (num1[--i]-'0')+(num2[--j]-'0')+addflag;
ans += (tmp%10 + '0');
addflag = tmp/10;
}
while(i>0){
int tmp = (num1[--i]-'0')+ addflag;
ans += (tmp%10+'0');
addflag = tmp/10;
}
while(j>0){
int tmp = (num2[--j]-'0')+ addflag;
ans += (tmp%10+'0');
addflag = tmp/10;
}
if(addflag){
ans += addflag+'0';
}
reverse(ans.begin(),ans.end());
ans.erase(0,ans.find_first_not_of('0'));
return ans;
}
string multiply(string num1, string num2) {
int len = num1.size()>num2.size()?num1.size():num2.size(),i=2;
if(len<=10){
stringstream s1(num1),s2(num2),s3;
long long a,b,s;
s1>>a;
s2>>b;
s = a*b;
s3<<s;
return s3.str();
}
// 这里之所以把字符串长度凑成2的整数幂长度,是为了方便后面分段,也可以尝试不这么做
while(len>i){
i*=2;
}
len=i;
// 将长度补为2的整数幂长,事实证明只在第一次执行时用到,因为后面划分肯定是整数幂
num1=strProcess(num1,len-num1.size());
num2=strProcess(num2,len-num2.size());
string a1=num1.substr(0,len/2),a0=num1.substr(len/2,len);
string b1=num2.substr(0,len/2),b0=num2.substr(len/2,len);
string c0=multiply(a0,b0), c1=Add(multiply(a0,b1), multiply(a1,b0)), c2=multiply(a1, b1);
string zeroby2(len/2,'0'), zero(len,'0');
c1 += zeroby2;
c2 += zero;
return Add(Add(c2, c1), c0);
}
};
结果
Runtime: 76 ms, faster than 10.65% of C++ online submissions for Multiply Strings.
Memory Usage: 24.1 MB, less than 100.00% of C++ online submissions for Multiply Strings.
可以看到使用分治的方法使得代码量几乎翻了一倍,并且效率并不是很高,有可能是因为里面包含了很多冗余代码需要优化,具体优化这里不再介绍了。在这里使用分治主要是想熟练以下分治的基本思想和使用方法。有兴趣的朋友可以继续优化此代码。
Status:Accepted