Files
interview/16-LeetCode Hot 100/最长回文子串.md
yasinshaw a5736a4db7 docs: improve solution explanations for 最长回文子串 and 括号生成
- 添加思路推导部分,从暴力解法分析优化过程
- 增加详细的算法流程和Q&A形式的解释
- 添加执行过程演示和常见错误分析
- 完善边界条件和复杂度分析
- 保持原有的代码实现和进阶问题
2026-03-08 21:32:02 +08:00

894 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 最长回文子串 (Longest Palindromic Substring)
## 题目描述
给你一个字符串 `s`,找到 `s` 中最长的回文子串。
### 示例
**示例 1**
```
输入s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
```
**示例 2**
```
输入s = "cbbd"
输出:"bb"
```
### 约束条件
- `1 <= s.length <= 1000`
- `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] == s[j]) && (j - i < 2 || dp[i+1][j-1])
解释:
1. s[i] == s[j]:首尾字符必须相等
2. j - i < 2子串长度小于 3如 "a", "aa")必然是回文
3. dp[i+1][j-1]:去掉首尾后的子串也必须是回文
```
**Q: 为什么按长度递增的顺序遍历?**
A: 因为计算 `dp[i][j]` 需要 `dp[i+1][j-1]`,而 `dp[i+1][j-1]` 对应更短的子串。
```python
# 按长度从 2 到 n 遍历
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
# 计算 dp[i][j]
```
## 代码实现
### Go 实现(中心扩展法)
```go
package main
import "fmt"
func longestPalindrome(s string) string {
if len(s) < 2 {
return s
}
start, maxLen := 0, 1
for i := 0; i < len(s); i++ {
// 奇数长度:以当前字符为中心
len1 := expandAroundCenter(s, i, i)
// 偶数长度:以当前字符和下一个字符之间为中心
len2 := expandAroundCenter(s, i, i+1)
currentLen := max(len1, len2)
if currentLen > maxLen {
maxLen = currentLen
start = i - (currentLen-1)/2
}
}
return s[start : start+maxLen]
}
func expandAroundCenter(s string, left, right int) int {
for left >= 0 && right < len(s) && s[left] == s[right] {
left--
right++
}
return right - left - 1
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
// 测试用例
func main() {
// 测试用例1
s1 := "babad"
fmt.Printf("输入: %s\n", s1)
fmt.Printf("输出: %s\n", longestPalindrome(s1))
// 测试用例2
s2 := "cbbd"
fmt.Printf("\n输入: %s\n", s2)
fmt.Printf("输出: %s\n", longestPalindrome(s2))
// 测试用例3: 单个字符
s3 := "a"
fmt.Printf("\n输入: %s\n", s3)
fmt.Printf("输出: %s\n", longestPalindrome(s3))
// 测试用例4: 全部相同
s4 := "aaaa"
fmt.Printf("\n输入: %s\n", s4)
fmt.Printf("输出: %s\n", longestPalindrome(s4))
// 测试用例5: 无回文
s5 := "abc"
fmt.Printf("\n输入: %s\n", s5)
fmt.Printf("输出: %s\n", longestPalindrome(s5))
}
```
### Go 实现(动态规划)
```go
func longestPalindromeDP(s string) string {
if len(s) < 2 {
return s
}
n := len(s)
dp := make([][]bool, n)
for i := range dp {
dp[i] = make([]bool, n)
}
start, maxLen := 0, 1
// 初始化:所有单个字符都是回文串
for i := 0; i < n; i++ {
dp[i][i] = true
}
// 按长度递增的顺序遍历
for length := 2; length <= n; length++ {
for i := 0; i <= n-length; i++ {
j := i + length - 1
if s[i] == s[j] {
if length == 2 || dp[i+1][j-1] {
dp[i][j] = true
if length > maxLen {
maxLen = length
start = i
}
}
}
}
}
return s[start : start+maxLen]
}
```
## 执行过程演示
`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
func longestPalindromeWrong(s string) string {
for i := 0; i < len(s); i++ {
len := expand(s, i, i) // 只考虑奇数
// ...
}
}
```
**正确写法:**
```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"。
### 错误2边界条件错误
**错误写法:**
```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
}
```
**原因:**边界字符也是回文串的一部分,需要判断。
### 错误3起始位置计算错误
**错误写法:**
```go
if currentLen > maxLen {
maxLen = currentLen
start = i - currentLen/2 // 错误!
}
```
**正确写法:**
```go
if currentLen > maxLen {
maxLen = currentLen
start = i - (currentLen-1)/2 // 正确
}
```
**原因:**需要考虑奇偶性,统一公式是 `i - (len-1)/2`
## 进阶问题
### Q1: 如何找到所有回文子串?
**A:** 修改中心扩展法,找到每个回文子串都记录下来。
```go
func findAllPalindromes(s string) []string {
result := []string{}
for i := 0; i < len(s); i++ {
// 奇数长度
l, r := i, i
for l >= 0 && r < len(s) && s[l] == s[r] {
result = append(result, s[l:r+1])
l--
r++
}
// 偶数长度
l, r = i, i+1
for l >= 0 && r < len(s) && s[l] == s[r] {
result = append(result, s[l:r+1])
l--
r++
}
}
return result
}
```
### Q2: 如何判断一个字符串是否可以通过重新排列成为回文串?
**A:** 统计每个字符出现的次数,最多只能有一个字符出现奇数次。
```go
func canPermutePalindrome(s string) bool {
count := make(map[rune]int)
for _, c := range s {
count[c]++
}
oddCount := 0
for _, c := range count {
if c%2 == 1 {
oddCount++
}
}
return oddCount <= 1
}
```
### Q3: 最长回文子序列(非连续)如何求解?
**A:** 使用动态规划,`dp[i][j]` 表示 `s[i:j+1]` 的最长回文子序列长度。
```go
func longestPalindromeSubseq(s string) int {
n := len(s)
dp := make([][]int, n)
for i := range dp {
dp[i] = make([]int, n)
dp[i][i] = 1
}
for length := 2; length <= n; length++ {
for i := 0; i <= n-length; i++ {
j := i + length - 1
if s[i] == s[j] {
dp[i][j] = dp[i+1][j-1] + 2
} else {
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
}
}
}
return dp[0][n-1]
}
```
## P7 加分项
### 1. 深度理解:为什么中心扩展法最优?
**对比分析:**
- **暴力法:** O(n³) - 枚举所有子串 O(n²),判断是否回文 O(n)
- **动态规划:** O(n²) 时间O(n²) 空间
- **中心扩展法:** O(n²) 时间O(1) 空间
- **Manacher 算法:** O(n) 时间O(n) 空间
**选择建议:**
- **面试:** 中心扩展法 - 代码简洁,易于实现
- **实际应用:** Manacher 算法 - 性能最优
- **学习:** 都要掌握,理解不同思路
### 2. 实战扩展:回文串相关算法
#### 短回文串构造
**LeetCode 214:** 给定一个字符串 s你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串。
```go
func shortestPalindrome(s string) string {
if len(s) < 2 {
return s
}
// 找到最长的回文前缀
n := len(s)
rev := reverse(s)
combined := s + "#" + rev
// KMP 算法计算最长公共前后缀
pi := make([]int, len(combined))
for i := 1; i < len(combined); i++ {
j := pi[i-1]
for j > 0 && combined[i] != combined[j] {
j = pi[j-1]
}
if combined[i] == combined[j] {
j++
}
pi[i] = j
}
// 添加剩余字符的逆序
add := rev[:n-pi[len(combined)-1]]
return add + s
}
func reverse(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
```
### 3. 变形题目
#### 变形1回文子串个数
**LeetCode 647:** 给定一个字符串,计算这个字符串中有多少个回文子串。
```go
func countSubstrings(s string) int {
count := 0
for i := 0; i < len(s); i++ {
// 奇数长度
count += expandAroundCenterCount(s, i, i)
// 偶数长度
count += expandAroundCenterCount(s, i, i+1)
}
return count
}
func expandAroundCenterCount(s string, left, right int) int {
count := 0
for left >= 0 && right < len(s) && s[left] == s[right] {
count++
left--
right++
}
return count
}
```
#### 变形2分割回文串
**LeetCode 131:** 给定一个字符串 s将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。
```go
func partition(s string) [][]string {
result := [][]string{}
current := []string{}
var backtrack func(start int)
backtrack = func(start int) {
if start == len(s) {
temp := make([]string, len(current))
copy(temp, current)
result = append(result, temp)
return
}
for end := start + 1; end <= len(s); end++ {
if isPalindrome(s[start:end]) {
current = append(current, s[start:end])
backtrack(end)
current = current[:len(current)-1]
}
}
}
backtrack(0)
return result
}
func isPalindrome(s string) bool {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
if s[i] != s[j] {
return false
}
}
return true
}
```
### 4. 优化技巧
#### 优化1提前终止
如果找到的回文串已经接近最大可能长度,可以提前终止。
```go
func longestPalindromeOptimized(s string) string {
if len(s) < 2 {
return s
}
start, maxLen := 0, 1
for i := 0; i < len(s); i++ {
// 提前终止:不可能找到更长的回文串了
if maxLen >= 2*(len(s)-i) {
break
}
len1 := expandAroundCenter(s, i, i)
len2 := expandAroundCenter(s, i, i+1)
currentLen := max(len1, len2)
if currentLen > maxLen {
maxLen = currentLen
start = i - (currentLen-1)/2
}
}
return s[start : start+maxLen]
}
```
### 5. 实际应用场景
- **DNA 序列分析:** 寻找重复序列
- **文本编辑:** 检查拼写和语法
- **数据压缩:** 利用重复模式
- **模式匹配:** 查找对称模式
### 6. 面试技巧
**面试官可能会问:**
1. "为什么中心扩展法比动态规划更好?"
2. "Manacher 算法的核心思想是什么?"
3. "如何处理 unicode 字符?"
**回答要点:**
1. 中心扩展法空间复杂度 O(1),动态规划需要 O(n²)
2. Manacher 算法利用回文串的对称性,避免重复计算
3. 使用 rune 类型处理 unicode 字符
### 7. 相关题目推荐
- LeetCode 5: 最长回文子串(本题)
- LeetCode 125: 验证回文串
- LeetCode 131: 分割回文串
- LeetCode 214: 最短回文串
- LeetCode 516: 最长回文子序列
- LeetCode 647: 回文子串个数