给定字符串 s,找出其中最长的回文子串。本文用"中心扩展":把每个位置当作回文的中心,向两边同时扩展,直到两端字符不等。时间 O(n²),空间 O(1),写法简单,不容易出错。真正容易漏掉的是回文中心有两种:奇数长度的中心是一个字符,偶数长度的中心是两个相邻字符之间。本文实测只处理奇数中心会漏掉什么,给出五种语言的实现,并把扩展过程做成分步演示。
完整题目见官方页面(链接在文末)。这道题的答案不唯一,"babad" 的最长回文可以是 "bab" 也可以是 "aba",所以核对方式不是比较字符串是否相等,而是检查三件事:返回值是原串的子串、它本身是回文、长度等于暴力解(枚举所有子串逐个检查)得到的最大长度。前几组基准用例的输出取的是暴力解返回的最靠左的一个:
输入 ["babad"] 输出 "bab" 输入 ["cbbd"] 输出 "bb" 输入 ["a"] 输出 "a" 输入 ["ac"] 输出 "a" 输入 ["bb"] 输出 "bb" 输入 ["abcba"] 输出 "abcba"
只用"每个字符当中心"的写法,扩展出来的回文长度一定是奇数。"bb"、"abba" 这样的偶数长度回文没有中心字符,必须再把"相邻两个字符之间"也当作中心。下面是只扩展奇数中心的写法和正确写法的真实结果:
# 只处理奇数长度中心(漏掉偶数长度)的写法 "cbbd": 只扩奇数中心 -> "c" 正确写法 -> "bb" "bb": 只扩奇数中心 -> "b" 正确写法 -> "bb" "abba": 只扩奇数中心 -> "a" 正确写法 -> "abba"
"cbbd" 的最长回文是 "bb",只扩展奇数中心的写法得到的是 "c"。题目的第二个官方示例恰好就是这种情况,所以这个错误很容易在提交时才被发现。
对每个下标 i,做两次扩展:一次从 (i, i) 开始(奇数长度),一次从 (i, i + 1) 开始(偶数长度)。每次扩展到两端字符不相等或越界为止,然后退回一格得到这个中心的最长回文,和当前最优比较长度。Python、Java、Go、JavaScript、C++ 五种语言都是真实编译运行的;实现里用一个 k 循环(0 是奇数、1 是偶数)合并两种中心。
Python · 解法代码def solve(s): def expand(l, r): # 从中心向两边扩,返回回文的 [l, r] while l >= 0 and r < len(s) and s[l] == s[r]: l -= 1 r += 1 return l + 1, r - 1 best_l, best_r = 0, 0 for i in range(len(s)): for l, r in (expand(i, i), expand(i, i + 1)): # 奇数长度、偶数长度两种中心 if r - l > best_r - best_l: best_l, best_r = l, r return s[best_l:best_r + 1]
$ python3 solution.py # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON ["babad"] → "bab" ["cbbd"] → "bb" ["a"] → "a" ["ac"] → "a" ["bb"] → "bb" ["abcba"] → "abcba"
Java · 解法代码class Solution { public String longestPalindrome(String s) { int bestL = 0, bestR = 0; for (int i = 0; i < s.length(); i++) { for (int k = 0; k < 2; k++) { // k = 0:奇数长度中心;k = 1:偶数长度中心 int l = i, r = i + k; while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) { l--; r++; } l++; r--; // 退回到最后一个成功的回文 if (r - l > bestR - bestL) { bestL = l; bestR = r; } } } return s.substring(bestL, bestR + 1); } }
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl ["babad"] → "bab" ["cbbd"] → "bb" ["a"] → "a" ["ac"] → "a" ["bb"] → "bb" ["abcba"] → "abcba"
Go · 解法代码func longestPalindrome(s string) string { bestL, bestR := 0, 0 for i := 0; i < len(s); i++ { for k := 0; k < 2; k++ { // k = 0:奇数长度中心;k = 1:偶数长度中心 l, r := i, i+k for l >= 0 && r < len(s) && s[l] == s[r] { l-- r++ } l++ r-- // 退回到最后一个成功的回文 if r-l > bestR-bestL { bestL, bestR = l, r } } } return s[bestL : bestR+1] }
$ go build -o bin . && ./bin < cases.jsonl ["babad"] → "bab" ["cbbd"] → "bb" ["a"] → "a" ["ac"] → "a" ["bb"] → "bb" ["abcba"] → "abcba"
JavaScript · 解法代码function longestPalindrome(s) { let bestL = 0, bestR = 0; for (let i = 0; i < s.length; i++) { for (let k = 0; k < 2; k++) { // k = 0:奇数长度中心;k = 1:偶数长度中心 let l = i, r = i + k; while (l >= 0 && r < s.length && s[l] === s[r]) { l--; r++; } l++; r--; // 退回到最后一个成功的回文 if (r - l > bestR - bestL) { bestL = l; bestR = r; } } } return s.slice(bestL, bestR + 1); }
$ node run.js < cases.jsonl ["babad"] → "bab" ["cbbd"] → "bb" ["a"] → "a" ["ac"] → "a" ["bb"] → "bb" ["abcba"] → "abcba"
C++ · 解法代码#include <string> using namespace std; class Solution { public: string longestPalindrome(string s) { int bestL = 0, bestR = 0, n = s.size(); for (int i = 0; i < n; i++) { for (int k = 0; k < 2; k++) { // k = 0:奇数长度中心;k = 1:偶数长度中心 int l = i, r = i + k; while (l >= 0 && r < n && s[l] == s[r]) { l--; r++; } l++; r--; // 退回到最后一个成功的回文 if (r - l > bestR - bestL) { bestL = l; bestR = r; } } } return s.substr(bestL, bestR - bestL + 1); } };
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl ["babad"] → "bab" ["cbbd"] → "bb" ["a"] → "a" ["ac"] → "a" ["bb"] → "bb" ["abcba"] → "abcba"
共 10 组用例(含边界),以暴力解的结果为基准逐组比对: Python 通过 10/10 Java 通过 10/10 Go 通过 10/10 JavaScript 通过 10/10 C++ 通过 10/10
最坏输入是全部字符相同(每个中心都能一路扩到边界)。下表里"暴力解"是枚举所有子串再逐个判断回文,每个规模取三次中最快的一次:
字符串长度 n | 暴力解 | 本文解法
100 | 0.715 ms | 0.220 ms
300 | 8.526 ms | 2.103 ms
1000 | (未测,太慢) | 25.396 ms
2000 | (未测,太慢) | 102.819 ms
4000 | (未测,太慢) | 413.663 ms中心扩展从 1000 到 2000 再到 4000(每次翻倍),耗时从 26.2ms 涨到 102.1ms 再涨到 424.0ms,每次增长约 4 倍,符合 O(n²)。暴力解在这个测试里只比中心扩展慢 3 到 4 倍(0.76ms 对 0.21ms、8.3ms 对 2.0ms),远没有理论上 O(n³) 与 O(n²) 差一个数量级的样子,因为暴力解里判断回文用的是 Python 内置的切片和比较,跑在 C 层,常数很小;这个差距不能拿来说明"暴力解只慢几倍",只说明在这个实现下的常数。更大规模的暴力解耗时太长,没有再测。
Python 版本另外和暴力解做了随机对拍(因为答案不唯一,检查的是返回值是回文、是原串的子串、长度等于暴力解的最大长度):
随机对拍 5000 组(长度 1 到 25、字母表大小 1 到 3 的随机字符串(字母表小,回文多,经常有多个等长的最长回文);因为答案不唯一,检查的是:返回值确实是回文、是原串的子串、长度等于暴力解得到的最大长度),solve 与 brute 的结果全部一致。
另外四种语言只在上面 10 组固定用例上验证过,没有做大规模随机对拍。
下面的演示用 s = "cbbd",它会经过奇数中心和偶数中心两种情况。每一帧都是把上面的解法在这个字符串上真实执行记录下来的,运行器还检查了演示最终得到的答案是合法的最长回文。