kmp算法及manacher算法分析

1.KMP算法

kmp算法主要用来解决字符串匹配的问题,即一个字符串是否是另外一个字符串的子串。

(1)暴力法

首先想到的方法就是暴力匹配法,即两个字符串按位进行匹配,如果遇到不相同的,则从从头开始的下一位开始匹配。

package algorithm.manacher.kmp;

/**
 * @author chengzhengda
 * @version 1.0
 * @date 2020-04-16 18:39
 * @desc
 */
public class baoli {
    public static boolean baoli(String str1, String str2) {
        for (int i = 0; i < str1.length(); i++) {
            int idx1 = i;
            for (int j = 0; idx1 < str1.length() && j < str2.length(); j++) {
                if (str1.charAt(idx1) != str2.charAt(j)) {
                    break;
                }
                idx1++;
            }
            if (idx1 == i + str2.length()) {
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        String str1 = "abcdefg";
        String str2 = "efgd";
        System.out.println(baoli(str1, str2));
    }
}

暴力解法的问题是每一次都得从头开始匹配,没有利用到之前的匹配结果,导致了较高的时间复杂度。

(2)kmp算法

那么如何利用之前的匹配结果呢,我们先从一个例子开始:
Str1 = “abcdabca”
Str2 = “abcdabcq”
如上面的两个字符串所示,当两个字符串按位匹配到最后一位时,发现最后一位不相同,这时候按照暴力法的解法,需要回到str1的第二位与str2的第一位开始匹配,
但是我们发现str2的最后一位之前的前缀”abc”与后缀”abc”相同。既然str1和str2匹配到了最后一位,那说明str1和str2最后一位的前面是相同的,str2可以直接跳到第四位与str1的最后一位进行匹配,这样就利用到了前面匹配的信息,同时利用到了字符串的前后缀对称特性。
那么字符串匹配问题就成了计算字符串的前后缀对称问题,这里利用一个next数组来存储前后缀对称信息。
还是以字符串”abcdabcq”为例:
由于第一位不存在前缀数据,所以初始化为-1,而第二位不存在前后缀对称信息,这里初始化为0,从第三位开始,我们设置两个指针i和j,以及一个next数组,比较i-1与j位置上的字符是否相同,如果相同,则next[i++]=++j,如果不相同,则判断j是否大于0,如果大于0,则j=next[j],如果等于0,则next[i++]=0,依次遍历字符串,便可求解出next数组,下面是例子中的字符串计算出的next数组:
a b c d a b c q
-1 0 0 0 0 1 2 3
有了next数组之后,我们如何利用它来解决匹配问题呢?
在两个字符串str1,str2遍历匹配的过程中,有两个指针i和j,分别表示字符串遍历到的位置,如果相应的字符相同,则i和j都往后移动一位。如果不相同,判断str2的指针是否在第一位,如果是,则i++,即从下一位开始匹配,如果不是,则根据next数组记录的前后缀对称信息,跳到相应的位置继续进行匹配。最后判断str2是否已经执行到了最后一位,来判断两个字符串是否匹配。下面是kmp算法的代码:

package algorithm.manacher.kmp;

/**
 * @author chengzhengda
 * @version 1.0
 * @date 2020-04-16 20:33
 * @desc
 */
public class kmp {
    public static boolean kmp(String str1, String str2) {
        char[] ch1 = str1.toCharArray();
        char[] ch2 = str2.toCharArray();
        int i = 0;
        int j = 0;
        int[] next = next(ch2);
        while (i < ch1.length && j < ch2.length) {
            if (ch1[i] == ch2[j]) {
                i++;
                j++;
            } else {
                if (next[j] == -1) {
                    i++;
                } else {
                    j = next[j];
                }
            }
        }
        return j == ch2.length;
    }

    public static int[] next(char[] ch) {
        int next[] = new int[ch.length];
        next[0] = -1;
        next[1] = 0;
        int pos = 2;
        int cn = 0;
        while (pos < ch.length) {
            if (ch[pos - 1] == ch[cn]) {
                next[pos++] = ++cn;
            } else if (cn > 0) {
               cn = next[cn];
            } else {
                next[pos++] = cn;
            }
        }
        return next;
    }
}

2.Manacher算法

manacher算法是用来解决最长回文子串的问题,至于什么是回文字符串就不在这里赘述了。感性的可以自行百度,我们先来讨论一下如何查找最长回文子串。

(1)中心扩散法

最容易想到的方法是中心扩散法,首先遍历字符串,以每个字符串为中心,向左右扩散,如果左右相同,则继续扩散,否则停止。但是需要分奇偶数两种情况,因为回文串的长度可能是奇数,也可能是偶数。
下面来看一下代码:

package algorithm.manacher;

/**
 * @author chengzhengda
 * @version 1.0
 * @date 2020-03-18 15:03
 * @desc
 */
public class CircleExpand {
    // 双指针中心扩散
    public static int expand(String s, int left, int right) {
        int l = left;
        int r = right;
        while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {
            l--;
            r++;
        }
        return r - l + 1;
    }

    public static String findLongest(String s) {
        if (s == null || s.length() < 1) {
            return "";
        }
        int start = 0;
        int end = 0;
        int len = 0;
        for (int i = 0; i < s.length(); i++) {
            int len1 = expand(s, i, i);
            int len2 = expand(s, i, i + 1);
            len = Math.max(len1, len2);
            if (len > (end - start)) {
                start = i - (len - 1) / 2;
                end = i + len / 2;
            }
        }
        return s.substring(start, end - start + 1);
    }

    public static void main(String[] args) {
        System.out.println(findLongest("aba"));

    }
}

(2)manacher算法

从上述代码中可以发现,该算法存在几个缺陷:
(1)需要分奇偶两种情况
(2)没有利用到之前的结果
(3)没有利用到回文字符串的特性
那么manacher算法是如何解决这几个问题的呢,我们首先看第一个问题,
通过在原字符串的每个相邻两个字符中间插入一个分隔符,同时在首位也要添加一个分隔符,这里以插入"#“为例,通过该方法,回文串"aba"就变成了”#a#b#a#",假设原字符串的长度为n,可以发现新字符串的长度等于2*n+1,因此回文串的长度永远是奇数的,这样就解决了奇偶数的问题了。
字符串"#a#b#a#"的中心为#,坐标为3,则可以定义一个数组Len,用来记录每一个坐标的回文半径的长度,Len[i] = r, 而该回文串的长度则为r-1,那么求解最长回文串的问题就转化为了求Len数组。
kmp算法及manacher算法分析_第1张图片
如上图所示,id为字符串中的一个回文串的中心,mx为回文串的最有边界,则mx = Len[id]+id,j为回文串左半部分中的一个点,i为回文串中j对应的点。
我们分为3种情况进行讨论:
(1)j 的回文串有一部分在 id 的之外,如下图

kmp算法及manacher算法分析_第2张图片
上图中,黑线为 id 的回文,i 与 j 关于 id 对称,红线为 j 的回文。那么此时p[i] = mx - i,即紫线。那么p[i]还可以更大么?答案是不可能!见下图:
kmp算法及manacher算法分析_第3张图片
假设右侧新增的紫色部分是p[i]可以增加的部分,那么根据回文的性质,a 等于 d ,也就是说 id 的回文不仅仅是黑线,而是黑线+两条紫线,矛盾,所以假设不成立,故p[i] = mx - i,不可以再增加一分。
(2)j 回文串全部在 id 的内部,如下图:
kmp算法及manacher算法分析_第4张图片
根据代码,此时p[i] = p[j],那么p[i]还可以更大么?答案亦是不可能!见下图:
kmp算法及manacher算法分析_第5张图片
假设右侧新增的红色部分是p[i]可以增加的部分,那么根据回文的性质,a 等于 b ,也就是说 j 的回文应该再加上 a 和 b ,矛盾,所以假设不成立,故p[i] = p[j],也不可以再增加一分。
(3)j 回文串左端正好与 id 的回文串左端重合,见下图:
kmp算法及manacher算法分析_第6张图片
此时p[i] = p[j]或p[i] = mx - i,并且p[i]还可以继续增加,所以需要继续进行中心扩散。
根据上述三种情况的分析,可以发现,manacher算法较好的利用了前面的结果以及回文串的对称性。那么代码也就很好写了,如下所示:

package algorithm.manacher;

/**
 * @author chengzhengda
 * @version 1.0
 * @date 2020-03-20 10:46
 * @desc
 */
public class manacher {
    public static String expand(String s) {
        StringBuilder stringBuilder = new StringBuilder("@#");
        for (int i = 0; i < s.length(); i++) {
            stringBuilder.append(s.charAt(i));
            stringBuilder.append("#");
        }
        return stringBuilder.toString();
    }

    public static String manacher(String s) {
        String str = expand(s);
        int p[] = new int[str.length()];
        int mx = 0;
        int id = 0;
        int maxLen = 0;
        int start = 0;
        for (int i = 1; i < str.length(); i++) {
            if (i < mx) {
                p[i] = Math.min(p[2 * id - i], mx - i);
            } else {
                p[i] = 1;
            }
            while ((i - p[i]) > 0 && (i + p[i]) < str.length() && str.charAt(i - p[i]) == str.charAt(i + p[i])) {
                p[i]++;
            }
            if (mx < (i + p[i])) {
                id = i;
                mx = i + p[i];
            }
            if (maxLen < (p[i] - 1)) {
                maxLen = p[i] - 1;
                start = (id - maxLen) / 2;
            }
        }
        return s.substring(start, maxLen);

    }

    public static void main(String[] args) {
        String str = "abbacc";
        System.out.println(manacher(str));
    }
}

你可能感兴趣的:(算法)