作者介绍:10年大厂数据\经营分析经验,现任大厂数据部门负责人。
会一些的技术:数据分析、算法、SQL、大数据相关、python
欢迎加入社区:码上找工作
作者专栏每日更新:
LeetCode解锁1000题: 打怪升级之旅
python数据分析可视化:企业实战案例
备注说明:方便大家阅读,统一使用python,带必要注释,公众号 数据分析螺丝钉 一起打怪升级
LeetCode题目43 “字符串相乘” 要求计算两个以字符串形式表示的非负整数的乘积,并返回结果的字符串形式。本文将详细介绍解决这一问题的第一种方法:模拟乘法手算过程。
题目描述
输入:两个字符串 num1
和 num2
,分别表示两个非负整数。
输出:一个字符串,表示 num1
和 num2
的乘积。
输入输出格式
输入
- num1,num2:非负整数的字符串形式,不包含任何前导零,除非是数字0本身。
输出
- 返回值:两个整数乘积的字符串形式。
示例
示例 1
输入: num1 = "2", num2 = "3"
输出: "6"
示例 2
输入: num1 = "123", num2 = "456"
输出: "56088"
方法一:模拟乘法手算
解题步骤
- 特殊情况处理:如果
num1
或num2
为 “0”,直接返回 “0”。 - 初始化结果数组:创建一个长度为
len(num1) + len(num2)
的数组result
来存储乘积的每一位。 - 逐位相乘:遍历
num1
和num2
的每一位,将每一位的乘积累加到result
的对应位置。 - 处理进位:从
result
的最后一位开始处理进位,将每位的数字处理为个位数,进位加到下一位。 - 生成结果字符串:将
result
数组转换成字符串,去除前导零。
详细规范带中文注释的代码示例
def multiply(num1: str, num2: str) -> str:
# 特殊情况处理,任一数字为'0',结果即为'0'
if num1 == "0" or num2 == "0":
return "0"
# 结果最多为两数字长度之和
result = [0] * (len(num1) + len(num2))
# 从后向前遍历两个字符串
for i in range(len(num1)-1, -1, -1):
for j in range(len(num2)-1, -1, -1):
# 乘积在result对应的索引位置
mul = (ord(num1[i]) - ord('0')) * (ord(num2[j]) - ord('0'))
# 加上result[i+j+1]位置的旧值
sum_ = mul + result[i+j+1]
# 更新result数组
result[i+j+1] = sum_ % 10 # 当前位
result[i+j] += sum_ // 10 # 进位
# 将结果数组转换为字符串,跳过前导零
result_str = ''.join(map(str, result))
return result_str.lstrip('0') # 去除可能存在的前导零
# 示例调用
print(multiply("123", "456")) # 输出: "56088"
算法分析
- 时间复杂度:O(MN),其中 M 和 N 分别是
num1
和num2
的长度。每一位的乘积需要遍历一次,共有 MN 次操作。 - 空间复杂度:O(M+N),用于存储乘积的数组。
方法二:基于四则运算优化的分治算法(Karatsuba算法)
当处理非常大的数字乘法时,传统的逐位相乘算法可能不够高效。Karatsuba算法提供了一个更快的递归方法,利用分治技术来加速大数的乘法过程。这种方法基于数位分割,将大数乘法问题转换为小数乘法问题,从而减少了乘法的总次数。
解题步骤
- 分割数字:将每个数分割成两部分,例如,123456可以分割为123和456,分割点基于数的长度。
- 递归计算:
- 设两个大数为 ( a ) 和 ( b ),分别分割为 ( a1, a0 ) 和 ( b1, b0 ),其中 ( a1, b1 ) 是高位部分,( a0, b0 ) 是低位部分。
- 根据Karatsuba乘法,计算三个乘积:
- ( p1 = a1 \times b1 )
- ( p2 = a0 \times b0 )
- ( p3 = (a1 + a0) \times (b1 + b0) )
- 结果可以通过这三个乘积组合得到:( result = p1 \times 10^{2m} + (p3 - p1 - p2) \times 10^m + p2 ),其中 ( m ) 是分割的位数。
- 合并结果:根据上述公式合并计算结果,得到最终的乘积。
代码示例
def multiply(num1: str, num2: str) -> str:
# 基础情况处理
if len(num1) == 1 or len(num2) == 1:
return str(int(num1) * int(num2))
# 使得num1和num2长度相同,前面补0
max_len = max(len(num1), len(num2))
num1 = num1.zfill(max_len)
num2 = num2.zfill(max_len)
# 分割数字
m = max_len // 2
high1, low1 = num1[:-m], num1[-m:]
high2, low2 = num2[:-m], num2[-m:]
# 递归计算p1, p2, p3
z0 = multiply(low1, low2)
z1 = multiply(str(int(low1) + int(high1)), str(int(low2) + int(high2)))
z2 = multiply(high1, high2)
# 计算并合并结果
return str(int(z2 + '0'*(2*m)) + (int(z1) - int(z2) - int(z0)) * (10**m) + int(z0))
# 示例调用
print(multiply("123", "456")) # 输出: "56088"
算法分析
- 时间复杂度:O(n1.585)。Karatsuba算法相比于传统O(n2)的方法有明显的时间复杂度优势,特别是在处理非常大的数字时。
- 空间复杂度:O(n),主要消耗在递归过程中对数字的分割以及递归栈的使用上。
方法三:FFT(快速傅立叶变换)基础上的多项式乘法
快速傅立叶变换(FFT)是处理大整数乘法的一种高效算法,特别适用于数字非常大的情况。FFT可以将多项式乘法的时间复杂度从 (O(n^2)) 降低到 (O(n \log n)),使得它在多项式乘法和大整数乘法中非常有效。
解题步骤
- 表示为点值形式:将两个数表示为点值形式,这可以通过评估多项式在各点上的值实现。
- 使用FFT计算点值乘法:将两个输入数的多项式表示通过FFT转换到频域(点值形式),然后对应点值相乘。
- 逆FFT变换:将乘法后的点值结果通过逆FFT变换回系数表示形式,即得到原多项式乘法的结果。
- 处理结果和进位:将FFT的结果转换为数字,处理进位。
代码示例
这里提供一个基于FFT的多项式乘法的Python简化实现,考虑到FFT的复杂性,实际使用时可能需要依赖专门的库如numpy
或scipy
。
import numpy as np
def fft(a):
n = len(a)
if n == 1:
return a
omega_n = np.exp(2j * np.pi / n)
omega = 1
a0 = a[0::2]
a1 = a[1::2]
y0 = fft(a0)
y1 = fft(a1)
y = np.zeros(n, dtype=complex)
for k in range(n // 2):
y[k] = y0[k] + omega * y1[k]
y[k + n // 2] = y0[k] - omega * y1[k]
omega *= omega_n
return y
def ifft(y):
n = len(y)
y_conj = np.conjugate(y)
a = fft(y_conj)
a = np.conjugate(a)
return a / n
def multiply(num1, num2):
# 初始化数据
n = 1
while n < max(len(num1), len(num2)):
n *= 2
n *= 2
a = np.array([int(x) for x in num1][::-1] + [0] * (n - len(num1)), dtype=complex)
b = np.array([int(x) for x in num2][::-1] + [0] * (n - len(num2)), dtype=complex)
# FFT
A = fft(a)
B = fft(b)
C = A * B
# 逆FFT
c = ifft(C).real.round().astype(int)
# 处理进位
carry = 0
for i in range(len(c)):
c[i] += carry
carry = c[i] // 10
c[i] %= 10
# 去除前导0并转换为字符串
while len(c) > 1 and c[-1] == 0:
c = c[:-1]
return ''.join(map(str, c[::-1]))
# 示例调用
print(multiply("123", "456")) # 输出: "56088"
算法分析
- 时间复杂度:O(n \log n),其中 n 是输入数字的位数。这是由FFT和逆FFT操作的复杂度决定的。
- 空间复杂度:O(n),主要是存储FFT结果和临时数组的需要。
总结
下面的表格将展示它们在不同技术维度的性能和特点,包括时间复杂度、空间复杂度以及各自的优势和劣势。
特征 | 方法一:模拟乘法手算 | 方法二:Karatsuba算法 | 方法三:FFT乘法 |
---|---|---|---|
时间复杂度 | O(M*N) | O(N^1.585) | O(N log N) |
空间复杂度 | O(M+N) | O(N) | O(N) |
优势 | - 直观且易于实现 - 适合小数字乘法 | - 快于传统方法 - 适合中等大小数字 | - 非常高效,特别适合大数字乘法 - 大规模数字处理的首选 |
劣势 | - 时间复杂度较高 - 不适合大规模数字 | - 实现复杂度较高 - 需要处理数字分割 | - 需要FFT库支持 - 处理进位相对复杂 |
适用场景 | - 数字较小 - 教学或简单应用 | - 数字大小适中 - 需要比直接方法快但实现复杂度可接受 | - 处理非常大的数字 - 高性能计算需求 |