(LeetCode-Hot100)647. 回文子串

647. 回文子串

LeetCode 题目链接

❓ 问题简介

给你一个字符串 s,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串是正着读和倒过来读一样的字符串。

子字符串是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例说明

示例 1:

复制代码
输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"

示例 2:

复制代码
输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

💡 解题思路

方法一:中心扩展法(推荐)

核心思想:

回文串有两种形式:

  • 奇数长度:以单个字符为中心(如 "aba")
  • 偶数长度:以两个字符之间为中心(如 "abba")

我们可以枚举每个可能的中心点,然后向两边扩展,统计所有回文子串。

步骤:

  1. 遍历字符串的每个位置作为中心点
  2. 对于每个中心点,分别处理奇数长度和偶数长度的情况
  3. 向两边扩展,如果字符相等则计数加1,否则停止扩展
  4. 累加所有回文子串的数量

方法二:动态规划

核心思想:

使用二维布尔数组 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

步骤:

  1. 初始化 dp 数组
  2. 按子串长度从小到大遍历
  3. 根据状态转移方程填充 dp 数组
  4. 统计 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. 无重复:虽然同一个字符可能在不同的回文串中出现,但每个回文子串由其起始和结束位置唯一确定,我们的扩展过程对每个中心独立计算,不会重复计数相同的子串。

  3. 终止性:每次扩展都会使左右边界向外移动,当越界或字符不匹配时停止,保证算法会终止。

动态规划正确性证明:

  1. 基础情况:长度为1和2的子串判断正确
  2. 归纳步骤 :假设所有长度小于k的子串判断正确,那么长度为k的子串 s[i...j] 是回文当且仅当 s[i]==s[j]s[i+1...j-1] 是回文(已由归纳假设正确计算)
  3. 最优子结构:回文性质具有最优子结构性质

📊 复杂度分析

方法 时间复杂度 空间复杂度 优缺点
中心扩展法 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

相关推荐
回敲代码的猴子1 小时前
2月18日打卡
算法
春和景明3601 小时前
mysql复习
java
宇木灵1 小时前
C语言基础-六、指针
c语言·开发语言·学习·算法
苦藤新鸡1 小时前
64 搜索平移递增数组中的元素
数据结构·算法
百锦再1 小时前
Java InputStream和OutputStream实现类完全指南
java·开发语言·spring boot·python·struts·spring cloud·kafka
Vic101011 小时前
链表算法三道
java·数据结构·算法·链表
zheshiyangyang2 小时前
前端面试基础知识整理【Day-8】
前端·面试·职场和发展
再难也得平2 小时前
[LeetCode刷题]128.最长连续序列(从零开始的java题解)
java·算法·leetcode
亓才孓2 小时前
【MyBatis Exception】SQLSyntaxErrorException(按批修改不加配置会报错)
java·开发语言·mybatis