批量生成 19 道 LeetCode Hot 100 Medium 难度题目,每道题包含: - 题目描述和示例 - 多种解题思路(回溯、DP、双指针等) - Go 和 Java 双语解答 - 完整的测试用例 - 复杂度分析 - 进阶问题 - P7 加分项(深度理解、实战扩展、变形题目) 新增题目: 1. 盛最多水的容器 (Container With Most Water) - LeetCode 11 2. 电话号码的字母组合 (Letter Combinations) - LeetCode 17 3. 删除链表的倒数第N个结点 - LeetCode 19 4. 括号生成 - LeetCode 22 5. 最长回文子串 - LeetCode 5 6. 子集 - LeetCode 78 7. 单词搜索 - LeetCode 79 8. 柱状图中最大的矩形 - LeetCode 84 9. 最大正方形 - LeetCode 221 10. 完全平方数 - LeetCode 279 11. 最长连续序列 - LeetCode 128 12. 除自身以外数组的乘积 - LeetCode 238 13. 最小栈 - LeetCode 155 14. 二叉树的中序遍历 - LeetCode 94 15. 二叉树的最大深度 - LeetCode 104 16. 翻转二叉树 - LeetCode 226 17. 对称二叉树 - LeetCode 101 18. 路径总和 - LeetCode 112 19. 从前序与中序遍历序列构造二叉树 - LeetCode 105 所有代码均包含: - 清晰的注释说明 - 完整的可运行测试用例 - 时间和空间复杂度分析 - 优化技巧和变形题目 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
14 KiB
14 KiB
最长回文子串 (Longest Palindromic Substring)
题目描述
给你一个字符串 s,找到 s 中最长的回文子串。
示例
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
约束条件
1 <= s.length <= 1000s仅由数字和英文字母组成
解题思路
方法一:动态规划(推荐)
**核心思想:**使用二维数组 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]为真
算法步骤:
- 初始化
dp数组,所有单个字符都是回文串 - 按长度递增的顺序遍历所有子串
- 更新最长回文子串的起始位置和长度
方法二:中心扩展法(最优)
**核心思想:**回文串关于中心对称。从每个字符(或两个字符之间)向两边扩展,寻找最长的回文串。
算法步骤:
- 遍历每个字符作为中心点
- 从中心点向两边扩展,直到不再是回文串
- 记录最长的回文串
- 需要考虑奇数长度和偶数长度两种情况
方法三:Manacher 算法(最优)
**核心思想:**利用回文串的对称性,避免重复计算。时间复杂度 O(n)。
算法步骤:
- 在字符串中插入特殊字符(如
#),统一处理奇偶长度 - 使用数组
P记录以每个字符为中心的最长回文半径 - 利用对称性快速计算回文半径
代码实现
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))
}
Java 实现(中心扩展法)
public class LongestPalindromicSubstring {
public String longestPalindrome(String s) {
if (s == null || s.length() < 2) {
return s;
}
int start = 0, maxLen = 1;
for (int i = 0; i < s.length(); i++) {
// 奇数长度:以当前字符为中心
int len1 = expandAroundCenter(s, i, i);
// 偶数长度:以当前字符和下一个字符之间为中心
int len2 = expandAroundCenter(s, i, i + 1);
int currentLen = Math.max(len1, len2);
if (currentLen > maxLen) {
maxLen = currentLen;
start = i - (currentLen - 1) / 2;
}
}
return s.substring(start, start + maxLen);
}
private int expandAroundCenter(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
return right - left - 1;
}
// 测试用例
public static void main(String[] args) {
LongestPalindromicSubstring solution = new LongestPalindromicSubstring();
// 测试用例1
String s1 = "babad";
System.out.println("输入: " + s1);
System.out.println("输出: " + solution.longestPalindrome(s1));
// 测试用例2
String s2 = "cbbd";
System.out.println("\n输入: " + s2);
System.out.println("输出: " + solution.longestPalindrome(s2));
// 测试用例3: 单个字符
String s3 = "a";
System.out.println("\n输入: " + s3);
System.out.println("输出: " + solution.longestPalindrome(s3));
// 测试用例4: 全部相同
String s4 = "aaaa";
System.out.println("\n输入: " + s4);
System.out.println("输出: " + solution.longestPalindrome(s4));
// 测试用例5: 无回文
String s5 = "abc";
System.out.println("\n输入: " + s5);
System.out.println("输出: " + solution.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]
}
Java 实现(动态规划)
public String longestPalindromeDP(String s) {
if (s == null || s.length() < 2) {
return s;
}
int n = s.length();
boolean[][] dp = new boolean[n][n];
int start = 0, maxLen = 1;
// 初始化:所有单个字符都是回文串
for (int i = 0; i < n; i++) {
dp[i][i] = true;
}
// 按长度递增的顺序遍历
for (int length = 2; length <= n; length++) {
for (int i = 0; i <= n - length; i++) {
int j = i + length - 1;
if (s.charAt(i) == s.charAt(j)) {
if (length == 2 || dp[i + 1][j - 1]) {
dp[i][j] = true;
if (length > maxLen) {
maxLen = length;
start = i;
}
}
}
}
}
return s.substring(start, start + maxLen);
}
Go 实现(Manacher 算法)
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: 修改中心扩展法,找到每个回文子串都记录下来。
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: 统计每个字符出现的次数,最多只能有一个字符出现奇数次。
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] 的最长回文子序列长度。
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,你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串。
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: 给定一个字符串,计算这个字符串中有多少个回文子串。
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 所有可能的分割方案。
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:提前终止
如果找到的回文串已经接近最大可能长度,可以提前终止。
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. 面试技巧
面试官可能会问:
- "为什么中心扩展法比动态规划更好?"
- "Manacher 算法的核心思想是什么?"
- "如何处理 unicode 字符?"
回答要点:
- 中心扩展法空间复杂度 O(1),动态规划需要 O(n²)
- Manacher 算法利用回文串的对称性,避免重复计算
- 使用 rune 类型处理 unicode 字符
7. 相关题目推荐
- LeetCode 5: 最长回文子串(本题)
- LeetCode 125: 验证回文串
- LeetCode 131: 分割回文串
- LeetCode 214: 最短回文串
- LeetCode 516: 最长回文子序列
- LeetCode 647: 回文子串个数