0043-字符串相乘

Raphael Liu Lv10

给定两个以字符串形式表示的非负整数 num1num2,返回 num1num2 的乘积,它们的乘积也表示为字符串形式。

注意: 不能使用任何内置的 BigInteger 库或直接将输入转换为整数。

示例 1:

**输入:** num1 = "2", num2 = "3"
**输出:** "6"

示例 2:

**输入:** num1 = "123", num2 = "456"
**输出:** "56088"

提示:

  • 1 <= num1.length, num2.length <= 200
  • num1num2 只能由数字组成。
  • num1num2 都不包含任何前导零,除了数字0本身。

方法一:做加法

如果 $\textit{num}_1$ 和 $\textit{num}_2$ 之一是 $0$,则直接将 $0$ 作为结果返回即可。

如果 $\textit{num}_1$ 和 $\textit{num}_2$ 都不是 $0$,则可以通过模拟「竖式乘法」的方法计算乘积。从右往左遍历乘数,将乘数的每一位与被乘数相乘得到对应的结果,再将每次得到的结果累加。这道题中,被乘数是 $\textit{num}_1$,乘数是 $\textit{num}_2$。

需要注意的是,$\textit{num}_2$ 除了最低位以外,其余的每一位的运算结果都需要补 $0$。

fig1

对每次得到的结果进行累加,可以使用「415. 字符串相加 」的做法。

