算法精讲-马拉车算法(Manacher's Algorithm)-查找最长回文子串

写在前面

一般在查找最长回文子串时,更多的通过扩展中心解法,这种解法的时间复杂度为O(n^2),而马拉车算法将时间复杂度精进到了O(n),是一位名叫Manacher的人在1975年提出的一种算法。

1-预处理

将原字符串以’#'分离,并在首尾增加不相同的字符。
例如:

原始字符串:abba                   -->字符串长度为:4
将字符串以‘#’分离:#a#b#a#b#        -->字符串长度为:9
首尾增加不相同的字符:$#a#b#a#b#^    -->字符串长度为:11

原始字符串:ersds                  -->字符串长度为:5
将字符串以‘#’分离:#e#r#s#d#s#      -->字符串长度为:11
首尾增加不相同的字符:$#e#r#s#d#s#^  -->字符串长度为:13

将字符串以“#”分离:目的是将字符串的长度都转为奇数。
首尾增加不相同的字符:目的是设定停止遍历的条件,因为首尾字符不与其他任何字符相同。

2-计算最长回文子串长度

定义两个容器:arr[]、p[],和一个变量i
arr[]中记录所有字符,p[]中记录每个字符为中心的最长回文半径

	i	0	1	2	3	4	5	6	7	8	9	10	11	12	13	14	15	16	17	18
arr[]	$	#	a	#	b	#	a	#	b	#	c	#	b	#	a	#	e	#	^
 p[]	1	1	2	1	2	1	4	1	2	1	6	1	2	1	2	1	2	1	1

因为原字符串的所有间隔都增加了 ’#‘ 符号,而增加的#比原先字符多一个
则增加的字符 # 的数量 减1 之后与初始字符串字符数相等

因此,最后我们得到最长回文半径和最长回文子串长度之间的关系:int maxLength = p[i]-1。maxLength表示最长回文子串长度。

如果不理解可拿几个示例验证:int maxLength = p[i]-1

3-计算最长回文子串起始索引

因为增加的分隔符均为#,所以#一定会存在于回文子串的两端位置
则回文子串的两侧#数量与字符数量一致

例如:

当i=5时,此时回文子串为#,回文子串的左侧为:$#a#b
当i=6时,此时回文子串为#b#a#b#,回文子串的左侧为:$#a
	其中:回文子串左侧的字符数量 = 回文子串的左侧长度 / 2

又因为可以通过i-p[i]来获取回文子串的左侧长度,
则:回文子串左侧的字符数量 = (i - p[i])/2
所以:最长回文子串的起始索引int index = (i - p[i])/2。

如果不理解可拿几个示例验证:int index = (i - p[i])/2。

4-计算p数组

2、3两步都使用了p数组,p数组是怎么得来的呢?我们还需要另外两个变量id和mx
id是回文子串的中心,mx是回文子串最右端的限制位

	i	0	1	2	3	4	5	6	7	8	9	10	11	12	13	14	15	16	17	18
arr[]	$	#	a	#	b	#	a	#	b	#	c	#	b	#	a	#	e	#	^
 p[]	1	1	2	1	2	1	4	1	2	1	6	1	2	1	2	1	2	1	1
 
 当i=6时,id=6, mx=10,那么p[id] = mx -id = 4
 当i=7时,id=7, mx=8,那么p[id] = mx -id = 1
 当i=8时,id=8, mx=10,那么p[id] = mx -id = 2

因为回文子串是中心对称的,知道中心对称的位置id,如果一个回文子串以i为中心,并且包含在以id为中心的回文子串中,即mx>i,那么肯定会存在另外一个以j为中心回文子串,和以i为中心的回文子串相等且对称,即p[j] = p[i],而i和j是以id为中心对称,即i+j=2id,如果知道了i的值,那么j = 2id - i。

例如:当id= 6,i = 8 时,
根据:j = 2*id - i,计算得:j = 4.
反之:当id= 6,i = 4 时,计算得:j = 8.

	i	0	1	2	3	4	5	6	7	8	9	10	11	12	13	14	15	16	17	18
arr[]	$	#	a	#	b	#	a	#	b	#	c	#	b	#	a	#	e	#	^
 p[]	1	1	2	1	2	1	4	1	2	1	6	1	2	1	2	1	2	1	1

但是我们需要考虑另外一种情况,如果存在一个以i为中心的回文子串,依旧有mx > i,但是以i为中心的回文子串右边界超过了mx,在i到mx的这段回文子串中,与另一端对称的以j为中心的回文子串还是相等的,此时p[i] = mx - i,p[j] = [pi],至于右边界mx之外的子串,即以i为中心的回文子串超出的部分是否还是满足上述条件就需要遍历比较字符了。

因此,在mx > i的情况下,p[i] = Math.min(p[2id - i], mx - i)。
另外如果i大于mx了,也即是边界mx后面的子串,依旧需要去比较字符计算。

代码示例

public class Test {
    public static void main(String[] args) {
        String nums = "ababcbae";
        String s = longestPalindrome2(nums);
        System.out.println("符合条件的集合为:"+s);
    }
    public static String preProcess(String s) {
        int n = s.length();
        if (n == 0) {
            return "^$";
        }
        String ret = "^";
        for (int i = 0; i < n; i++)
            ret += "#" + s.charAt(i);
        ret += "#$";
        return ret;
    }

    // 马拉车算法
    public static String longestPalindrome2(String s) {
        //预处理字符串
        String str = preProcess(s);
        int n = str.length();
        int[] p = new int[n];
        int id = 0, mx = 0;
        // 最长回文子串的长度
        int maxLength = -1;
        // 最长回文子串的中心位置索引
        int index = 0;
        for (int j=1; j<n-1; j++) {
            p[j] = mx > j ? Math.min(p[2*id-j], mx-j) : 1;
            // 向左右两边延伸,扩展右边界
            while (str.charAt(j+p[j]) == str.charAt(j-p[j])) {
                p[j]++;
            }
            // 如果回文子串的右边界超过了mx,则需要更新mx和id的值
            if (mx < p[j] + j) {
                mx = p[j] + j;
                id = j;
            }
            // 如果回文子串的长度大于maxLength,则更新maxLength和index的值
            if (maxLength < p[j] - 1) {
                maxLength = p[j] - 1;
                index = j;
            }
        }
        // 截取字符串,输出结果
        int start = (index-maxLength)/2;
        return s.substring(start, start + maxLength);
    }
}

运行结果

在这里插入图片描述

发布了17 篇原创文章 · 获赞 17 · 访问量 3073

猜你喜欢

转载自blog.csdn.net/weixin_43954926/article/details/104204537