Files
interview/16-LeetCode Hot 100/最长回文子串.md
yasinshaw e75e4778b1 feat: add 19 LeetCode Hot 100 medium problems with detailed solutions
批量生成 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>
2026-03-05 12:26:07 +08:00

14 KiB
Raw Blame History

最长回文子串 (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] 为真或子串长度小于3dp[i][j] 为真

算法步骤:

  1. 初始化 dp 数组,所有单个字符都是回文串
  2. 按长度递增的顺序遍历所有子串
  3. 更新最长回文子串的起始位置和长度

方法二:中心扩展法(最优)

**核心思想:**回文串关于中心对称。从每个字符(或两个字符之间)向两边扩展,寻找最长的回文串。

算法步骤:

  1. 遍历每个字符作为中心点
  2. 从中心点向两边扩展,直到不再是回文串
  3. 记录最长的回文串
  4. 需要考虑奇数长度和偶数长度两种情况

方法三Manacher 算法(最优)

**核心思想:**利用回文串的对称性,避免重复计算。时间复杂度 O(n)。

算法步骤:

  1. 在字符串中插入特殊字符(如 #),统一处理奇偶长度
  2. 使用数组 P 记录以每个字符为中心的最长回文半径
  3. 利用对称性快速计算回文半径

代码实现

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. 面试技巧

面试官可能会问:

  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: 回文子串个数