Manacher算法浅显易懂的解析

一、是什么?

Manacher算法,又叫“马拉车”算法,可以在时间复杂度为O(n)的情况下求解一个字符串的最长回文子串长度的问题。它是对中心扩散法的一个优化。
中心扩散法,简称Z法。
Manacher算法,简称M法。

二、相关知识

  1. 回文字符串,一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。
  2. 中心扩散法,以某点为中心向两端扩散,找到以该点为中心的最长回文子串。(代码在最后)

三、为什么?

因为这个方法是对中心扩散法的一个优化。它的时间复杂度为O(n),而中心扩散法是时间复杂度O(n2),空间复杂度O(1)。
它是怎么做到的?Z法是对每个点都从中心向两边查找,而M法部分是从中心,部分是根据对称法则直接得到。

四、怎么做?

回文串分奇偶,所以首先会对字符串加“#”处理。这样可以简化代码。

private fun manacherString(s: String): String {
        val sb = StringBuilder()
        for (i in 0 until s.length) {
            sb.append("#${s[i]}")
        }
        sb.append("#")
        return sb.toString()
    }

接着弄清楚几个基本概念和几个结论。
在这里插入图片描述

1、回文半径数组radius

记录以每个位置的字符为回文中心求出的最长回文半径长度
比如i位置,radius[i]即为i位置的最长回文半径

2、当前最长回文右边界R

这个位置之前的回文子串所能到达的最右边的地方。
比如当前到达角标i,i之前的角标中radius最大值maxTemp,而R<=i + maxTemp - 1。

3、当前最长回文的对称中心所指的角标c

代码中会取较小的值Math.min(radius[2c-i],R-i+1),正常情况下c>=R/2。
比如当前到达角标i,而radius[i-1]是最大值,则c=i-1,R=radius[i-1]+i-1-1。
2
c-i实际上是当前角标i以c为对称中心的对称点,比如现在的角标c+1=i,对称点2*c-i=i-1。

4、如果radius中的最大值为max,则最大回文字符串长度为max-1。

这个结论用文字就可以说清楚,现在能取的最大回文必然是奇数,因为最左和最右必然是“#”,-1后则为偶数,因为这是最大回文半径,所以-1后的值就是最大回文字符串长度。
在这里插入图片描述
在这里插入图片描述
上面两个图取的是特殊点,是为了简单验证我给出的结论。

五、代码

Z法

/**
     * 中心扩展算法
     * 从中间往两边,必然是相等的字符
     */
    fun longestPalindrome1(s:String):String{
        val n = s.length
        if (n == 0 || n == 1) {
            return s
        }

        var left = 0
        var right =0
        for (i in 0 until n) {
            //两种对称的情况,中心点在2n或2n+1的位置
            val len1 = getLongestLen(s,i,i)
            val len2 = getLongestLen(s,i,i+1)
            val len = Math.max(len1, len2)
            if (len > right-left) {
                left = i - (len-1)/2//-1是为了防止出现小于0
                right = i+len/2
            }
        }
        return s.subSequence(left,right+1).toString()
    }

    private fun getLongestLen(s: String, left:Int, right:Int):Int{
        var lef = left
        var righ = right
        while (lef >= 0 && righ < s.length && s[lef] == s[righ]) {
            lef--
            righ++
        }
        return righ - lef+1-2//上面的操作会多进行一步,所以要-2
    }

M法

/**
     * Manacher算法
     *“马拉车”算法
     * 用“#”字符处理之后的新字符串
     * 1、回文半径数组radius   记录以每个位置的字符为回文中心求出的最长回文半径长度
     * 2、当前最长回文右边界R    这个位置之前的回文子串所能到达的最右边的地方。
     * 3、当前最长回文的对称中心所指的角标c  所以代码中会取较小的值Math.min(radius[2*c-i],R-i+1),正常情况下c>=R/2
     * 注:这里的最长回文是指某个点的最长回文,因为从某个点算,可能有多个回文串。
     * R <= radius[i] + i-1 取一个特殊的点就可以得到该结论
     * radius[i] - 1正好是原字符串中最长回文串的长度
     */
    fun longestPalindrome2(s:String):String{
        if (s.isEmpty()) {
            return s
        }
        val charArr = manacherString(s)
        //回文半径数组 radius[i] >= R-i+1
        val radius = IntArray(charArr.length)
        var R = 0//当前回文子串右边界
        var c = 0//当前最长回文子串中心指向的角标l
        var max = -1//最长回文半径
        var r = 0//最长回文子串的中心角标
        for(i in 0 until radius.size){
            //角标小于当前最大回文子串最右侧R时,根据中心对称原则直接计算radius[i]
            //否则需要通过下面的while循环计算
            radius[i] = if(R>i) Math.min(radius[2*c-i],R-i+1) else 1
            //和中心扩散方法一样,寻找该点的最长回文串;同时保证不超过边界
            //如果radius[i]为1,则判断i点周围的第一位字符;
            //不为1,理论上来说会直接到下一轮循环
            while (i + radius[i] < charArr.length && i - radius[i] > -1
                    && charArr[i - radius[i]] == charArr[i + radius[i]]) {
                radius[i]++
            }
            //如果回文半径有更新,则更新最右边界,从而提高效率;这就是比Z法快的原因
            if (i + radius[i]-1 > R) {
                R = i+radius[i]-1
                c = i
            }
            //存储最长回文半径,及其角标
            if (radius[i] > max) {
                r = i
                max = radius[i]
            }
        }
        //最长回文半径为max-1,中心为r,则字符串所在位置[r-(max-1),r+(max-1)]
        val sb = charArr.subSequence(r-(max-1),r+(max-1)).replace(Regex("#"),"")
        return sb
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值