[sol1-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Solution {
public String multiply(String num1, String num2) {
if (num1.equals("0") || num2.equals("0")) {
return "0";
}
String ans = "0";
int m = num1.length(), n = num2.length();
for (int i = n - 1; i >= 0; i--) {
StringBuffer curr = new StringBuffer();
int add = 0;
for (int j = n - 1; j > i; j--) {
curr.append(0);
}
int y = num2.charAt(i) - '0';
for (int j = m - 1; j >= 0; j--) {
int x = num1.charAt(j) - '0';
int product = x * y + add;
curr.append(product % 10);
add = product / 10;
}
if (add != 0) {
curr.append(add % 10);
}
ans = addStrings(ans, curr.reverse().toString());
}
return ans;
}

public String addStrings(String num1, String num2) {
int i = num1.length() - 1, j = num2.length() - 1, add = 0;
StringBuffer ans = new StringBuffer();
while (i >= 0 || j >= 0 || add != 0) {
int x = i >= 0 ? num1.charAt(i) - '0' : 0;
int y = j >= 0 ? num2.charAt(j) - '0' : 0;
int result = x + y + add;
ans.append(result % 10);
add = result / 10;
i--;
j--;
}
ans.reverse();
return ans.toString();
}
}
[sol1-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Solution {
public:
string multiply(string num1, string num2) {
if (num1 == "0" || num2 == "0") {
return "0";
}
string ans = "0";
int m = num1.size(), n = num2.size();
for (int i = n - 1; i >= 0; i--) {
string curr;
int add = 0;
for (int j = n - 1; j > i; j--) {
curr.push_back(0);
}
int y = num2.at(i) - '0';
for (int j = m - 1; j >= 0; j--) {
int x = num1.at(j) - '0';
int product = x * y + add;
curr.push_back(product % 10);
add = product / 10;
}
while (add != 0) {
curr.push_back(add % 10);
add /= 10;
}
reverse(curr.begin(), curr.end());
for (auto &c : curr) {
c += '0';
}
ans = addStrings(ans, curr);
}
return ans;
}

string addStrings(string &num1, string &num2) {
int i = num1.size() - 1, j = num2.size() - 1, add = 0;
string ans;
while (i >= 0 || j >= 0 || add != 0) {
int x = i >= 0 ? num1.at(i) - '0' : 0;
int y = j >= 0 ? num2.at(j) - '0' : 0;
int result = x + y + add;
ans.push_back(result % 10);
add = result / 10;
i--;
j--;
}
reverse(ans.begin(), ans.end());
for (auto &c: ans) {
c += '0';
}
return ans;
}
};
[sol1-Python3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution:
def multiply(self, num1: str, num2: str) -> str:
if num1 == "0" or num2 == "0":
return "0"

ans = "0"
m, n = len(num1), len(num2)
for i in range(n - 1, -1, -1):
add = 0
y = int(num2[i])
curr = ["0"] * (n - i - 1)
for j in range(m - 1, -1, -1):
product = int(num1[j]) * y + add
curr.append(str(product % 10))
add = product // 10
if add > 0:
curr.append(str(add))
curr = "".join(curr[::-1])
ans = self.addStrings(ans, curr)

return ans

def addStrings(self, num1: str, num2: str) -> str:
i, j = len(num1) - 1, len(num2) - 1
add = 0
ans = list()
while i >= 0 or j >= 0 or add != 0:
x = int(num1[i]) if i >= 0 else 0
y = int(num2[j]) if j >= 0 else 0
result = x + y + add
ans.append(str(result % 10))
add = result // 10
i -= 1
j -= 1
return "".join(ans[::-1])
[sol1-Golang]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
func multiply(num1 string, num2 string) string {
if num1 == "0" || num2 == "0" {
return "0"
}
ans := "0"
m, n := len(num1), len(num2)
for i := n - 1; i >= 0; i-- {
curr := ""
add := 0
for j := n - 1; j > i; j-- {
curr += "0"
}
y := int(num2[i] - '0')
for j := m - 1; j >= 0; j-- {
x := int(num1[j] - '0')
product := x * y + add
curr = strconv.Itoa(product % 10) + curr
add = product / 10
}
for ; add != 0; add /= 10 {
curr = strconv.Itoa(add % 10) + curr
}
ans = addStrings(ans, curr)
}
return ans
}

func addStrings(num1, num2 string) string {
i, j := len(num1) - 1, len(num2) - 1
add := 0
ans := ""
for ; i >= 0 || j >= 0 || add != 0; i, j = i - 1, j - 1 {
x, y := 0, 0
if i >= 0 {
x = int(num1[i] - '0')
}
if j >= 0 {
y = int(num2[j] - '0')
}
result := x + y + add
ans = strconv.Itoa(result % 10) + ans
add = result / 10
}
return ans
}
[sol1-C]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
char* addStrings(char* num1, char* num2) {
int i = strlen(num1) - 1, j = strlen(num2) - 1, add = 0;
char* ans = malloc(sizeof(char) * (i + j + 5));
int ansLen = 0;
while (i >= 0 || j >= 0 || add != 0) {
int x = i >= 0 ? num1[i] - '0' : 0;
int y = j >= 0 ? num2[j] - '0' : 0;
int result = x + y + add;
ans[ansLen++] = result % 10;
add = result / 10;
i--;
j--;
}
for (int i = 0; i < ansLen / 2; i++) {
char t = ans[i];
ans[i] = ans[ansLen - 1 - i];
ans[ansLen - 1 - i] = t;
}
for (int i = 0; i < ansLen; i++) {
ans[i] += '0';
}
ans[ansLen++] = 0;
return ans;
}

char* multiply(char* num1, char* num2) {
int m = strlen(num1), n = strlen(num2);
char* ans = malloc(sizeof(char) * 2);
ans[0] = '0', ans[1] = 0;
if ((m == 1 && num1[0] == '0') || (n == 1 && num2[0] == '0')) {
return ans;
}
for (int i = n - 1; i >= 0; i--) {
char* curr = malloc(sizeof(char) * (n + m + 5));
int currLen = 0;
int add = 0;
for (int j = n - 1; j > i; j--) {
curr[currLen++] = 0;
}
int y = num2[i] - '0';
for (int j = m - 1; j >= 0; j--) {
int x = num1[j] - '0';
int product = x * y + add;
curr[currLen++] = product % 10;
add = product / 10;
}
while (add != 0) {
curr[currLen++] = add % 10;
add /= 10;
}
for (int i = 0; i < currLen / 2; i++) {
char t = curr[i];
curr[i] = curr[currLen - 1 - i];
curr[currLen - 1 - i] = t;
}
for (int i = 0; i < currLen; i++) {
curr[i] += '0';
}
curr[currLen++] = 0;
char* tmp = addStrings(ans, curr);
free(ans), free(curr);
ans = tmp;
}
return ans;
}

复杂度分析

  • 时间复杂度:$O(mn+n^2)$,其中 $m$ 和 $n$ 分别是 $\textit{num}_1$ 和 $\textit{num}_2$ 的长度。需要从右往左遍历 $\textit{num}_2$,对于 $\textit{num}_2$ 的每一位,都需要和 $\textit{num}_1$ 的每一位计算乘积,因此计算乘积的总次数是 $mn$。字符串相加操作共有 $n$ 次,相加的字符串长度最长为 $m+n$,因此字符串相加的时间复杂度是 $O(mn+n^2)$。总时间复杂度是 $O(mn+n^2)$。

  • 空间复杂度:$O(m+n)$,其中 $m$ 和 $n$ 分别是 $\textit{num}_1$ 和 $\textit{num}_2$ 的长度。空间复杂度取决于存储中间状态的字符串,由于乘积的最大长度为 $m+n$,因此存储中间状态的字符串的长度不会超过 $m+n$。

方法二:做乘法

方法一的做法是从右往左遍历乘数,将乘数的每一位与被乘数相乘得到对应的结果,再将每次得到的结果累加,整个过程中涉及到较多字符串相加的操作。如果使用数组代替字符串存储结果,则可以减少对字符串的操作。

令 $m$ 和 $n$ 分别表示 $\textit{num}_1$ 和 $\textit{num}_2$ 的长度,并且它们均不为 $0$,则 $\textit{num}_1$ 和 $\textit{num}_2$ 的乘积的长度为 $m+n-1$ 或 $m+n$。简单证明如下:

  • 如果 $\textit{num}_1$ 和 $\textit{num}_2$ 都取最小值,则 $\textit{num}_1=10^{m-1}$,$\textit{num}_2=10^{n-1}$,$\textit{num}_1 \times \textit{num}_2=10^{m+n-2}$,乘积的长度为 $m+n-1$;

  • 如果 $\textit{num}_1$ 和 $\textit{num}_2$ 都取最大值,则 $\textit{num}_1=10^m-1$,$\textit{num}_2=10^n-1$,$\textit{num}_1 \times \textit{num}_2=10^{m+n}-10^m-10^n+1$,乘积显然小于 $10^{m+n}$ 且大于 $10^{m+n-1}$,因此乘积的长度为 $m+n$。

由于 $\textit{num}_1$ 和 $\textit{num}_2$ 的乘积的最大长度为 $m+n$,因此创建长度为 $m+n$ 的数组 $\textit{ansArr}$ 用于存储乘积。对于任意 $0 \le i < m$ 和 $0 \le j < n$,$\textit{num}_1[i] \times \textit{num}_2[j]$ 的结果位于 $\textit{ansArr}[i+j+1]$,如果 $\textit{ansArr}[i+j+1] \ge 10$,则将进位部分加到 $\textit{ansArr}[i+j]$。

最后,将数组 $\textit{ansArr}$ 转成字符串,如果最高位是 $0$ 则舍弃最高位。

<fig1,fig2,fig3,fig4,fig5,fig6,fig7,fig8,fig9,fig10,fig11,fig12,fig13>

[sol2-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
public String multiply(String num1, String num2) {
if (num1.equals("0") || num2.equals("0")) {
return "0";
}
int m = num1.length(), n = num2.length();
int[] ansArr = new int[m + n];
for (int i = m - 1; i >= 0; i--) {
int x = num1.charAt(i) - '0';
for (int j = n - 1; j >= 0; j--) {
int y = num2.charAt(j) - '0';
ansArr[i + j + 1] += x * y;
}
}
for (int i = m + n - 1; i > 0; i--) {
ansArr[i - 1] += ansArr[i] / 10;
ansArr[i] %= 10;
}
int index = ansArr[0] == 0 ? 1 : 0;
StringBuffer ans = new StringBuffer();
while (index < m + n) {
ans.append(ansArr[index]);
index++;
}
return ans.toString();
}
}
[sol2-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
public:
string multiply(string num1, string num2) {
if (num1 == "0" || num2 == "0") {
return "0";
}
int m = num1.size(), n = num2.size();
auto ansArr = vector<int>(m + n);
for (int i = m - 1; i >= 0; i--) {
int x = num1.at(i) - '0';
for (int j = n - 1; j >= 0; j--) {
int y = num2.at(j) - '0';
ansArr[i + j + 1] += x * y;
}
}
for (int i = m + n - 1; i > 0; i--) {
ansArr[i - 1] += ansArr[i] / 10;
ansArr[i] %= 10;
}
int index = ansArr[0] == 0 ? 1 : 0;
string ans;
while (index < m + n) {
ans.push_back(ansArr[index]);
index++;
}
for (auto &c: ans) {
c += '0';
}
return ans;
}
};
[sol2-Python3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution:
def multiply(self, num1: str, num2: str) -> str:
if num1 == "0" or num2 == "0":
return "0"

m, n = len(num1), len(num2)
ansArr = [0] * (m + n)
for i in range(m - 1, -1, -1):
x = int(num1[i])
for j in range(n - 1, -1, -1):
ansArr[i + j + 1] += x * int(num2[j])

for i in range(m + n - 1, 0, -1):
ansArr[i - 1] += ansArr[i] // 10
ansArr[i] %= 10

index = 1 if ansArr[0] == 0 else 0
ans = "".join(str(x) for x in ansArr[index:])
return ans
[sol2-Golang]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func multiply(num1 string, num2 string) string {
if num1 == "0" || num2 == "0" {
return "0"
}
m, n := len(num1), len(num2)
ansArr := make([]int, m + n)
for i := m - 1; i >= 0; i-- {
x := int(num1[i]) - '0'
for j := n - 1; j >= 0; j-- {
y := int(num2[j] - '0')
ansArr[i + j + 1] += x * y
}
}
for i := m + n - 1; i > 0; i-- {
ansArr[i - 1] += ansArr[i] / 10
ansArr[i] %= 10
}
ans := ""
idx := 0
if ansArr[0] == 0 {
idx = 1
}
for ; idx < m + n; idx++ {
ans += strconv.Itoa(ansArr[idx])
}
return ans
}
[sol2-C]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
char* multiply(char* num1, char* num2) {
int m = strlen(num1), n = strlen(num2);
char* ans = malloc(sizeof(char) * (m + n + 3));
memset(ans, 0, sizeof(char) * (m + n + 3));
if ((m == 1 && num1[0] == '0') || (n == 1 && num2[0] == '0')) {
ans[0] = '0', ans[1] = 0;
return ans;
}
int* ansArr = malloc(sizeof(int) * (m + n + 3));
memset(ansArr, 0, sizeof(int) * (m + n + 3));
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';
ansArr[i + j + 1] += x * y;
}
}
for (int i = m + n - 1; i > 0; i--) {
ansArr[i - 1] += ansArr[i] / 10;
ansArr[i] %= 10;
}
int index = ansArr[0] == 0 ? 1 : 0;
int ansLen = 0;
while (index < m + n) {
ans[ansLen++] = ansArr[index];
index++;
}
for (int i = 0; i < ansLen; i++) ans[i] += '0';
return ans;
}

复杂度分析

  • 时间复杂度:$O(mn)$,其中 $m$ 和 $n$ 分别是 $\textit{num}_1$ 和 $\textit{num}_2$ 的长度。需要计算 $\textit{num}_1$ 的每一位和 $\textit{num}_2$ 的每一位的乘积。

  • 空间复杂度:$O(m+n)$,其中 $m$ 和 $n$ 分别是 $\textit{num}_1$ 和 $\textit{num}_2$ 的长度。需要创建一个长度为 $m+n$ 的数组存储乘积。

结语

方法二还可以用另外一种方法改写。我们把两个数相乘看成是两个多项式相乘,因为任何一个数都可以表示成为

$$
\sum_{i = 0}^{n - 1} a_i \times 10^i
$$

的形式,也就相当于对多项式

$$
A(x) = \sum_{i = 0} ^ {n - 1} a_i x^i
$$

在 $x = 10$ 处求值。当两个数 $N_a$、$N_b$ 相乘的时候,我们也可以认为这两个数是两个多项式

$$
\left{
\begin{aligned}
& A(x) = \sum_{i = 0} ^ {n - 1} a_i x^i \
& B(x) = \sum_{i = 0} ^ {m - 1} b_i x^i
\end{aligned}
\right.
$$

相乘的结果 $C(x) = A(x) \times B(x)$ 在 $x = 10$ 处求值。我们可以这样表示 $C(x)$:

$$
C(x) = \sum_{i = 0}^{n + m - 2} c_i x^i
$$

这里

$$
c_i = \sum_{k = 0} ^ {i} a_k b_{i - k}
$$

于是我们就可以顺序求解 $c_i$,每次 $O(i)$ 地选取下标和为 $i$ 的一组 $(a_k, b_{i - k})$。求到 $c_i$ 序列之后,再处理进位即可得到答案。对比这两种做法:

  • 顺序求解 $c_i$ 的过程相当于集中计算 $c_i$
  • 而方法二相当于每对 $(a_i, b_j)$ 对 $c_{i + j}$ 算贡献(注意这里的 $a_i$ 并不是题目中的 ${\it num}_1[i]$,$a_i$ 下标越小,代表的位权越小,而 ${\it num}_1[i]$ 下标越小,代表的位权越大)

它们的本质是一样的,并且时间复杂度都是 $O(\max { n, m} ^2)$。我们再仔细的观察 $c_i$ 的形式:

$$
c_i = \sum_{k = 0}^{i} a_k b_{i - k}
$$

它揭示了多项式乘法的另一面:$c_i$ 序列其实是 $a_i$ 序列和 $b_i$ 序列的卷积,即:

$$
\vec{c} = \vec{a} * \vec{b}
$$

至此,我们就可以用一种叫做快速傅立叶变换(Fast Fourier Transform,FFT)的方法来加速卷积的计算,使得时间复杂度降低到 $O(c \log c)$,这里 $c$ 是不小于 $n + m$ 的最小的 $2$ 的整数幂。由于这个方法并不在面试的考纲范围内,感兴趣的同学可以自行学习。

 Comments
On this page
0043-字符串相乘