docs: improve solution explanations for 最长回文子串 and 括号生成
- 添加思路推导部分,从暴力解法分析优化过程 - 增加详细的算法流程和Q&A形式的解释 - 添加执行过程演示和常见错误分析 - 完善边界条件和复杂度分析 - 保持原有的代码实现和进阶问题
This commit is contained in:
@@ -22,54 +22,353 @@
|
|||||||
|
|
||||||
- `1 <= n <= 8`
|
- `1 <= n <= 8`
|
||||||
|
|
||||||
|
## 思路推导
|
||||||
|
|
||||||
|
### 暴力解法分析
|
||||||
|
|
||||||
|
**第一步:最直观的思路 - 生成所有可能的组合**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def generateParenthesis_brute(n):
|
||||||
|
# 生成所有长度为 2n 的括号组合
|
||||||
|
all_combinations = []
|
||||||
|
generate_all("", 2*n, all_combinations)
|
||||||
|
|
||||||
|
# 过滤出有效的组合
|
||||||
|
valid_combinations = []
|
||||||
|
for combo in all_combinations:
|
||||||
|
if is_valid(combo):
|
||||||
|
valid_combinations.append(combo)
|
||||||
|
|
||||||
|
return valid_combinations
|
||||||
|
|
||||||
|
def generate_all(current, max_len, result):
|
||||||
|
if len(current) == max_len:
|
||||||
|
result.append(current)
|
||||||
|
return
|
||||||
|
|
||||||
|
generate_all(current + "(", max_len, result)
|
||||||
|
generate_all(current + ")", max_len, result)
|
||||||
|
|
||||||
|
def is_valid(s):
|
||||||
|
balance = 0
|
||||||
|
for char in s:
|
||||||
|
if char == "(":
|
||||||
|
balance += 1
|
||||||
|
else:
|
||||||
|
balance -= 1
|
||||||
|
if balance < 0:
|
||||||
|
return False
|
||||||
|
return balance == 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**时间复杂度分析:**
|
||||||
|
- 生成所有组合:每个位置有 2 种选择,共 2^(2n) 个组合
|
||||||
|
- 判断每个组合是否有效:O(n)
|
||||||
|
- **总时间复杂度:O(n × 2^(2n))**
|
||||||
|
|
||||||
|
**问题:**
|
||||||
|
- 生成了大量无效组合
|
||||||
|
- 对于 n = 8,2^(16) = 65536 个组合,但有效的只有卡特兰数 C(8) = 1430 个
|
||||||
|
- 效率极低,大部分计算都浪费了
|
||||||
|
|
||||||
|
### 优化思考 - 如何避免生成无效组合?
|
||||||
|
|
||||||
|
**核心观察:**
|
||||||
|
1. **暴力法的瓶颈**:生成了大量无效组合,最后才过滤
|
||||||
|
2. **优化方向**:能否在生成过程中就避免无效组合?
|
||||||
|
|
||||||
|
**关键问题:什么是无效组合?**
|
||||||
|
|
||||||
|
无效组合的特征:
|
||||||
|
1. 右括号数量超过左括号数量:")()("
|
||||||
|
2. 最终左右括号数量不相等:"((()"
|
||||||
|
|
||||||
|
**思路:在生成过程中实时检查**
|
||||||
|
|
||||||
|
与其生成所有组合再过滤,不如:
|
||||||
|
1. 在添加每个括号时就检查是否合法
|
||||||
|
2. 如果不合法,直接跳过这个分支(剪枝)
|
||||||
|
3. 只生成有可能合法的组合
|
||||||
|
|
||||||
|
```python
|
||||||
|
def generateParenthesis_optimized(n):
|
||||||
|
result = []
|
||||||
|
|
||||||
|
def backtrack(current, open_count, close_count):
|
||||||
|
# 终止条件
|
||||||
|
if len(current) == 2 * n:
|
||||||
|
result.append(current)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 关键剪枝条件
|
||||||
|
# 1. 左括号数量不能超过 n
|
||||||
|
if open_count < n:
|
||||||
|
backtrack(current + "(", open_count + 1, close_count)
|
||||||
|
|
||||||
|
# 2. 右括号数量不能超过左括号数量
|
||||||
|
if close_count < open_count:
|
||||||
|
backtrack(current + ")", open_count, close_count + 1)
|
||||||
|
|
||||||
|
backtrack("", 0, 0)
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
### 为什么这样思考?
|
||||||
|
|
||||||
|
**1. 剪枝思想(Pruning)**
|
||||||
|
```
|
||||||
|
暴力法:生成所有 2^(2n) 个组合,然后过滤
|
||||||
|
↓
|
||||||
|
优化:在生成过程中就剪掉不可能的分支
|
||||||
|
↓
|
||||||
|
效果:只生成卡特兰数 C(n) 个有效组合
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 有效括号的本质**
|
||||||
|
```
|
||||||
|
有效括号的两个充要条件:
|
||||||
|
1. 任何时候,左括号数量 >= 右括号数量
|
||||||
|
2. 最终,左括号数量 = 右括号数量 = n
|
||||||
|
|
||||||
|
这意味着:
|
||||||
|
- 可以添加左括号的条件:open < n
|
||||||
|
- 可以添加右括号的条件:close < open
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. 递归树视角**
|
||||||
|
```
|
||||||
|
n = 2 时的递归树:
|
||||||
|
|
||||||
|
"" (open=0, close=0)
|
||||||
|
├── "(" (open=1, close=0)
|
||||||
|
│ ├── "((" (open=2, close=0)
|
||||||
|
│ │ └── "(()" (open=2, close=1)
|
||||||
|
│ │ └── "(())" (open=2, close=2) ✓
|
||||||
|
│ └── "()" (open=1, close=1)
|
||||||
|
│ └── "()(" (open=2, close=1)
|
||||||
|
│ └── "()()" (open=2, close=2) ✓
|
||||||
|
└── ")" (open=0, close=0)
|
||||||
|
✗ 剪枝:close 不能 > open
|
||||||
|
|
||||||
|
只生成了 2 个有效组合,而不是 2^4 = 16 个!
|
||||||
|
```
|
||||||
|
|
||||||
## 解题思路
|
## 解题思路
|
||||||
|
|
||||||
### 方法一:回溯法(推荐)
|
### 方法一:回溯法(推荐)
|
||||||
|
|
||||||
**核心思想:**使用回溯法生成所有可能的括号组合。在生成过程中,始终保持括号的有序性:
|
**核心思想:**使用回溯法生成所有可能的括号组合。在生成过程中,始终保持括号的有序性。
|
||||||
1. 左括号数量不能超过 n
|
|
||||||
2. 右括号数量不能超过左括号数量
|
|
||||||
|
|
||||||
**算法步骤:**
|
### 详细算法流程
|
||||||
1. 初始化结果数组 `result` 和当前字符串 `current`
|
|
||||||
2. 定义回溯函数 `backtrack(open, close)`:
|
|
||||||
- `open`:已使用的左括号数量
|
|
||||||
- `close`:已使用的右括号数量
|
|
||||||
3. 终止条件:`len(current) == 2 * n`,将 `current` 加入 `result`
|
|
||||||
4. 选择条件:
|
|
||||||
- 如果 `open < n`,可以添加左括号
|
|
||||||
- 如果 `close < open`,可以添加右括号
|
|
||||||
5. 递归调用后撤销选择(回溯)
|
|
||||||
|
|
||||||
**为什么这样做?**
|
**步骤1:理解有效括号的生成规则**
|
||||||
- 通过限制 `close < open`,保证任何时候右括号数量不超过左括号数量
|
|
||||||
- 通过限制 `open < n`,保证左括号数量不超过 n
|
|
||||||
- 这样生成的所有组合都是有效的
|
|
||||||
|
|
||||||
### 方法二:DFS 深度优先搜索
|
```python
|
||||||
|
# 规则1:左括号数量不能超过 n
|
||||||
|
if open < n:
|
||||||
|
可以添加 '('
|
||||||
|
|
||||||
**核心思想:**与回溯法类似,但使用更纯粹的 DFS 思想。将问题看作在二叉树中搜索。
|
# 规则2:右括号数量不能超过左括号数量
|
||||||
|
if close < open:
|
||||||
|
可以添加 ')'
|
||||||
|
```
|
||||||
|
|
||||||
**算法步骤:**
|
**Q: 为什么右括号数量不能超过左括号数量?**
|
||||||
1. 构建一个递归树,每个节点代表一个状态
|
|
||||||
2. 从根节点开始,每次可以选择添加左括号或右括号
|
|
||||||
3. 剪枝:不符合条件的分支直接跳过
|
|
||||||
4. 到达叶子节点(长度为 2n)时,记录结果
|
|
||||||
|
|
||||||
### 方法三:动态规划
|
A: 因为在任何前缀中,如果右括号多于左括号,就不可能通过后续添加括号使其变成有效括号。
|
||||||
|
|
||||||
**核心思想:**利用卡特兰数(Catalan Number)的性质。n 对括号的有效组合数等于第 n 个卡特兰数。
|
举例:
|
||||||
|
```
|
||||||
|
")" → 不可能变有效,因为第一个字符就是右括号
|
||||||
|
"())(" → 中间的 "()" 后面是 ")",右括号已经多余了
|
||||||
|
```
|
||||||
|
|
||||||
|
**步骤2:设计回溯函数**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def backtrack(current, open, close):
|
||||||
|
# 终止条件:生成了足够的括号
|
||||||
|
if len(current) == 2 * n:
|
||||||
|
result.append(current)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 选择1:添加左括号(如果可以)
|
||||||
|
if open < n:
|
||||||
|
backtrack(current + "(", open + 1, close)
|
||||||
|
|
||||||
|
# 选择2:添加右括号(如果可以)
|
||||||
|
if close < open:
|
||||||
|
backtrack(current + ")", open, close + 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Q: 为什么不需要显式地撤销选择(回溯)?**
|
||||||
|
|
||||||
|
A: 因为我们使用的是字符串拼接 `current + "("`,这会创建新的字符串,不会修改原来的 `current`。每次递归调用都是独立的,不需要手动撤销。
|
||||||
|
|
||||||
|
如果要使用列表优化性能:
|
||||||
|
```python
|
||||||
|
def backtrack(current, open, close):
|
||||||
|
if len(current) == 2 * n:
|
||||||
|
result.append("".join(current))
|
||||||
|
return
|
||||||
|
|
||||||
|
if open < n:
|
||||||
|
current.append("(") # 做选择
|
||||||
|
backtrack(current, open + 1, close)
|
||||||
|
current.pop() # 撤销选择
|
||||||
|
|
||||||
|
if close < open:
|
||||||
|
current.append(")") # 做选择
|
||||||
|
backtrack(current, open, close + 1)
|
||||||
|
current.pop() # 撤销选择
|
||||||
|
```
|
||||||
|
|
||||||
|
**步骤3:初始化并启动回溯**
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = []
|
||||||
|
backtrack("", 0, 0)
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
**Q: 为什么初始状态是 open=0, close=0?**
|
||||||
|
|
||||||
|
A: 因为我们从空字符串开始,还没有添加任何括号。
|
||||||
|
|
||||||
|
### 关键细节说明
|
||||||
|
|
||||||
|
**细节1:为什么终止条件是 `len(current) == 2 * n`?**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 错误理解:应该是 len(current) == n
|
||||||
|
# 错误原因:n 是括号的对数,每对有 2 个括号
|
||||||
|
|
||||||
|
# 正确理解:总长度是 2n
|
||||||
|
# 例如 n=3,最终字符串长度是 6:"((()))"
|
||||||
|
```
|
||||||
|
|
||||||
|
**细节2:为什么两个 if 是独立的,而不是 if-else?**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 错误写法:if-else
|
||||||
|
if open < n:
|
||||||
|
backtrack(current + "(", open + 1, close)
|
||||||
|
else:
|
||||||
|
backtrack(current + ")", open, close + 1)
|
||||||
|
|
||||||
|
# 问题:这样会导致每步只能添加一种括号
|
||||||
|
# 正确写法:两个独立的 if
|
||||||
|
if open < n:
|
||||||
|
backtrack(current + "(", open + 1, close)
|
||||||
|
if close < open:
|
||||||
|
backtrack(current + ")", open, close + 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
**为什么?**
|
||||||
|
- 因为在满足两个条件的情况下,我们可以选择添加左括号或右括号
|
||||||
|
- 这是两种不同的选择,需要分别探索
|
||||||
|
|
||||||
|
**细节3:为什么判断条件是 `close < open` 而不是 `close <= open`?**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 错误写法
|
||||||
|
if close <= open: # ❌
|
||||||
|
backtrack(current + ")", open, close + 1)
|
||||||
|
|
||||||
|
# 问题:当 close == open 时,不能添加右括号
|
||||||
|
# 例如:current="()", open=1, close=1
|
||||||
|
# 如果再添加 ")",变成 "()()",这是错误的
|
||||||
|
|
||||||
|
# 正确写法
|
||||||
|
if close < open: # ✓
|
||||||
|
backtrack(current + ")", open, close + 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 边界条件分析
|
||||||
|
|
||||||
|
**边界1:n = 1**
|
||||||
|
```
|
||||||
|
输入:n = 1
|
||||||
|
输出:["()"]
|
||||||
|
过程:
|
||||||
|
"" → "(" → "()"
|
||||||
|
终止:len("()") = 2 = 2*1
|
||||||
|
```
|
||||||
|
|
||||||
|
**边界2:n = 8(最大值)**
|
||||||
|
```
|
||||||
|
输入:n = 8
|
||||||
|
输出:1430 个组合(卡特兰数 C(8))
|
||||||
|
注意:虽然约束是 n <= 8,但算法可以处理更大的 n
|
||||||
|
```
|
||||||
|
|
||||||
|
**边界3:某个时刻右括号用完了**
|
||||||
|
```
|
||||||
|
状态:current="(()(", open=3, close=1
|
||||||
|
分析:
|
||||||
|
- 不能添加 ")":因为 close(1) < open(3),可以添加
|
||||||
|
- 添加后:current="(()()", open=3, close=2
|
||||||
|
- 继续添加 ")":current="(()())", open=3, close=3 ✓
|
||||||
|
```
|
||||||
|
|
||||||
|
### 复杂度分析(详细版)
|
||||||
|
|
||||||
|
**时间复杂度:**
|
||||||
|
```
|
||||||
|
- 理论最坏情况:每个节点有 2 个分支,深度为 2n → O(2^(2n))
|
||||||
|
- 实际情况:由于剪枝,只有卡特兰数 C(n) 个有效节点
|
||||||
|
- 卡特兰数公式:C(n) = (2n)! / ((n+1)! × n!)
|
||||||
|
- 渐近复杂度:C(n) ≈ 4^n / (n^(3/2) × √π)
|
||||||
|
- **时间复杂度:O(4^n / √n)**
|
||||||
|
|
||||||
|
为什么是 4^n 而不是 2^(2n)?
|
||||||
|
- 数学上 2^(2n) = 4^n
|
||||||
|
- 但由于剪枝,实际是 O(4^n / √n),比 O(4^n) 小得多
|
||||||
|
```
|
||||||
|
|
||||||
|
**空间复杂度:**
|
||||||
|
```
|
||||||
|
- 递归栈深度:最多 2n 层(每添加一个括号递归一次)- O(n)
|
||||||
|
- 存储结果:O(C(n))(卡特兰数)- 通常不计入空间复杂度
|
||||||
|
- **空间复杂度:O(n)**(不计结果存储)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法二:动态规划
|
||||||
|
|
||||||
|
**核心思想:**利用卡特兰数的递推关系。
|
||||||
|
|
||||||
**递推公式:**
|
**递推公式:**
|
||||||
- `dp[n]` 表示 n 对括号的所有有效组合
|
```
|
||||||
- `dp[n] = "(" + dp[i] + ")" + dp[n-1-i]`,其中 `i` 从 0 到 n-1
|
dp[n] = "(" + dp[i] + ")" + dp[n-1-i]
|
||||||
|
|
||||||
**算法步骤:**
|
其中 i 从 0 到 n-1
|
||||||
1. 初始化 `dp[0] = [""]`
|
|
||||||
2. 对于 `i` 从 1 到 n:
|
解释:
|
||||||
- 对于 `j` 从 0 到 i-1:
|
- dp[n] 表示 n 对括号的所有有效组合
|
||||||
- 将 `dp[j]` 的每个组合加上一对括号,再拼接 `dp[i-1-j]` 的每个组合
|
- 每个组合可以看作:一对括号包裹着 i 对括号,后面跟着 n-1-i 对括号
|
||||||
3. 返回 `dp[n]`
|
```
|
||||||
|
|
||||||
|
**Q: 为什么这样递推?**
|
||||||
|
|
||||||
|
A: 任何有效的 n 对括号组合,都可以分解为:
|
||||||
|
1. 第一个左括号
|
||||||
|
2. 一个与之匹配的右括号
|
||||||
|
3. 中间有 i 对括号
|
||||||
|
4. 后面有 n-1-i 对括号
|
||||||
|
|
||||||
|
举例:
|
||||||
|
```
|
||||||
|
"(()())" 可以分解为:
|
||||||
|
( + ()() + )
|
||||||
|
↑ ↑ ↑
|
||||||
|
| | 匹配的右括号
|
||||||
|
| 中间的 i=2 对括号
|
||||||
|
第一个左括号
|
||||||
|
|
||||||
|
所以:"(())()" = "(" + "()" + ")" + "()"
|
||||||
|
i=1, n-1-i=1
|
||||||
|
```
|
||||||
|
|
||||||
## 代码实现
|
## 代码实现
|
||||||
|
|
||||||
@@ -137,6 +436,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Go 实现(动态规划)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func generateParenthesisDP(n int) []string {
|
func generateParenthesisDP(n int) []string {
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
@@ -161,23 +462,162 @@ func generateParenthesisDP(n int) []string {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **时间复杂度:** O(4^n / √n)
|
## 执行过程演示
|
||||||
- 在回溯树中,每个节点最多有 2 个分支
|
|
||||||
- 树的高度为 2n
|
|
||||||
- 但是由于剪枝,实际复杂度约为卡特兰数 C(n)
|
|
||||||
- 卡特兰数约为 O(4^n / (n^(3/2) * √π))
|
|
||||||
|
|
||||||
- **空间复杂度:** O(n)
|
以 `n = 3` 为例:
|
||||||
- 递归栈深度最大为 2n
|
|
||||||
- 存储结果的空间不算在内(这是必须的)
|
|
||||||
|
|
||||||
### 动态规划
|
```
|
||||||
|
初始状态: current="", open=0, close=0
|
||||||
|
|
||||||
- **时间复杂度:** O(4^n / √n)
|
第1层递归:
|
||||||
- 与回溯法类似,需要生成所有有效组合
|
current="" → 可以添加 "(" (open=0 < 3)
|
||||||
|
current="" → 不能添加 ")" (close=0 不 < open=0)
|
||||||
|
|
||||||
- **空间复杂度:** O(4^n / √n)
|
路径:backtrack("(", 1, 0)
|
||||||
- 需要存储中间结果和最终结果
|
|
||||||
|
第2层递归:
|
||||||
|
current="(" → 可以添加 "(" (open=1 < 3)
|
||||||
|
current="(" → 可以添加 ")" (close=0 < open=1)
|
||||||
|
|
||||||
|
路径1:backtrack("((", 2, 0)
|
||||||
|
路径2:backtrack("()", 1, 1)
|
||||||
|
|
||||||
|
第3层递归(路径1):
|
||||||
|
current="((" → 可以添加 "(" (open=2 < 3)
|
||||||
|
current="((" → 不能添加 ")" (close=0 < open=2) ✓
|
||||||
|
|
||||||
|
路径1.1:backtrack("(((", 3, 0)
|
||||||
|
路径1.2:backtrack("(()", 2, 1)
|
||||||
|
|
||||||
|
第4层递归(路径1.1):
|
||||||
|
current="(((" → 不能添加 "(" (open=3 不 < 3)
|
||||||
|
current="(((" → 不能添加 ")" (close=0 < open=3) ✓
|
||||||
|
|
||||||
|
路径1.1.1:backtrack("((()", 3, 1)
|
||||||
|
|
||||||
|
第5层递归(路径1.1.1):
|
||||||
|
current="((()" → 不能添加 "(" (open=3 不 < 3)
|
||||||
|
current="((()" → 不能添加 ")" (close=1 < open=3) ✓
|
||||||
|
|
||||||
|
路径1.1.1.1:backtrack("((())", 3, 2)
|
||||||
|
|
||||||
|
第6层递归(路径1.1.1.1):
|
||||||
|
current="((())" → 不能添加 "(" (open=3 不 < 3)
|
||||||
|
current="((())" → 不能添加 ")" (close=2 < open=3) ✓
|
||||||
|
|
||||||
|
路径1.1.1.1.1:backtrack("((()))", 3, 3)
|
||||||
|
|
||||||
|
第7层递归(路径1.1.1.1.1):
|
||||||
|
current="((()))" → len=6=2*3,终止!
|
||||||
|
添加到结果:["((()))"]
|
||||||
|
|
||||||
|
...(继续其他路径)
|
||||||
|
|
||||||
|
最终结果:
|
||||||
|
["((()))","(()())","(())()","()(())","()()()"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见错误
|
||||||
|
|
||||||
|
### 错误1:忘记剪枝条件
|
||||||
|
|
||||||
|
❌ **错误写法:**
|
||||||
|
```go
|
||||||
|
func generateParenthesisWrong(n int) []string {
|
||||||
|
result := []string{}
|
||||||
|
|
||||||
|
var backtrack func(current string, length int)
|
||||||
|
backtrack = func(current string, length int) {
|
||||||
|
if length == 2*n {
|
||||||
|
result = append(result, current)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有剪枝条件!
|
||||||
|
backtrack(current+"(", length+1)
|
||||||
|
backtrack(current+")", length+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
backtrack("", 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **正确写法:**
|
||||||
|
```go
|
||||||
|
func generateParenthesis(n int) []string {
|
||||||
|
result := []string{}
|
||||||
|
current := []byte{}
|
||||||
|
|
||||||
|
var backtrack func(open, close int)
|
||||||
|
backtrack = func(open, close int) {
|
||||||
|
if len(current) == 2*n {
|
||||||
|
result = append(result, string(current))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if open < n { // 剪枝条件1
|
||||||
|
current = append(current, '(')
|
||||||
|
backtrack(open+1, close)
|
||||||
|
current = current[:len(current)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if close < open { // 剪枝条件2
|
||||||
|
current = append(current, ')')
|
||||||
|
backtrack(open, close+1)
|
||||||
|
current = current[:len(current)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backtrack(0, 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**原因:**没有剪枝会生成大量无效组合,时间复杂度爆炸。
|
||||||
|
|
||||||
|
### 错误2:使用 if-else 而不是独立的 if
|
||||||
|
|
||||||
|
❌ **错误写法:**
|
||||||
|
```go
|
||||||
|
if open < n {
|
||||||
|
backtrack(open+1, close)
|
||||||
|
} else if close < open {
|
||||||
|
backtrack(open, close+1)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **正确写法:**
|
||||||
|
```go
|
||||||
|
if open < n {
|
||||||
|
backtrack(open+1, close)
|
||||||
|
}
|
||||||
|
if close < open {
|
||||||
|
backtrack(open, close+1)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**原因:**两个条件可能同时满足,需要都尝试。
|
||||||
|
|
||||||
|
### 错误3:终止条件错误
|
||||||
|
|
||||||
|
❌ **错误写法:**
|
||||||
|
```go
|
||||||
|
if open == n && close == n { // 错误
|
||||||
|
result = append(result, string(current))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **正确写法:**
|
||||||
|
```go
|
||||||
|
if len(current) == 2*n { // 正确
|
||||||
|
result = append(result, string(current))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**原因:**虽然 `open==n && close==n` 等价于 `len(current)==2*n`,但前者更冗余。
|
||||||
|
|
||||||
## 进阶问题
|
## 进阶问题
|
||||||
|
|
||||||
|
|||||||
@@ -24,39 +24,335 @@
|
|||||||
- `1 <= s.length <= 1000`
|
- `1 <= s.length <= 1000`
|
||||||
- `s` 仅由数字和英文字母组成
|
- `s` 仅由数字和英文字母组成
|
||||||
|
|
||||||
|
## 思路推导
|
||||||
|
|
||||||
|
### 暴力解法分析
|
||||||
|
|
||||||
|
**第一步:最直观的思路 - 枚举所有子串**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def longestPalindrome_brute(s):
|
||||||
|
n = len(s)
|
||||||
|
max_len = 1
|
||||||
|
start = 0
|
||||||
|
|
||||||
|
# 枚举所有可能的子串
|
||||||
|
for i in range(n):
|
||||||
|
for j in range(i+1, n):
|
||||||
|
# 检查 s[i:j+1] 是否是回文
|
||||||
|
if isPalindrome(s, i, j) and (j - i + 1) > max_len:
|
||||||
|
start = i
|
||||||
|
max_len = j - i + 1
|
||||||
|
|
||||||
|
return s[start:start+max_len]
|
||||||
|
|
||||||
|
def isPalindrome(s, left, right):
|
||||||
|
while left < right:
|
||||||
|
if s[left] != s[right]:
|
||||||
|
return False
|
||||||
|
left += 1
|
||||||
|
right -= 1
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
**时间复杂度分析:**
|
||||||
|
- 枚举所有子串:O(n²)(有 n(n+1)/2 个子串)
|
||||||
|
- 判断每个子串是否回文:O(n)
|
||||||
|
- **总时间复杂度:O(n³)**
|
||||||
|
|
||||||
|
**问题:**
|
||||||
|
- 当 n = 1000 时,n³ = 10⁹ 次操作,会超时
|
||||||
|
- 存在大量重复判断,效率极低
|
||||||
|
|
||||||
|
### 优化思考 - 如何降低复杂度?
|
||||||
|
|
||||||
|
**核心观察:**
|
||||||
|
1. **暴力法的瓶颈**:每次判断回文都要重新遍历子串
|
||||||
|
2. **优化方向**:能否利用已计算的信息,避免重复判断?
|
||||||
|
|
||||||
|
**思路一:动态规划(空间换时间)**
|
||||||
|
|
||||||
|
关键问题:小问题的解能否帮助解决大问题?
|
||||||
|
|
||||||
|
```
|
||||||
|
如果 s[i:j] 是回文,那么:
|
||||||
|
- s[i+1:j-1] 也必须是回文
|
||||||
|
- s[i] == s[j]
|
||||||
|
```
|
||||||
|
|
||||||
|
这就是**最优子结构**!
|
||||||
|
|
||||||
|
**思路二:中心扩展法(逆向思维)**
|
||||||
|
|
||||||
|
与其判断每个子串是否回文,不如:
|
||||||
|
1. 枚举每个可能的"中心"
|
||||||
|
2. 从中心向两边扩展
|
||||||
|
3. 扩展到不能扩展为止
|
||||||
|
|
||||||
|
为什么这样可行?
|
||||||
|
- 回文串是关于中心对称的
|
||||||
|
- 回文串的中心可能是:
|
||||||
|
- 一个字符(奇数长度):如 "aba",中心是 'b'
|
||||||
|
- 两个字符之间(偶数长度):如 "abba",中心在两个 'b' 之间
|
||||||
|
|
||||||
|
**思路三:Manacher 算法(极致优化)**
|
||||||
|
|
||||||
|
中心扩展法的问题:同一个位置可能被访问多次
|
||||||
|
|
||||||
|
Manacher 的核心思想:
|
||||||
|
- 利用回文串的对称性
|
||||||
|
- 如果已经知道一个长回文串,可以快速计算出它内部对称位置的回文半径
|
||||||
|
- **时间复杂度降到 O(n)**
|
||||||
|
|
||||||
|
### 为什么这样思考?
|
||||||
|
|
||||||
|
**1. 降维思想**
|
||||||
|
```
|
||||||
|
暴力法:枚举起点 + 枚举终点 + 判断回文
|
||||||
|
↓
|
||||||
|
优化:枚举中心 + 向两边扩展
|
||||||
|
|
||||||
|
从二维枚举(起点,终点) 降到 一维枚举(中心)
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 对称性利用**
|
||||||
|
```
|
||||||
|
回文串的定义:正读和反读相同
|
||||||
|
|
||||||
|
这意味着:
|
||||||
|
- 如果 s[left:right+1] 是回文
|
||||||
|
- 那么 s[left] == s[right]
|
||||||
|
- 且 s[left+1:right] 也是回文
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. 中心扩展的优势**
|
||||||
|
```
|
||||||
|
暴力法:每个子串都要单独判断
|
||||||
|
中心扩展:一次扩展可以同时判断多个子串
|
||||||
|
|
||||||
|
例如 "babad":
|
||||||
|
以 'b'(index=0) 为中心扩展:
|
||||||
|
- "b" ✓
|
||||||
|
- "aba" ✓
|
||||||
|
- "bab" 不可能(越界)
|
||||||
|
```
|
||||||
|
|
||||||
## 解题思路
|
## 解题思路
|
||||||
|
|
||||||
### 方法一:动态规划(推荐)
|
### 方法一:中心扩展法(推荐)
|
||||||
|
|
||||||
|
**核心思想:**回文串关于中心对称。从每个字符(或两个字符之间)向两边扩展,寻找最长的回文串。
|
||||||
|
|
||||||
|
### 详细算法流程
|
||||||
|
|
||||||
|
**步骤1:遍历所有可能的中心点**
|
||||||
|
|
||||||
|
```python
|
||||||
|
for i in range(len(s)):
|
||||||
|
# 奇数长度:以 s[i] 为中心
|
||||||
|
len1 = expand(s, i, i)
|
||||||
|
|
||||||
|
# 偶数长度:以 s[i] 和 s[i+1] 之间为中心
|
||||||
|
len2 = expand(s, i, i+1)
|
||||||
|
|
||||||
|
# 取较长的
|
||||||
|
max_len = max(max_len, len1, len2)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Q: 为什么要考虑奇数和偶数两种情况?**
|
||||||
|
|
||||||
|
A: 因为回文串长度的奇偶性不同,中心位置也不同:
|
||||||
|
- 奇数长度 "aba":中心是单个字符 'b'
|
||||||
|
- 偶数长度 "abba":中心在两个字符之间
|
||||||
|
|
||||||
|
**步骤2:从中心向两边扩展**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def expand(s, left, right):
|
||||||
|
# 当不越界且字符相等时,继续扩展
|
||||||
|
while left >= 0 and right < len(s) and s[left] == s[right]:
|
||||||
|
left -= 1
|
||||||
|
right += 1
|
||||||
|
|
||||||
|
# 返回回文串长度
|
||||||
|
# 注意:循环结束时,left 和 right 已经不满足条件
|
||||||
|
# 所以回文串是 s[left+1:right],长度为 right - left - 1
|
||||||
|
return right - left - 1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Q: 为什么返回 right - left - 1?**
|
||||||
|
|
||||||
|
A: 因为循环结束时:
|
||||||
|
- left 和 right 指向的位置要么越界,要么字符不等
|
||||||
|
- 实际的回文串是 s[left+1:right]
|
||||||
|
- 长度 = right - (left+1) = right - left - 1
|
||||||
|
|
||||||
|
举例:
|
||||||
|
```
|
||||||
|
s = "babad", 中心 i=1('a')
|
||||||
|
初始: left=1, right=1
|
||||||
|
第1次: left=0('b'), right=2('b'), 相等 ✓
|
||||||
|
第2次: left=-1(越界), right=3('a')
|
||||||
|
终止
|
||||||
|
返回: right - left - 1 = 3 - (-1) - 1 = 3
|
||||||
|
回文串: s[0:3] = "bab" ✓
|
||||||
|
```
|
||||||
|
|
||||||
|
**步骤3:更新最大长度和起始位置**
|
||||||
|
|
||||||
|
```python
|
||||||
|
if current_len > max_len:
|
||||||
|
max_len = current_len
|
||||||
|
# 计算起始位置
|
||||||
|
start = i - (current_len - 1) // 2
|
||||||
|
```
|
||||||
|
|
||||||
|
**Q: 起始位置为什么是 i - (current_len - 1) // 2?**
|
||||||
|
|
||||||
|
A: 需要根据奇偶性分别计算:
|
||||||
|
- 奇数长度:中心在字符上,起始 = i - len//2
|
||||||
|
- 偶数长度:中心在两个字符间,起始 = i - len//2 + 1
|
||||||
|
|
||||||
|
统一公式:`start = i - (len-1)//2`
|
||||||
|
|
||||||
|
举例:
|
||||||
|
```
|
||||||
|
奇数:i=1, len=3 ("bab"的中心)
|
||||||
|
start = 1 - (3-1)//2 = 1 - 1 = 0 ✓
|
||||||
|
|
||||||
|
偶数:i=1, len=2 ("bb"的中心在两个字符间)
|
||||||
|
start = 1 - (2-1)//2 = 1 - 0 = 1 ✓
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键细节说明
|
||||||
|
|
||||||
|
**细节1:为什么需要两次扩展(奇数和偶数)?**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 错误做法:只考虑奇数长度
|
||||||
|
for i in range(len(s)):
|
||||||
|
max_len = max(max_len, expand(s, i, i))
|
||||||
|
|
||||||
|
# 对于 "abba",会漏掉 "bb" 和 "abba"
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 正确做法:奇偶都要考虑
|
||||||
|
for i in range(len(s)):
|
||||||
|
len1 = expand(s, i, i) # 奇数长度
|
||||||
|
len2 = expand(s, i, i+1) # 偶数长度
|
||||||
|
max_len = max(max_len, len1, len2)
|
||||||
|
```
|
||||||
|
|
||||||
|
**细节2:为什么边界条件是 left >= 0 而不是 left > 0?**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 错误写法
|
||||||
|
while left > 0 and right < len(s)-1 and s[left] == s[right]:
|
||||||
|
left -= 1
|
||||||
|
right += 1
|
||||||
|
|
||||||
|
# 问题:会漏掉边界上的回文串
|
||||||
|
# 例如 "aba" 的中心在 index=1
|
||||||
|
# 如果 left > 0,就无法扩展到 index=0
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 正确写法
|
||||||
|
while left >= 0 and right < len(s) and s[left] == s[right]:
|
||||||
|
left -= 1
|
||||||
|
right += 1
|
||||||
|
```
|
||||||
|
|
||||||
|
**细节3:如何处理单个字符的情况?**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def longestPalindrome(s):
|
||||||
|
if len(s) < 2:
|
||||||
|
return s # 直接返回,长度为 0 或 1 都是回文
|
||||||
|
# ... 后续逻辑
|
||||||
|
```
|
||||||
|
|
||||||
|
### 边界条件分析
|
||||||
|
|
||||||
|
**边界1:空字符串或单字符**
|
||||||
|
```
|
||||||
|
输入: s = ""
|
||||||
|
输出: ""
|
||||||
|
原因: 空字符串也是回文串
|
||||||
|
|
||||||
|
输入: s = "a"
|
||||||
|
输出: "a"
|
||||||
|
原因: 单字符本身就是回文串
|
||||||
|
```
|
||||||
|
|
||||||
|
**边界2:全部相同字符**
|
||||||
|
```
|
||||||
|
输入: s = "aaaa"
|
||||||
|
输出: "aaaa"
|
||||||
|
处理: 每个位置扩展都能得到长度 4
|
||||||
|
```
|
||||||
|
|
||||||
|
**边界3:无回文串(除单字符外)**
|
||||||
|
```
|
||||||
|
输入: s = "abc"
|
||||||
|
输出: "a" 或 "b" 或 "c"
|
||||||
|
原因: 任意多字符子串都不是回文,返回任意单字符即可
|
||||||
|
```
|
||||||
|
|
||||||
|
**边界4:多个最长回文串**
|
||||||
|
```
|
||||||
|
输入: s = "babad"
|
||||||
|
输出: "bab" 或 "aba"
|
||||||
|
原因: 题目允许返回任意一个最长回文串
|
||||||
|
```
|
||||||
|
|
||||||
|
### 复杂度分析(详细版)
|
||||||
|
|
||||||
|
**时间复杂度:**
|
||||||
|
```
|
||||||
|
- 外层循环:遍历 n 个字符 - O(n)
|
||||||
|
- 内层扩展:每个中心最多扩展 O(n) 次
|
||||||
|
- 总计:O(n) × O(n) = O(n²)
|
||||||
|
|
||||||
|
为什么不是 O(n³)?
|
||||||
|
- 暴力法:枚举起点 O(n) × 枚举终点 O(n) × 判断回文 O(n) = O(n³)
|
||||||
|
- 中心扩展:枚举中心 O(n) × 扩展 O(n) = O(n²)
|
||||||
|
- 关键:扩展是"一次判断,逐层扩展",而不是对每个子串重新判断
|
||||||
|
```
|
||||||
|
|
||||||
|
**空间复杂度:**
|
||||||
|
```
|
||||||
|
- 只使用了常数级别的变量:O(1)
|
||||||
|
- 不需要额外的数组或哈希表
|
||||||
|
- 递归调用栈:O(1)(虽然看起来像递归,但深度最多 O(n))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法二:动态规划
|
||||||
|
|
||||||
**核心思想:**使用二维数组 `dp[i][j]` 表示 `s[i:j+1]` 是否为回文串。
|
**核心思想:**使用二维数组 `dp[i][j]` 表示 `s[i:j+1]` 是否为回文串。
|
||||||
|
|
||||||
**状态转移方程:**
|
**状态转移方程:**
|
||||||
- `dp[i][j] = (s[i] == s[j]) && (j - i < 2 || dp[i+1][j-1])`
|
```
|
||||||
- 如果 `s[i] == s[j]` 且 `dp[i+1][j-1]` 为真(或子串长度小于3),则 `dp[i][j]` 为真
|
dp[i][j] = (s[i] == s[j]) && (j - i < 2 || dp[i+1][j-1])
|
||||||
|
|
||||||
**算法步骤:**
|
解释:
|
||||||
1. 初始化 `dp` 数组,所有单个字符都是回文串
|
1. s[i] == s[j]:首尾字符必须相等
|
||||||
2. 按长度递增的顺序遍历所有子串
|
2. j - i < 2:子串长度小于 3(如 "a", "aa")必然是回文
|
||||||
3. 更新最长回文子串的起始位置和长度
|
3. dp[i+1][j-1]:去掉首尾后的子串也必须是回文
|
||||||
|
```
|
||||||
|
|
||||||
### 方法二:中心扩展法(最优)
|
**Q: 为什么按长度递增的顺序遍历?**
|
||||||
|
|
||||||
**核心思想:**回文串关于中心对称。从每个字符(或两个字符之间)向两边扩展,寻找最长的回文串。
|
A: 因为计算 `dp[i][j]` 需要 `dp[i+1][j-1]`,而 `dp[i+1][j-1]` 对应更短的子串。
|
||||||
|
|
||||||
**算法步骤:**
|
```python
|
||||||
1. 遍历每个字符作为中心点
|
# 按长度从 2 到 n 遍历
|
||||||
2. 从中心点向两边扩展,直到不再是回文串
|
for length in range(2, n + 1):
|
||||||
3. 记录最长的回文串
|
for i in range(n - length + 1):
|
||||||
4. 需要考虑奇数长度和偶数长度两种情况
|
j = i + length - 1
|
||||||
|
# 计算 dp[i][j]
|
||||||
### 方法三:Manacher 算法(最优)
|
```
|
||||||
|
|
||||||
**核心思想:**利用回文串的对称性,避免重复计算。时间复杂度 O(n)。
|
|
||||||
|
|
||||||
**算法步骤:**
|
|
||||||
1. 在字符串中插入特殊字符(如 `#`),统一处理奇偶长度
|
|
||||||
2. 使用数组 `P` 记录以每个字符为中心的最长回文半径
|
|
||||||
3. 利用对称性快速计算回文半径
|
|
||||||
|
|
||||||
## 代码实现
|
## 代码实现
|
||||||
|
|
||||||
@@ -134,6 +430,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Go 实现(动态规划)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func longestPalindromeDP(s string) string {
|
func longestPalindromeDP(s string) string {
|
||||||
if len(s) < 2 {
|
if len(s) < 2 {
|
||||||
@@ -174,89 +472,150 @@ func longestPalindromeDP(s string) string {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 执行过程演示
|
||||||
|
|
||||||
|
以 `s = "babad"` 为例:
|
||||||
|
|
||||||
|
```
|
||||||
|
初始状态: start=0, maxLen=1
|
||||||
|
|
||||||
|
i=0, 字符='b':
|
||||||
|
奇数扩展: expand(s, 0, 0)
|
||||||
|
left=0, right=0: 'b'=='b' ✓
|
||||||
|
left=-1, right=1: 越界,停止
|
||||||
|
返回: 1 - (-1) - 1 = 1
|
||||||
|
|
||||||
|
偶数扩展: expand(s, 0, 1)
|
||||||
|
left=0, right=1: 'b'!='a' ✗
|
||||||
|
返回: 1 - 0 - 1 = 0
|
||||||
|
|
||||||
|
maxLen = max(1, 1, 0) = 1
|
||||||
|
|
||||||
|
i=1, 字符='a':
|
||||||
|
奇数扩展: expand(s, 1, 1)
|
||||||
|
left=1, right=1: 'a'=='a' ✓
|
||||||
|
left=0, right=2: 'b'=='b' ✓
|
||||||
|
left=-1, right=3: 越界,停止
|
||||||
|
返回: 3 - (-1) - 1 = 3
|
||||||
|
|
||||||
|
偶数扩展: expand(s, 1, 2)
|
||||||
|
left=1, right=2: 'a'!='b' ✗
|
||||||
|
返回: 2 - 1 - 1 = 0
|
||||||
|
|
||||||
|
更新: maxLen=3, start=1-(3-1)/2=0
|
||||||
|
当前最长: s[0:3]="bab"
|
||||||
|
|
||||||
|
i=2, 字符='b':
|
||||||
|
奇数扩展: expand(s, 2, 2)
|
||||||
|
left=2, right=2: 'b'=='b' ✓
|
||||||
|
left=1, right=3: 'a'=='a' ✓
|
||||||
|
left=0, right=4: 'b'!='d' ✗
|
||||||
|
返回: 4 - 0 - 1 = 3
|
||||||
|
|
||||||
|
偶数扩展: expand(s, 2, 3)
|
||||||
|
left=2, right=3: 'b'!='a' ✗
|
||||||
|
返回: 3 - 2 - 1 = 0
|
||||||
|
|
||||||
|
maxLen = max(3, 3, 0) = 3 (保持不变)
|
||||||
|
|
||||||
|
i=3, 字符='a':
|
||||||
|
奇数扩展: expand(s, 3, 3)
|
||||||
|
left=3, right=3: 'a'=='a' ✓
|
||||||
|
left=2, right=4: 'b'!='d' ✗
|
||||||
|
返回: 4 - 2 - 1 = 1
|
||||||
|
|
||||||
|
偶数扩展: expand(s, 3, 4)
|
||||||
|
left=3, right=4: 'a'!='d' ✗
|
||||||
|
返回: 4 - 3 - 1 = 0
|
||||||
|
|
||||||
|
maxLen = max(3, 1, 0) = 3 (保持不变)
|
||||||
|
|
||||||
|
i=4, 字符='d':
|
||||||
|
奇数扩展: expand(s, 4, 4)
|
||||||
|
left=4, right=4: 'd'=='d' ✓
|
||||||
|
left=3, right=5: 越界,停止
|
||||||
|
返回: 5 - 3 - 1 = 1
|
||||||
|
|
||||||
|
偶数扩展: expand(s, 4, 5)
|
||||||
|
right=5 越界,直接返回 0
|
||||||
|
|
||||||
|
maxLen = max(3, 1, 0) = 3 (保持不变)
|
||||||
|
|
||||||
|
最终结果: s[0:3] = "bab"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见错误
|
||||||
|
|
||||||
|
### 错误1:只考虑奇数长度回文
|
||||||
|
|
||||||
|
❌ **错误写法:**
|
||||||
```go
|
```go
|
||||||
func longestPalindromeManacher(s string) string {
|
func longestPalindromeWrong(s string) string {
|
||||||
if len(s) < 2 {
|
for i := 0; i < len(s); i++ {
|
||||||
return s
|
len := expand(s, i, i) // 只考虑奇数
|
||||||
}
|
// ...
|
||||||
|
}
|
||||||
// 预处理:插入特殊字符
|
|
||||||
t := "#"
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
t += string(s[i]) + "#"
|
|
||||||
}
|
|
||||||
|
|
||||||
n := len(t)
|
|
||||||
p := make([]int, n)
|
|
||||||
center, right := 0, 0
|
|
||||||
maxCenter, maxLen := 0, 0
|
|
||||||
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
if i < right {
|
|
||||||
mirror := 2*center - i
|
|
||||||
p[i] = min(right-i, p[mirror])
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试扩展
|
|
||||||
for i+p[i]+1 < n && i-p[i]-1 >= 0 && t[i+p[i]+1] == t[i-p[i]-1] {
|
|
||||||
p[i]++
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新中心和右边界
|
|
||||||
if i+p[i] > right {
|
|
||||||
center = i
|
|
||||||
right = i + p[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新最大回文串
|
|
||||||
if p[i] > maxLen {
|
|
||||||
maxLen = p[i]
|
|
||||||
maxCenter = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算原字符串中的起始位置
|
|
||||||
start := (maxCenter - maxLen) / 2
|
|
||||||
return s[start : start+maxLen]
|
|
||||||
}
|
|
||||||
|
|
||||||
func min(a, b int) int {
|
|
||||||
if a < b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 复杂度分析
|
✅ **正确写法:**
|
||||||
|
```go
|
||||||
|
func longestPalindrome(s string) string {
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
len1 := expand(s, i, i) // 奇数
|
||||||
|
len2 := expand(s, i, i+1) // 偶数
|
||||||
|
maxLen = max(maxLen, len1, len2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 中心扩展法
|
**原因:**会漏掉偶数长度的回文串,如 "abba"。
|
||||||
|
|
||||||
- **时间复杂度:** O(n²)
|
### 错误2:边界条件错误
|
||||||
- 外层循环遍历 n 个字符
|
|
||||||
- 内层扩展最多 O(n) 次
|
|
||||||
- 总时间复杂度:O(n²)
|
|
||||||
|
|
||||||
- **空间复杂度:** O(1)
|
❌ **错误写法:**
|
||||||
- 只使用了常数级别的额外空间
|
```go
|
||||||
|
func expand(s string, left, right int) int {
|
||||||
|
for left > 0 && right < len(s)-1 && s[left] == s[right] {
|
||||||
|
left--
|
||||||
|
right++
|
||||||
|
}
|
||||||
|
return right - left - 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 动态规划
|
✅ **正确写法:**
|
||||||
|
```go
|
||||||
|
func expand(s string, left, right int) int {
|
||||||
|
for left >= 0 && right < len(s) && s[left] == s[right] {
|
||||||
|
left--
|
||||||
|
right++
|
||||||
|
}
|
||||||
|
return right - left - 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- **时间复杂度:** O(n²)
|
**原因:**边界字符也是回文串的一部分,需要判断。
|
||||||
- 需要填充 n×n 的 dp 数组
|
|
||||||
- 但由于剪枝,实际复杂度约为 O(n²/2)
|
|
||||||
|
|
||||||
- **空间复杂度:** O(n²)
|
### 错误3:起始位置计算错误
|
||||||
- 需要存储 n×n 的 dp 数组
|
|
||||||
|
|
||||||
### Manacher 算法
|
❌ **错误写法:**
|
||||||
|
```go
|
||||||
|
if currentLen > maxLen {
|
||||||
|
maxLen = currentLen
|
||||||
|
start = i - currentLen/2 // 错误!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- **时间复杂度:** O(n)
|
✅ **正确写法:**
|
||||||
- 只需遍历字符串一次
|
```go
|
||||||
- 利用对称性避免重复计算
|
if currentLen > maxLen {
|
||||||
|
maxLen = currentLen
|
||||||
|
start = i - (currentLen-1)/2 // 正确
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- **空间复杂度:** O(n)
|
**原因:**需要考虑奇偶性,统一公式是 `i - (len-1)/2`。
|
||||||
- 需要存储处理后的字符串和半径数组
|
|
||||||
|
|
||||||
## 进阶问题
|
## 进阶问题
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user