对KMP算法的理解

背景

KMP算法看了很多回,但是每次回忆起来都有一些关键点感觉理解的很模糊,这次仔细研究了一下KMP,给出一个自己的理解。

构造next数组

1 构造数组,使得该数组满足i>0时, [ P 0 , P next i ) = [ P i − next i , P i ) [P_0, P_{\text{next}_i})=[P_{i-\text{next}_i},P_i) [P0,Pnexti)=[Pinexti,Pi),我们称其为一个不完全next数组,构造方法如下:

因为对任意nexti(i>0)有,故一个全零数组为不完全next数组。

  1. i=0的情况另行讨论
  2. [ P 0 P_0 P0, P next i P_{\text{next}_i} Pnexti)表示P的子串,前闭后开区间

现在我们来优化next数组,使数组内元素尽可能大。

P next i + 1 = P i + 1 P_{\text{next}_i + 1} = P_{i + 1} Pnexti+1=Pi+1, 对于 P next i P_{\text{next}_i} Pnexti [ P 0 , P next i ) = [ P i − next i , P i ) [P_0, P_{\text{next}_i})=[P_{i-\text{next}_i},P_i) [P0,Pnexti)=[Pinexti,Pi)

[ P 0 , P next i + 1 ) = [ P i − next i , P i + 1 ) [P_0, P_{\text{next}_i + 1})=[P_{i-\text{next}_i},P_{i + 1}) [P0,Pnexti+1)=[Pinexti,Pi+1)

j = i + 1 j=i+1 j=i+1, k= next i + 1 \text{next}_i+1 nexti+1,代入得 [ P 0 , P k ) = [ P j − k , P j ) [P_0, P_k)=[P_{j-k},P_j) [P0,Pk)=[Pjk,Pj) next j = k \text{next}_j=k nextj=k, 元素代回后有 next i + 1 = next i + 1 \text{next}_{i+1}=\text{next}_i+1 nexti+1=nexti+1

所以next数组的求法为,初始化一个全零数组,从1开始遍历,若 P next i + 1 = P i + 1 P_{\text{next}_i + 1} = P_{i + 1} Pnexti+1=Pi+1,则 next i + 1 = next i + 1 \text{next}_{i+1}=\text{next}_i+1 nexti+1=nexti+1;

 private static int[] getNext(String pattern){
    int[] next = new int[pattern.length()];

    for (int i = 2; i < pattern.length(); i++) {
        if(pattern.charAt(next[i-1]) == pattern.charAt(i-1)){
            next[i] = next[i-1]+1;
        }
    }

    next[0]=-1;

    return next;
}

可以用反证法证明这样求出的数组是最大的不完全next数组,因为这个数组是最大的,所以对于任意 next i < k < i \text{next}_i<k<i nexti<k<i, 有 [ P 0 , P k ) ≠ [ P i − k , P i ) [P_0,P_k)\ne [P_{i-k},P_i) [P0,Pk)=[Pik,Pi) ,我们称这个最大的不完全next数组为next数组

证明KMP

设指针j、i分别指向主串和模式串中正在匹配的字符,则 S j − i = P 0 S_{j-i}=P_0 Sji=P0 是第一个匹配的字符,匹配过程中需要一直满足 [ S j − i , S j ) = [ P 0 , P i ) [S_{j-i},S_j)=[P_0,P_i) [Sji,Sj)=[P0,Pi) , 若

  1. S j = P i S_j=P_i Sj=Pi ,易得 [ S j − i , S j + 1 ) = [ P 0 , P i + 1 ) [S_{j-i},S_{j+1})=[P_0,P_{i+1}) [Sji,Sj+1)=[P0,Pi+1)
  2. S j ≠ P i S_j\ne P_i Sj=Pi, 则令 j = j − i + 1 , i = 0 j=j-i+1,i=0 j=ji+1,i=0 ; 有 [ S j − i + 1 , S j − i + 1 = 0 ) = [ P 0 , P 0 ) [S_{j-i+1},S_{j-i+1=0})=[P_0,P_0) [Sji+1,Sji+1=0)=[P0,P0) ,遍历 S j − i + 1 = P 0 S_{j-i+1}=P_0 Sji+1=P0 是第1个匹配的字符的情况

