题目描述
思路解析
一句话答案:LeetCode 647 回文子串的主流解法是中心扩展计数:把 n 个字符与 n-1 条缝隙共 2n-1 个位置都当作对称中心向两边扩,每成功对称一层就恰好对应一个新的回文子串,计数加一。每个回文有且只有一个中心,所以不重不漏,时间 O(n²)、空间 O(1)。
回文子串这题在数什么
给字符串 s,统计其中回文子串的总个数。两条规则决定了计数口径:子串必须连续;只要起止位置不同就算不同的子串,哪怕内容一模一样。比如 "aa" 里两个 "a" 要算两个。s = "abacaba" 的答案是 12——单字符 7 个,再加 "aba"(两处)、"aca"、"bacab"、"abacaba"。
为什么不逐个子串验证回文
暴力路线是枚举全部 O(n²) 个子串、每个花 O(n) 验证,总共 O(n³)。真正的浪费在于验证之间零复用:"bacab" 从两端往里比,比到一半其实撞上了 "aca" 已经回文的事实,却当作不知道重比一遍。
把视角从「子串」换到「中心」就能复用这份信息:任何回文都关于自己的中心对称。固定一个中心向外扩,里层对称成立时只需再比最外侧两个字符,就能判定大一圈的子串是不是回文——里层的结论被直接继承,每往外一层只花 O(1)。
为什么每扩一层计数就加一
中心扩展和计数能严丝合缝地咬合,靠的是一个对应关系:以某中心向外扩张,第一层对称成功对应以它为中心的最短回文,再扩一层成功就对应大一圈的另一个回文。也就是说「对称成功一次」和「一个回文子串」一一对应,所以 while 循环里每比中一次就 ans 加一。
不重不漏也由中心保证:每个回文子串的中心是唯一确定的(奇数长度中心在字符上,偶数长度中心在缝隙上),它只会在枚举到自己中心时被数到一次,不同位置的相同内容因为中心不同而分别计数,正合题意。
2n-1 个中心怎么用一层循环枚举
中心必须包含 n 个字符和 n-1 条缝隙,共 2n-1 个——漏掉缝隙中心,所有偶数长度的回文(如 "bb")就永远数不到。参考代码用一个循环变量 c 从 0 走到 2n-2 统一编码两类中心:l = c // 2,r = c // 2 + c % 2。c 为偶数时 l 与 r 重合,是字符中心;c 为奇数时 r 比 l 大一,正好落在缝隙两侧。这样一层循环替代了奇偶两套代码。
复杂度多少,还有什么别的解法
中心 2n-1 个,每个最多向外扩约 n/2 层,时间 O(n²);只用两个指针和一个计数器,空间 O(1)。写扩展循环时越界判断要放在字符比较之前——while 条件先查 l >= 0 且 r < n,顺序反了会越界访问。
同类解法还有区间动态规划:dp[i][j] 表示 s[i..j] 是否回文,由 s[i] == s[j] 且 dp[i+1][j-1] 推出,时间同为 O(n²) 但要 O(n²) 空间。追求线性时间可用 Manacher 马拉车算法,靠对称性复用回文半径做到 O(n),实现复杂,通常面试点到为止。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
核心一句话:枚举每个中心,向两边扩,每对称成功一次就是一个新回文,计数加一。下面每一帧都在套它。
以单字符 'a'(下标 0)为中心。它本身就是一个回文(长度 1)。从这里开始向左右两边扩展。
左右指针落在同一个字符 'a' 上,它自己就是回文,计数变成 1,再往外扩一格。
以下标 0、1 之间的缝隙为中心。偶数长度回文从这条缝隙往外扩。从这里开始向左右两边扩展。
左 s[0]='a' 与右 s[1]='b' 不相等,无法再对称,这个中心扩完了,去枚举下一个中心。
以单字符 'b'(下标 1)为中心。它本身就是一个回文(长度 1)。从这里开始向左右两边扩展。
左右指针落在同一个字符 'b' 上,它自己就是回文,计数变成 2,再往外扩一格。
左 s[0]='a' 与右 s[2]='a' 相等,"aba" 是回文,计数加到 3。左指针左移、右指针右移,继续往外扩。
以下标 1、2 之间的缝隙为中心。偶数长度回文从这条缝隙往外扩。从这里开始向左右两边扩展。
左 s[1]='b' 与右 s[2]='a' 不相等,无法再对称,这个中心扩完了,去枚举下一个中心。
以单字符 'a'(下标 2)为中心。它本身就是一个回文(长度 1)。从这里开始向左右两边扩展。
左右指针落在同一个字符 'a' 上,它自己就是回文,计数变成 4,再往外扩一格。
左 s[1]='b' 与右 s[3]='c' 不相等,无法再对称,这个中心扩完了,去枚举下一个中心。
以下标 2、3 之间的缝隙为中心。偶数长度回文从这条缝隙往外扩。从这里开始向左右两边扩展。
左 s[2]='a' 与右 s[3]='c' 不相等,无法再对称,这个中心扩完了,去枚举下一个中心。
以单字符 'c'(下标 3)为中心。它本身就是一个回文(长度 1)。从这里开始向左右两边扩展。
左右指针落在同一个字符 'c' 上,它自己就是回文,计数变成 5,再往外扩一格。
左 s[2]='a' 与右 s[4]='a' 相等,"aca" 是回文,计数加到 6。左指针左移、右指针右移,继续往外扩。
左 s[1]='b' 与右 s[5]='b' 相等,"bacab" 是回文,计数加到 7。左指针左移、右指针右移,继续往外扩。
左 s[0]='a' 与右 s[6]='a' 相等,"abacaba" 是回文,计数加到 8。左指针左移、右指针右移,继续往外扩。
以下标 3、4 之间的缝隙为中心。偶数长度回文从这条缝隙往外扩。从这里开始向左右两边扩展。
左 s[3]='c' 与右 s[4]='a' 不相等,无法再对称,这个中心扩完了,去枚举下一个中心。
以单字符 'a'(下标 4)为中心。它本身就是一个回文(长度 1)。从这里开始向左右两边扩展。
左右指针落在同一个字符 'a' 上,它自己就是回文,计数变成 9,再往外扩一格。
左 s[3]='c' 与右 s[5]='b' 不相等,无法再对称,这个中心扩完了,去枚举下一个中心。
以下标 4、5 之间的缝隙为中心。偶数长度回文从这条缝隙往外扩。从这里开始向左右两边扩展。
左 s[4]='a' 与右 s[5]='b' 不相等,无法再对称,这个中心扩完了,去枚举下一个中心。
以单字符 'b'(下标 5)为中心。它本身就是一个回文(长度 1)。从这里开始向左右两边扩展。
左右指针落在同一个字符 'b' 上,它自己就是回文,计数变成 10,再往外扩一格。
左 s[4]='a' 与右 s[6]='a' 相等,"aba" 是回文,计数加到 11。左指针左移、右指针右移,继续往外扩。
以下标 5、6 之间的缝隙为中心。偶数长度回文从这条缝隙往外扩。从这里开始向左右两边扩展。
左 s[5]='b' 与右 s[6]='a' 不相等,无法再对称,这个中心扩完了,去枚举下一个中心。
以单字符 'a'(下标 6)为中心。它本身就是一个回文(长度 1)。从这里开始向左右两边扩展。
左右指针落在同一个字符 'a' 上,它自己就是回文,计数变成 12,再往外扩一格。
13 个中心全部扩展完毕,把每次对称成功累加起来,回文子串一共有 12 个,返回 12。
注意 "aa" 是 3 不是 2:两个单字符各算一个,再加整体 "aa"。
两个高频追问,面试常被追问其他解法和复杂度来源。
参考代码
def countSubstrings(s: str) -> int: n = len(s) ans = 0 for c in range(2 * n - 1): l, r = c // 2, c // 2 + c % 2 while l >= 0 and r < n and s[l] == s[r]: ans += 1 l -= 1 r += 1 return ans复杂度
- 时间:O(n²),2n-1 个中心,每个最多扩 n/2 次
- 空间:O(1),只用计数器和两个指针,不开额外数组
易错点
面试追问把动画讲成自己的话
追问除了中心扩展还有别的解法吗?
追问中心扩展为什么是 O(n²)?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
解码方法
LeetCode 91 · 中等 · 沿着 一维动态规划 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题