- 添加思路推导部分,从暴力解法分析优化过程 - 增加详细的算法流程和Q&A形式的解释 - 添加执行过程演示和常见错误分析 - 完善边界条件和复杂度分析 - 保持原有的代码实现和进阶问题
894 lines
20 KiB
Markdown
894 lines
20 KiB
Markdown
# 最长回文子串 (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: 回文子串个数
|