力扣hot100之5:最长回文子串
力扣hot100之5:最长回文子串

LeetCode 5:最长回文子串(Longest Palindromic Substring)题解

Longest Palindromic Substring 是一道非常经典的字符串题。

题目要求我们在一个字符串 s 中,找到最长的回文子串,并把这个子串返回出来。

所谓回文串,就是正着读和反着读都一样的字符串,例如:

  • "aba"

  • "abba"

  • "racecar"

这道题有很多解法,比如:

  • 暴力枚举

  • 动态规划

  • 中心扩展法

  • Manacher 算法

本文采用的是最常见、也最容易理解的一种方法:

中心扩展法

这种写法思路清晰,代码简洁,非常适合作为这道题的主流题解。


题目示例

输入:s = "babad"
输出:"bab"

或者返回:

"aba"

因为 "bab""aba" 都是合法答案。

再看一个例子:

输入:s = "cbbd"
输出:"bb"

解题思路

回文串有一个非常重要的性质:

它一定是围绕某个“中心”对称的。

例如:

  • "aba" 的中心是字符 'b'

  • "abba" 的中心是中间两个字符之间的位置

所以我们可以枚举每一个位置,把它当成回文中心,然后向左右两边扩展,看看最长能扩展多远。

这里要注意,回文串分为两种情况:

  1. 奇数长度回文串 例如 "aba",中心是一个字符

  2. 偶数长度回文串 例如 "abba",中心是两个字符之间

因此,对于每一个位置 i,都需要做两次扩展:

  • i 为中心扩展:expandAroundCenter(s, i, i)

  • ii+1 之间为中心扩展:expandAroundCenter(s, i, i + 1)

然后取两者中的最大值即可。


Java 代码(带注释)

class Solution {
    public String longestPalindrome(String s) {
        // 特殊情况处理:
        // 如果字符串为空,或者长度小于 1,直接返回空字符串
        if (s == null || s.length() < 1) return "";

        // start 和 end 用来记录当前找到的最长回文子串的左右边界
        int start = 0, end = 0;

        // 枚举每一个位置,把它作为回文中心
        for (int i = 0; i < s.length(); i++) {
            // 情况 1:以 s[i] 为中心,寻找奇数长度回文串
            int len1 = expandAroundCenter(s, i, i);

            // 情况 2:以 i 和 i+1 之间为中心,寻找偶数长度回文串
            int len2 = expandAroundCenter(s, i, i + 1);

            // 取两种情况中的较大值
            int len = Math.max(len1, len2);

            // 如果当前回文长度大于之前记录的最长长度,就更新边界
            if (len > end - start) {
                // 计算当前最长回文的左边界
                start = i - (len - 1) / 2;

                // 计算当前最长回文的右边界
                end = i + len / 2;
            }
        }

        // 根据最终记录的左右边界,截取并返回最长回文子串
        return s.substring(start, end + 1);
    }

    private int expandAroundCenter(String s, int left, int right) {
        // 只要左右边界没有越界,并且左右字符相等,就继续向两边扩展
        while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
            left--;
            right++;
        }

        // 跳出循环时,left 和 right 已经多走了一步
        // 因此回文串长度应为 right - left - 1
        return right - left - 1;
    }
}

代码拆解

这段代码虽然不长,但细节很多。下面分步骤拆开来看。


1. 特殊情况处理

if (s == null || s.length() < 1) return "";

这一步是为了防止空字符串或者空对象导致后面处理出错。

如果字符串本身为空,那么最长回文子串自然也是空字符串。


2. 用 startend 记录答案区间

int start = 0, end = 0;

这里不直接保存答案字符串,而是先保存最长回文子串的左右边界。

这样做的好处是:

  • 避免在遍历过程中频繁截取字符串

  • 最后只需要调用一次 substring() 就能得到结果


3. 枚举每一个中心点

for (int i = 0; i < s.length(); i++)

这里的 i 表示当前枚举到的位置。

每个位置都可能成为一个回文中心,所以需要全部检查一遍。


4. 为什么要扩展两次?

int len1 = expandAroundCenter(s, i, i);
int len2 = expandAroundCenter(s, i, i + 1);

因为回文串既可能是奇数长度,也可能是偶数长度。

奇数长度回文

例如:

"aba"

中心是字符 'b',所以左右起点相同:

expandAroundCenter(s, i, i)

偶数长度回文

例如:

"abba"

中心不是某个字符,而是中间的空隙,所以左右起点不同:

expandAroundCenter(s, i, i + 1)

这就是为什么每个位置都要检查两种情况。


5. 更新最长回文区间

if (len > end - start) {
    start = i - (len - 1) / 2;
    end = i + len / 2;
}

这里是整段代码最关键的地方之一。

为什么左边界是:

i - (len - 1) / 2

为什么右边界是:

i + len / 2

因为我们需要同时兼容奇数和偶数长度的回文串。

举例:

情况 1:奇数长度回文 "aba"

  • 中心位置 i = 1

  • 长度 len = 3

则:

start = 1 - (3 - 1) / 2 = 0
end   = 1 + 3 / 2 = 2

得到区间 [0, 2],正确。

情况 2:偶数长度回文 "abba"

假设当前中心靠左的位置是 i = 1,长度 len = 4

则:

start = 1 - (4 - 1) / 2 = 0
end   = 1 + 4 / 2 = 3

得到区间 [0, 3],也正确。

所以这个公式能够统一处理奇偶两种情况。


6. 中心扩展函数的作用

private int expandAroundCenter(String s, int left, int right)

这个函数做的事情非常明确:

  • 从给定中心开始

  • 向左右两边扩展

  • 直到不能继续扩展为止

  • 返回当前回文串的长度

核心代码:

while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
    left--;
    right++;
}

只要满足以下三个条件,就继续扩展:

  1. 左边界没有越界

  2. 右边界没有越界

  3. 左右字符相等


7. 为什么返回 right - left - 1

这一点非常容易写错。

因为 while 循环结束时,leftright 已经多走了一步。

也就是说:

  • left 已经走到了回文边界外面一格

  • right 也已经走到了回文边界外面一格

所以真正的回文长度应该是:

right - left - 1

例如字符串 "aba"

  • 最终 left = -1

  • 最终 right = 3

那么长度就是:

3 - (-1) - 1 = 3

刚好正确。


执行过程演示

以字符串:

"babad"

为例。

i = 0

  • 奇数扩展得到 "b",长度为 1

  • 偶数扩展无法形成回文,长度为 0

当前最长回文是 "b"

i = 1

  • 奇数扩展可以得到 "bab",长度为 3

  • 偶数扩展长度较小

更新最长回文为 "bab"

i = 2

  • 奇数扩展可以得到 "aba",长度为 3

因为 "aba" 长度也为 3,所以答案可能仍然保持 "bab",也可能更新为 "aba",这两种都合法。


复杂度分析

时间复杂度

O(n^2)

原因是:

  • 外层循环会枚举每一个字符作为中心

  • 每次中心扩展在最坏情况下可能扩展到整个字符串长度

因此整体最坏时间复杂度是 O(n^2)

空间复杂度

O(1)

整个过程中只使用了少量变量,没有额外使用与输入规模相关的数据结构,因此空间复杂度为常数级。


这道题的优点和特点

中心扩展法的优点非常明显:

  • 思路自然,容易理解

  • 代码简洁

  • 不需要额外数组

  • 面试中非常常见

  • 足够通过大多数测试

当然,它不是这道题最优的理论解法。

如果追求更高效率,还可以学习 Manacher 算法,它可以做到:

O(n)

但对于大多数刷题场景来说,中心扩展法已经非常实用。


总结

这道题最核心的思想只有一句话:

回文串一定是围绕某个中心对称扩展出来的。

理解了这一点,整道题就非常顺理成章了:

  1. 枚举每个位置作为中心

  2. 分别处理奇数回文和偶数回文

  3. 每次扩展后更新最长回文区间

  4. 最后根据区间返回答案

这也是这道题最经典、最常用的写法。


适合直接发布的结尾

如果你刚开始刷字符串题,我非常推荐把这道题吃透。

因为它能帮助你掌握几个非常重要的能力:

  • 如何从题目性质中提炼解题思路

  • 如何设计辅助函数简化主逻辑

  • 如何统一处理奇数和偶数两种情况

  • 如何在遍历中维护当前最优解

把这道题真正理解透之后,再去看动态规划解法或者 Manacher 算法,你会轻松很多。

如果对您有帮助的话,能否支持一下博主?
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