此时为暴力匹配算法

 public static int bfFind(String S, String P) {
    char[] arr1 = S.toCharArray();
    char[] arr2 = P.toCharArray();
    int i = 0;
    int j = 0;
    while(j < arr1.length && i < arr2.length) {
        // kmp这里要补上arr1[j] != arr2[0]的情况
        // if(i==0 && arr1[j] == arr2[i]){j=j+1;continue;}
        if(arr1[j] == arr2[i]) {
            j++;
            i++;
        }
        else {
            j = j - i + 1;
            i = 0;
            // kmp, 这里替换成 j=j; i=next[i];
        }
    }
    if(i == arr2.length) return j - i;
    else return -1;
}

如果 S j + i ≠ P i S_{j+i} \ne P_{i} Sj+i=Pi, 因为 [ S j − i , S j ) = [ P 0 , P i ) [S_{j-i},S_j)=[P_0,P_{i}) [Sji,Sj)=[P0,Pi),所以 [ S j − next i , S j ) = [ P i − next i , P i ) [S_{j-\text{next}_i},S_j)=[P_{i-\text{next}_i},P_i) [Sjnexti,Sj)=[Pinexti,Pi),又根据next数组的定义有 [ P i − next i , P i ) = [ P 0 , P next i ) [P_{i-\text{next}_i},P_i)=[P_0,P_{\text{next}_i}) [Pinexti,Pi)=[P0,Pnexti),所以 [ S j − next i , S j ) = [ P 0 , P next i ) [S_{j-\text{next}_i},S_j)=[P_0,P_{\text{next}_i}) [Sjnexti,Sj)=[P0,Pnexti)

又因为 对于任意$\text{next}_i<k<i, 有 [ P 0 , P k ) ≠ [ P i − k , P i ) [P_0,P_k)\ne [P_{i-k},P_i) [P0,Pk)=[Pik,Pi),即 [ S j − k , S j ) ≠ [ P 0 , P k ) [S_{j-k},S_j)\ne[P_0,P_k) [Sjk,Sj)=[P0,Pk) ,所以可以跳过第一个匹配字符在 [ S j − i , S j − next i ) [S_{j-i}, S_{j-\text{next}_i}) [Sji,Sjnexti) 中的情况

next[0] = -1 的情况

S j ≠ P 0 S_j\ne P_0 Sj=P0时,有j=j+1, i=i+1=0,遍历下一种情况,这样就不用单独处理 S j ≠ P 0 S_j\ne P_0 Sj=P0的情况了,贴一下优化后的代码。

 private static int[] getNext(String pattern) {
    //获取两串的字符数组,以便遍历
    char patternOfChars[] = pattern.toCharArray();
    //创建next数组
    int[] next = new int[pattern.length()];
    int nextValue = -1, loopOfPattern = 0;//初始化next值及模式串下标
    next[0] = -1;//这里采用-1做标识
    while(loopOfPattern < pattern.length() -1)
    {
        //获取next数组
        if(nextValue == -1 || patternOfChars[loopOfPattern] == patternOfChars[nextValue])
        {
            nextValue++;
            loopOfPattern++;
            next[loopOfPattern] = nextValue;
        } else {
            nextValue = next[nextValue];
        }
    }
    return next;
}

public static int kmp(String str, String pattern) {
    //如果主串长度不小于模式串,则进入模式匹配
    if (str.length() >= pattern.length()) {
        //获取next数组
        int next[] = getNext(pattern);

        //获取两串的字符数组,以便遍历
        char strOfChars[] = str.toCharArray();
        char patternOfChars[] = pattern.toCharArray();

        //两个循环控制变量
        int loopOfStr, loopOfPattern;
        //遍历主串,任意一串遍历结束,则匹配结束
        for (loopOfStr = 0, loopOfPattern = 0 ; loopOfStr < str.length() && loopOfPattern < pattern.length() ;) {
            //如果两元素相同,或模式串全部匹配失败,比较下一个元素
            if (loopOfPattern == -1 || strOfChars[loopOfStr] == patternOfChars[loopOfPattern]) {
                loopOfStr++;
                loopOfPattern++;
            } else {
                loopOfPattern = next[loopOfPattern];//模式串下标置为next值
            }
        }

        //模式串匹配结束,表示匹配成功
        if (loopOfPattern == pattern.length()) {
            return loopOfStr - loopOfPattern;//主串中模式串第一次出现的位置
        }
    }

    //模式匹配失败
    return -1;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值