# 最长回文子串 (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: 回文子串个数