Changes: - Removed all Java code implementations - Kept only Go language solutions - Renamed "## Go 解法" to "## 解法" - Removed "### Go 代码要点" sections - Cleaned up duplicate headers and empty sections - Streamlined documentation for better readability Updated files (9): - 三数之和.md - 两数相加.md - 无重复字符的最长子串.md - 最长回文子串.md - 括号生成.md - 子集.md - 单词搜索.md - 电话号码的字母组合.md - 柱状图中最大的矩形.md All 22 LeetCode Hot 100 Medium problems now use Go exclusively. Code is cleaner, more focused, and easier to follow. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
535 lines
12 KiB
Markdown
535 lines
12 KiB
Markdown
# 最长回文子串 (Longest Palindromic Substring)
|
||
|
||
## 题目描述
|
||
|
||
给你一个字符串 `s`,找到 `s` 中最长的回文子串。
|
||
|
||
### 示例
|
||
|
||
**示例 1:**
|
||
```
|
||
输入:s = "babad"
|
||
输出:"bab"
|
||
解释:"aba" 同样是符合题意的答案。
|
||
```
|
||
|
||
**示例 2:**
|
||
```
|
||
输入:s = "cbbd"
|
||
输出:"bb"
|
||
```
|
||
|
||
### 约束条件
|
||
|
||
- `1 <= s.length <= 1000`
|
||
- `s` 仅由数字和英文字母组成
|
||
|
||
## 解题思路
|
||
|
||
### 方法一:动态规划(推荐)
|
||
|
||
**核心思想:**使用二维数组 `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]` 为真
|
||
|
||
**算法步骤:**
|
||
1. 初始化 `dp` 数组,所有单个字符都是回文串
|
||
2. 按长度递增的顺序遍历所有子串
|
||
3. 更新最长回文子串的起始位置和长度
|
||
|
||
### 方法二:中心扩展法(最优)
|
||
|
||
**核心思想:**回文串关于中心对称。从每个字符(或两个字符之间)向两边扩展,寻找最长的回文串。
|
||
|
||
**算法步骤:**
|
||
1. 遍历每个字符作为中心点
|
||
2. 从中心点向两边扩展,直到不再是回文串
|
||
3. 记录最长的回文串
|
||
4. 需要考虑奇数长度和偶数长度两种情况
|
||
|
||
### 方法三:Manacher 算法(最优)
|
||
|
||
**核心思想:**利用回文串的对称性,避免重复计算。时间复杂度 O(n)。
|
||
|
||
**算法步骤:**
|
||
1. 在字符串中插入特殊字符(如 `#`),统一处理奇偶长度
|
||
2. 使用数组 `P` 记录以每个字符为中心的最长回文半径
|
||
3. 利用对称性快速计算回文半径
|
||
|
||
## 代码实现
|
||
|
||
### 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
|
||
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]
|
||
}
|
||
```
|
||
|
||
```go
|
||
func longestPalindromeManacher(s string) string {
|
||
if len(s) < 2 {
|
||
return s
|
||
}
|
||
|
||
// 预处理:插入特殊字符
|
||
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
|
||
}
|
||
```
|
||
|
||
## 复杂度分析
|
||
|
||
### 中心扩展法
|
||
|
||
- **时间复杂度:** O(n²)
|
||
- 外层循环遍历 n 个字符
|
||
- 内层扩展最多 O(n) 次
|
||
- 总时间复杂度:O(n²)
|
||
|
||
- **空间复杂度:** O(1)
|
||
- 只使用了常数级别的额外空间
|
||
|
||
### 动态规划
|
||
|
||
- **时间复杂度:** O(n²)
|
||
- 需要填充 n×n 的 dp 数组
|
||
- 但由于剪枝,实际复杂度约为 O(n²/2)
|
||
|
||
- **空间复杂度:** O(n²)
|
||
- 需要存储 n×n 的 dp 数组
|
||
|
||
### Manacher 算法
|
||
|
||
- **时间复杂度:** O(n)
|
||
- 只需遍历字符串一次
|
||
- 利用对称性避免重复计算
|
||
|
||
- **空间复杂度:** O(n)
|
||
- 需要存储处理后的字符串和半径数组
|
||
|
||
## 进阶问题
|
||
|
||
### 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: 回文子串个数
|