Files
interview/16-LeetCode Hot 100/最长回文子串.md
yasinshaw 4247e0700d refactor: convert all LeetCode solutions to Go-only
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>
2026-03-05 12:32:55 +08:00

535 lines
12 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` 仅由数字和英文字母组成
## 解题思路
### 方法一:动态规划(推荐)
**核心思想:**使用二维数组 `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: 回文子串个数