字符串相加问题详解:从简单到复杂的全面分析
一、基本概念
1. 什么是字符串相加?
字符串相加是一类特殊的算法问题,主要处理以字符串形式表示的数字的加法运算。这类问题的特点是:
- 输入是以字符串形式表示的数字
- 数字可能非常大,超出常规整数类型的范围
- 需要模拟人工加法的过程
- 结果同样以字符串形式返回
例如:
1
2
"123" + "456" = "579"
"1234567890" + "9876543210" = "11111111100"
2. 为什么需要字符串相加?
- 处理大数
- 超出整数范围的数字计算
- 避免数值溢出问题
- 保持精确计算
- 实际应用场景
- 金融计算
- 科学计算
- 大数据处理
- 算法思维训练
- 培养模拟能力
- 提高字符串处理能力
- 锻炼进位处理思维
3. 基本解题思路
- 对齐处理
- 从个位开始对齐
- 处理不等长字符串
- 补零对齐
- 逐位相加
- 按位计算和
- 处理进位
- 保存结果
- 结果处理
- 处理最高位进位
- 去除前导零
- 返回最终字符串
二、基础实现方案
1. 基础实现
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
func addStrings(_ num1: String, _ num2: String) -> String {
// 处理边界情况
guard !num1.isEmpty && !num2.isEmpty else {
return num1.isEmpty ? num2 : num1
}
var chars1 = Array(num1)
var chars2 = Array(num2)
var result = ""
var carry = 0
// 从后向前遍历
var i = chars1.count - 1
var j = chars2.count - 1
while i >= 0 || j >= 0 || carry > 0 {
let digit1 = i >= 0 ? Int(String(chars1[i]))! : 0
let digit2 = j >= 0 ? Int(String(chars2[j]))! : 0
let sum = digit1 + digit2 + carry
carry = sum / 10
result = String(sum % 10) + result
i -= 1
j -= 1
}
return result
}
三、进阶问题
1. 数字字符串加一
问题描述:给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。
示例:
1
2
3
4
5
6
7
输入: [1,2,3]
输出: [1,2,4]
解释: 123 + 1 = 124
输入: [9,9,9]
输出: [1,0,0,0]
解释: 999 + 1 = 1000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func plusOne(_ digits: [Int]) -> [Int] {
var result = digits
for i in (0..<result.count).reversed() {
if result[i] < 9 {
result[i] += 1
return result
}
result[i] = 0
}
result.insert(1, at: 0)
return result
}
2. 二进制字符串相加
给你两个二进制字符串 a 和 b ,以二进制字符串的形式返回它们的和。
示例1:
1
2
3
4
5
6
7
输入: a = "11", b = "1"
输出: "100"
解释:
11
1
---
100
示例2:
1
2
3
4
5
6
7
输入: a = "1010", b = "1011"
输出: "10101"
解释:
1010
1011
------
10101
关键点:
- 二进制加法规则:1+1=10
- 需要处理进位
- 结果可能比输入字符串更长
与十进制加法的区别:
- 进位规则不同(逢2进1,而不是逢10进1)
- 每位只能是0或1
- 计算更简单但容易出错
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
func addBinary(_ a: String, _ b: String) -> String {
// 特殊情况处理
if a == "0" { return b }
if b == "0" { return a }
var result = ""
var carry = 0
let chars1 = Array(a)
let chars2 = Array(b)
var i = chars1.count - 1
var j = chars2.count - 1
// 从后向前遍历
while i >= 0 || j >= 0 || carry > 0 {
let bit1 = i >= 0 ? Int(String(chars1[i]))! : 0
let bit2 = j >= 0 ? Int(String(chars2[j]))! : 0
// 二进制加法
let sum = bit1 + bit2 + carry
carry = sum / 2
result = String(sum % 2) + result
i -= 1
j -= 1
}
return result
}
// 优化版本:使用位运算 -- 可以忽略不计 不好理解
func addBinaryOptimized(_ a: String, _ b: String) -> String {
// 将二进制字符串转换为字符数组
let chars1 = Array(a)
let chars2 = Array(b)
let n1 = chars1.count
let n2 = chars2.count
let maxLen = max(n1, n2)
// 使用数组存储每一位的结果,避免字符串拼接
var result = Array(repeating: "0", count: maxLen + 1)
var carry = 0
// 从低位到高位处理
for i in 0..<maxLen {
// 使用位运算获取当前位
let p1 = n1 - 1 - i
let p2 = n2 - 1 - i
// 使用位运算计算当前位的值
let bit1 = p1 >= 0 ? chars1[p1] == "1" ? 1 : 0 : 0
let bit2 = p2 >= 0 ? chars2[p2] == "1" ? 1 : 0 : 0
// 使用异或运算计算当前位
let sum = bit1 ^ bit2 ^ carry
// 使用与运算计算进位
// carry = 1 当且仅当至少有两个1
carry = (bit1 & bit2) | (bit1 & carry) | (bit2 & carry)
// 从低位开始填充结果
result[maxLen - i] = String(sum)
}
// 处理最高位的进位
if carry > 0 {
result[0] = "1"
return result.joined()
}
// 去除前导零
return result[1...].joined()
}
// 添加测试用例和解释
/*
位运算优化说明:
1. 异或运算(^):计算不带进位的和
0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 0 = 1
1 ^ 1 = 0
2. 与运算(&):计算进位
当有两个或以上的1时产生进位
可以用 (a & b) | (a & c) | (b & c) 判断
3. 性能优化:
- 避免字符串拼接操作
- 使用位运算代替算术运算
- 预分配结果数组空间
测试用例:
a = "10100000100100110110010000010101111011011001101110111111111101000000101111001110001111100001101"
b = "110101001011101110001111100110001010100001101011101010000011011011001011101111001100000011011110011"
解释:
1. 预分配长度为max(len(a), len(b)) + 1的结果数组
2. 从低位到高位,使用位运算处理每一位
3. 使用异或运算计算当前位的值
4. 使用与运算计算进位
*/
3. 复数字符串运算
给定两个表示复数的字符串,返回表示它们乘积的字符串。注:复数的格式为”a+bi”,其中a和b都是实数。
示例1:
1
2
3
4
输入: "1+1i", "1+1i"
输出: "0+2i"
解释:
(1 + i) * (1 + i) = 1 - 1 + 2i = 2i
示例2:
1
2
3
4
输入: "1+-1i", "1+-1i"
输出: "2+-2i"
解释:
(1 - i) * (1 - i) = 1 + 1 - 2i = 2 - 2i
关键点:
- 需要分别处理实部和虚部
- 字符串解析要处理负数
- 结果格式需要统一
这三个问题的共同点:
- 都需要处理字符串形式的数字
- 都涉及进位处理
- 都需要考虑特殊情况
主要区别:
- 进制不同(十进制、二进制)
- 数据类型不同(整数、复数)
- 运算规则不同(加法、乘法)
易错点总结:
- 进位处理不当
- 特殊情况未考虑(0、负数、空串)
- 结果格式不统一
- 字符串解析错误
优化方向:
- 使用数组预处理提高效率
- 原地修改减少空间使用
- 特殊情况早判断避免无效计算
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
66
67
68
69
70
71
72
73
func complexNumberMultiply(_ num1: String, _ num2: String) -> String {
// 解析复数字符串
func parseComplex(_ s: String) -> (Int, Int) {
// 处理带负号的情况
let parts = s.split(separator: "+")
var real: Int
var imag: Int
if parts.count == 1 {
// 只有一部分,可能是纯实数或纯虚数
let part = String(parts[0])
if part.hasSuffix("i") {
// 纯虚数
imag = Int(part.dropLast()) ?? 0
real = 0
} else {
// 纯实数
real = Int(part) ?? 0
imag = 0
}
} else {
// 有实部和虚部
real = Int(parts[0]) ?? 0
imag = Int(String(parts[1].dropLast())) ?? 0
}
return (real, imag)
}
// 解析两个复数
let (real1, imag1) = parseComplex(num1)
let (real2, imag2) = parseComplex(num2)
// 计算复数乘法
// (a + bi)(c + di) = (ac - bd) + (ad + bc)i
let realResult = real1 * real2 - imag1 * imag2
let imagResult = real1 * imag2 + real2 * imag1
// 构造结果字符串
return "\(realResult)+\(imagResult)i"
}
// 优化版本:支持更复杂的格式和边界情况
func complexNumberMultiplyOptimized(_ num1: String, _ num2: String) -> String {
// 增强的复数解析函数
func parseComplexEnhanced(_ s: String) -> (Int, Int) {
var str = s.replacingOccurrences(of: " ", with: "")
// 分割实部和虚部
let components = str.split(separator: "+", maxSplits: 1)
// 处理实部
let real = Int(components[0]) ?? 0
// 处理虚部(去掉'i'并转换为整数)
var imag = 0
if components.count > 1 {
let imagStr = String(components[1].dropLast()) // 去掉'i'
imag = Int(imagStr) ?? 0
}
return (real, imag)
}
let (real1, imag1) = parseComplexEnhanced(num1)
let (real2, imag2) = parseComplexEnhanced(num2)
// 复数乘法
let realResult = real1 * real2 - imag1 * imag2
let imagResult = real1 * imag2 + real2 * imag1
// 直接返回标准格式
return "\(realResult)+\(imagResult)i"
}
四、优化技巧
1. 空间优化
- 使用原地修改而不是创建新字符串
- 预分配合适大小的数组
- 复用已有空间
2. 时间优化
- 使用位运算代替除法和取模
- 避免不必要的字符串转换
- 特殊情况提前处理
3. 代码优化
- 提取公共函数
- 使用更清晰的变量命名
- 添加适当的注释
五、常见错误和解决方案
- 进位处理错误
1 2 3 4
// 错误示例 let sum = digit1 + digit2 // 漏掉了carry // 正确处理 let sum = digit1 + digit2 + carry
- 边界情况处理不当
1 2 3 4
// 错误示例 let digit = Int(String(chars[i]))! // 可能崩溃 // 正确处理 let digit = i >= 0 ? Int(String(chars[i])) ?? 0 : 0
- 格式化输出问题
1 2 3 4 5
// 错误示例 return "\(real)+\(imag)i" // 没有处理负数情况 // 正确处理 let sign = imag >= 0 ? "+" : "" return "\(real)\(sign)\(imag)i"
六、性能分析
1. 时间复杂度
- 基本实现:O(max(n,m)),n和m为输入字符串的长度
- 多字符串相加:O(n*k),k为字符串数量
- 优化版本:O(max(n,m)),但常数更小
2. 空间复杂度
- 基本实现:O(max(n,m))
- 原地修改:O(1)
- 查表优化:O(1)额外空间,但有固定大小的表
七、实战技巧
1. 编写字符串相加代码的步骤
- 处理特殊情况(空字符串、零等)
- 对齐字符串
- 实现逐位相加
- 处理进位
- 优化性能
2. 调试技巧
- 使用小规模测试用例
- 验证边界情况
- 检查进位处理
- 注意前导零
3. 代码模板
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
// 通用字符串相加模板
func stringAddition(_ num1: String, _ num2: String) -> String {
// 1. 参数验证
guard !num1.isEmpty && !num2.isEmpty else {
return num1.isEmpty ? num2 : num1
}
// 2. 初始化变量
var result = ""
var carry = 0
var i = num1.count - 1
var j = num2.count - 1
let chars1 = Array(num1)
let chars2 = Array(num2)
// 3. 主循环
while i >= 0 || j >= 0 || carry > 0 {
// 获取当前位
let digit1 = i >= 0 ? Int(String(chars1[i]))! : 0
let digit2 = j >= 0 ? Int(String(chars2[j]))! : 0
// 计算和与进位
let sum = digit1 + digit2 + carry
carry = sum / 10
result = String(sum % 10) + result
// 移动指针
i -= 1
j -= 1
}
// 4. 返回结果
return result
}
八、总结
1. 适用场景
- 大数计算
- 精确计算
- 进制转换
- 复数运算
2. 优缺点
优点:
- 可处理任意大的数字
- 精确计算无误差
- 实现简单直观
缺点:
- 计算速度较慢
- 需要额外空间
- 代码较为冗长
3. 实践建议
- 注意边界条件
- 合理使用优化技巧
- 重视代码可读性
- 做好错误处理
九、练习题推荐
- LeetCode相关题目:
- #415 字符串相加
- #67 二进制求和
- #43 字符串相乘
- #989 数组形式的整数加法
- #2 两数相加(链表版本)
- 进阶练习:
- #66 加一
- #369 给单链表加一
- #445 两数相加 II
- #537 复数乘法
本文由作者按照 CC BY 4.0 进行授权