前缀函数和`KMP`

764 阅读4分钟

前置知识

  • 前缀 是指从串首开始到某个位置 结束的一个特殊子串。字符串S的以i为结尾的前缀表示为 Prefix(S,i),也就是 Prefix(S,i)。

    举例:

  • 真前缀 指除了S本身的S的前缀。

    举例来说,字符串 abcabcd 的所有前缀为 {a, ab, abc, abca, abcab, abcabc, abcabcd}, 而它的真前缀为 {a, ab, abc, abca, abcab, abcabc}

  • 子串

    字符串S的 子串S[i...j]i<j表示 串中从i到j这一段,也就是顺次排列S[i],S[i+1],...S[j]形成的字符串。

    有时也会用 S[i...j],i>j来表示空串。

  • 前缀函数的定义

    给定一个长度为 的字符串 ,其 前缀函数 被定义为一个长度为n的数组 Xn。 其中 的定义是:

    1. 如果子串s[0...i]有一对相等的真前缀与真后缀:s[0...k-1]和s[i-k+1...i],那么 就是这个相等的真前缀(或者真后缀,因为它们相等)的长度,也就是Xn=[k];
    2. 如果不止有一对相等的,那么 就是其中最长的那一对的长度;
    3. 如果没有相等的,那么 。

    简单来说 就是,子串 最长的相等的真前缀与真后缀的长度。

    用数学语言描述如下:

    特别地,规定 Xn[0] = 0(此处忽略了空串,在上述的字串定义中将空串规避掉了)。

    举例来说,对于字符串 abcabcd

    因为 a 没有真前缀和真后缀,根据规定为 0

    因为 ab 无相等的真前缀和真后缀

    因为 abc 无相等的真前缀和真后缀

    因为 abca 只有一对相等的真前缀和真后缀:a,长度为 1

    因为 abcab 相等的真前缀和真后缀只有 ab,长度为 2

    因为 abcabc 相等的真前缀和真后缀只有 abc,长度为 3

    因为 abcabcd 无相等的真前缀和真后缀,

    同理可以计算字符串 aabaaab 的前缀函数为[0,1,0,1,2,2,3]。

计算字符串的前缀函数

  • 求取前缀函数的朴素算法
package com.algorithms.other;

import java.util.Arrays;

//求取前缀函数
//复杂度为O(n^3)
public class PrefixFun {
    public static void main(String[] args) {
        int[] res =  PrefixFun.prefixFun("aabaaab");
        Arrays.stream(res).forEach(System.out::println);
//        0 1 0 1 2 2 3
    }
    /**
     * 求取前缀函数
     * @param str
     */
    static int[] prefixFun(String str){

       int len = str.length();
       int[] result = new int[len];
       //最外层循环用于遍历字串
       //字串s[1]
       //字串s[1,2]
       //字串s[1,2,3]
       //...
        result[0] = 0;
       for(int i = 1;i<len;i++){
        //内层循环用于寻找字串的最长公共字串
           for(int j = i;j>=0;j--){
               if(str.substring(0,j).equals(str.substring(i-j+1,i+1))){
                   result[i] = j;
                   break;
               }
           }
       }
       return result;
    }
}

这个算法的复杂度为O(n^3)

  • 优化方向1

    减枝:相邻两个前缀函数的值要么加1要么不变要么减少

-  for(int j = i;j>=0;j--){
 + for(int j = result[i-1]+1;j>=0;j--){

KMP算法

求取前缀函数的方法为

package com.algorithms.string;

import java.util.Arrays;

//求取前缀函数
//复杂度为O(n^3)
public class PrefixFun {
    public static void main(String[] args) {
        int[] res =  PrefixFun.prefixFunOn("ababcabcacbab");

        Arrays.stream(res).forEach(System.out::println);
//        0 1 0 1 2 2 3
    }
    /**
     * 暴力求取前缀函数(复杂度为On3,后期改进后复杂度为On2)
     * @param str
     */
    static int[] prefixFunOn3(String str){
       int len = str.length();
       int[] result = new int[len];
       //最外层循环用于遍历字串
       //字串s[1]
       //字串s[1,2]
       //字串s[1,2,3]
       //...
        result[0] = 0;
       for(int i = 1;i<len;i++){
        //内层循环用于寻找字串的最长公共字串
    //           for(int j = i;j>=0;j--){
               for(int j = result[i-1]+1;j>=0;j--){
               if(str.substring(0,j).equals(str.substring(i-j+1,i+1))){
                   result[i] = j;
                   break;
               }
           }
       }
       return result;
    }

    /**
     * 复杂度为o(n)的前缀函数算法//
     * @param str
     * @return
     */
    static int[] prefixFunOn(String str){
        int len = str.length();
        //pr
        int[] prefixArr = new int[len];
        //按照惯例让result[0] = 0
        prefixArr[0] = 0;
        for(int i = 1;i<len;i++){
            //得到转移方程为第

            int j = prefixArr[i-1];

            //如果prefix[i-1]与新加入相邻的字符串相匹配的话,则prefix[i]的值将直接为prefix[i]+1
            //否则的话我们通过前缀数组做二次匹配
            //我们不断去寻找这个条件

            while(j>0&&str.charAt(j)!=str.charAt(i)){
                j = prefixArr[j-1];
            }
            //如果最终没有找到的话则j必为0
            //找到了的话则将结果加1
            if(str.charAt(i)==str.charAt(j)){
                j++;
            }
            prefixArr[i] = j;
        }


        return prefixArr;
    }
}

采用双指针方法去对字符串进行搜索,

“KMP”

整个流程为:

具体代码为

package com.algorithms.string;

public class Kmp {

    public static void main(String[] args) {
        System.out.println( Kmp.KmpSolution("adsdbbabb", "dbba"));
    }

    /**
     * 字符串匹配问题
     * @param str 字符串
     * @param pattern 匹配串
     */
    static int KmpSolution(String str,String pattern){
        int[] prefix = PrefixFun.prefixFunOn(str);
        int i=0,j=0;
        while (i<str.length()&&j<pattern.length()){
            //串和匹配串字符相同两者指针都向前移动
            if(str.charAt(i)==pattern.charAt(j)){
                //如果j到达最后一个下标的话直接返回
                if(j==pattern.length()-1){
                    return i-j;
                }
                i++;
                j++;
            }

            //让j进行回跳,查看前一个位置的最小前缀函数,回跳至这个位置的下一个位置继续进行比较
           else if(j==0||prefix[j-1]==0){
               i++;
            }
           else {
               j = prefix[j-1];
            }
        }
        return -1;
    }
}

参考

前缀函数与 KMP 算法 - OI Wiki (oi-wiki.org)