647. 回文子串
❓ 问题简介
给你一个字符串 s,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串是正着读和倒过来读一样的字符串。
子字符串是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例说明
示例 1:
输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"
示例 2:
输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
💡 解题思路
方法一:中心扩展法(推荐)
核心思想:
回文串有两种形式:
- 奇数长度:以单个字符为中心(如 "aba")
- 偶数长度:以两个字符之间为中心(如 "abba")
我们可以枚举每个可能的中心点,然后向两边扩展,统计所有回文子串。
步骤:
- 遍历字符串的每个位置作为中心点
- 对于每个中心点,分别处理奇数长度和偶数长度的情况
- 向两边扩展,如果字符相等则计数加1,否则停止扩展
- 累加所有回文子串的数量
方法二:动态规划
核心思想:
使用二维布尔数组 dp[i][j] 表示子串 s[i...j] 是否为回文串。
状态转移方程:
- 如果
s[i] == s[j]且(j - i <= 2 或 dp[i+1][j-1] == true),则dp[i][j] = true - 否则
dp[i][j] = false
步骤:
- 初始化
dp数组 - 按子串长度从小到大遍历
- 根据状态转移方程填充
dp数组 - 统计
dp[i][j] == true的数量
方法三:马拉车算法(Manacher's Algorithm)
核心思想:
这是一种专门用于解决回文串问题的高效算法,可以在 O(n) 时间内找到所有回文子串。
不过对于本题来说,实现相对复杂,而中心扩展法已经足够高效,因此不详细展开。
💻 代码实现
java
class Solution {
// 方法一:中心扩展法
public int countSubstrings(String s) {
int count = 0;
char[] chars = s.toCharArray();
for (int i = 0; i < chars.length; i++) {
// 奇数长度回文串,以i为中心
count += expandAroundCenter(chars, i, i);
// 偶数长度回文串,以i和i+1为中心
count += expandAroundCenter(chars, i, i + 1);
}
return count;
}
private int expandAroundCenter(char[] chars, int left, int right) {
int count = 0;
while (left >= 0 && right < chars.length && chars[left] == chars[right]) {
count++;
left--;
right++;
}
return count;
}
}
// 方法二:动态规划
class Solution2 {
public int countSubstrings(String s) {
int n = s.length();
boolean[][] dp = new boolean[n][n];
int count = 0;
// 按子串长度遍历
for (int len = 1; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
if (len == 1) {
// 单个字符
dp[i][j] = true;
} else if (len == 2) {
// 两个字符
dp[i][j] = (s.charAt(i) == s.charAt(j));
} else {
// 长度大于2
dp[i][j] = (s.charAt(i) == s.charAt(j)) && dp[i + 1][j - 1];
}
if (dp[i][j]) {
count++;
}
}
}
return count;
}
}
go
// 方法一:中心扩展法
func countSubstrings(s string) int {
count := 0
chars := []byte(s)
for i := 0; i < len(chars); i++ {
// 奇数长度回文串,以i为中心
count += expandAroundCenter(chars, i, i)
// 偶数长度回文串,以i和i+1为中心
count += expandAroundCenter(chars, i, i+1)
}
return count
}
func expandAroundCenter(chars []byte, left, right int) int {
count := 0
for left >= 0 && right < len(chars) && chars[left] == chars[right] {
count++
left--
right++
}
return count
}
// 方法二:动态规划
func countSubstrings2(s string) int {
n := len(s)
dp := make([][]bool, n)
for i := range dp {
dp[i] = make([]bool, n)
}
count := 0
// 按子串长度遍历
for length := 1; length <= n; length++ {
for i := 0; i <= n-length; i++ {
j := i + length - 1
if length == 1 {
// 单个字符
dp[i][j] = true
} else if length == 2 {
// 两个字符
dp[i][j] = (s[i] == s[j])
} else {
// 长度大于2
dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1]
}
if dp[i][j] {
count++
}
}
}
return count
}
🧪 示例演示
让我们以 s = "aaa" 为例演示中心扩展法:
| 中心位置 | 奇数扩展结果 | 偶数扩展结果 | 累计计数 |
|---|---|---|---|
| i=0 | "a" → 1个 | "aa" → 2个 | 3 |
| i=1 | "a", "aaa" → 2个 | "aa" → 2个 | 7 |
| i=2 | "a" → 1个 | 无 → 0个 | 8 |
等等,这里有个错误!实际上对于 "aaa":
- i=0: 奇数→"a"(1), 偶数→"aa"(1) → 小计2
- i=1: 奇数→"a","aaa"(2), 偶数→"aa"(1) → 小计3
- i=2: 奇数→"a"(1), 偶数→无(0) → 小计1
总计:2+3+1=6 ✅
✅ 答案有效性证明
中心扩展法正确性证明:
-
完备性:任何回文子串都有唯一的中心(奇数长度)或中心间隙(偶数长度),我们的算法枚举了所有可能的中心,因此不会遗漏任何回文子串。
-
无重复:虽然同一个字符可能在不同的回文串中出现,但每个回文子串由其起始和结束位置唯一确定,我们的扩展过程对每个中心独立计算,不会重复计数相同的子串。
-
终止性:每次扩展都会使左右边界向外移动,当越界或字符不匹配时停止,保证算法会终止。
动态规划正确性证明:
- 基础情况:长度为1和2的子串判断正确
- 归纳步骤 :假设所有长度小于k的子串判断正确,那么长度为k的子串
s[i...j]是回文当且仅当s[i]==s[j]且s[i+1...j-1]是回文(已由归纳假设正确计算) - 最优子结构:回文性质具有最优子结构性质
📊 复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 优缺点 |
|---|---|---|---|
| 中心扩展法 | O(n²) | O(1) | ✅ 空间效率高,代码简洁 ❌ 最坏情况仍需O(n²) |
| 动态规划 | O(n²) | O(n²) | ✅ 思路清晰,易于理解 ❌ 空间开销大 |
| 马拉车算法 | O(n) | O(n) | ✅ 时间最优 ❌ 实现复杂,常数因子大 |
📌 推荐使用中心扩展法,因为它在空间效率和代码简洁性方面表现最佳。
📌 问题总结
- 关键洞察:回文串具有对称性,可以从中心向外扩展
- 两种形式:必须同时考虑奇数和偶数长度的回文串
- 时间权衡:虽然理论上存在O(n)解法,但O(n²)的中心扩展法在实际应用中更实用
- 扩展应用:此方法可轻松修改为返回最长回文子串、所有回文子串等变种问题
💡 面试提示:在面试中,建议先实现中心扩展法,如果面试官要求优化空间或时间,再讨论其他方法。
github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions