题目
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为1000。
Input: “babcd”
Output: “bab”
本文将介绍三种解法,重点介绍时间复杂度最低的Manacher算法。
O(n^3)算法
思路
- 从最长的子串开始,遍历所有该原字符串的子串;
- 每找出一个字符串,就判断该字符串是否为回文;
- 子串为回文时,则找到了最长的回文子串,因此结束;反之,继续遍历。
时间复杂度
- 遍历字符串子串:嵌套一个循环:O(n^2);
- 判断是否为回文:再次嵌套一个循环:O(n^3)。
代码
public static String longestPalindrome(String s) { |
O(n^2)算法
思路
- 将子串分为单核和双核的情况,单核即指子串长度为奇数,双核则为偶数;
- 遍历每个除最后一个位置的字符index(字符位置),单核:初始low = 初始high = index,low和high均不超过原字符串的下限和上限;判断low和high处的字符是否相等,相等则low++、high++(双核:初始high = 初始low+1 = index + 1);
- 每次low与high处的字符相等时,都将当前最长的回文子串长度与high-low+1比较。后者大时,将最长的回文子串改为low与high之间的;
- 重复执行第二、三步,直至high-low+1 等于原字符串长度或者遍历到最后一个字符,取当前截取到的回文子串,该子串即为最长的回文子串。
时间复杂度
遍历字符:一层循环——O(n-1);
找以当前字符为中心的最长回文子串:嵌套两个独立循环——O(2n*(n-1)) = O(n^2)
代码
private static int maxLen = 0; |
O(n)算法——Manacher算法
思路
Manacher算法是通过求解一个中心点,在距离这个点R长度以内都是关于这个点左右对称的,也就是说这个长度为2R的字符串是一个回文串,最后再比较大小,求出最大的长度2R及其中心点。整个过程只扫描整个字符串一遍。
这里也可以看出,因为要求回文子串的中心点,这个中心点也是唯一的,所以它只能处理字符串是奇数位的情况。因此第一步就是把字符串长度变为奇数,这里使用一个非常巧妙的方式,把字符串中每个字符使用一个其它字符号包围起来,这里以#
号为例,可以想象一下,要把每个字符都使用#号包裹,那么需要的#号总是要比原来的字符串长度多一位,才能保证每个字符都能被插入到#与#中间,比如:
a -> #a#
abaf -> #a#b#a#f#
可以看出,不管原来的字符串长度是什么,奇数加偶数结果肯定为奇数,在这之后,就可以开始求最长回文子串的半径R了。
借助两个变量center、right分别记录回文子串对应的中心点和右端点
可以直接看出,right就是2*center-i
(也就是i关于center的对称点),既然是对称点,那么当端点right > i时,端点i需要进行计算回文子串R,但它的对称点有可能也进行过计算,所以可以无需从头开始匹配,因为这些点都包含在一个已经进行过匹配的父回文串中,所以这里可以直接取right-i和它的对称点回文子串半径长度较小的,用来保证绝对进行过计算的回文子串的部分;
反之,就只能从1个长度开始匹配了,就是下面的这行代码:
r[i] = right > i ? (Math.min(r[2*center-i], right-i)) : 1 |
这里借助一个辅助的数组r[]来记录回文子串的半径R,r[i]表示的是以i为中心点的回文字符串的半径长度(初始情况下为1),知道r[i]后,就可以继续把索引向左右两边扩充,也就是看i+r[i]与i-r[i]左右端点的位置所对应的字符是否相等,相等的话就把回文半径r[i]继续扩充,直到不相等为止。
进行这一轮扩充后,观察之前的右端点right是否小于回文子串扩充后的右端点i+r[i],小于就直接更新右端点和中心点,不小于就说明当前回文子串还是在当前right端点的内部。
时间复杂度
只需进行一次遍历,时间复杂度为O(n),具体的证明过程省略。
代码
public static String longestPalindrome(String s) { |