字符串匹配KMP算法

目录

1,什么是字符串匹配

1.1,蛮力法匹配

1.2,蛮力法匹配过程

2,KMP算法

2.1,KMP算法的过程描述

2.2,KMP算法的过程

2.3,寻找最长前缀后缀

2.4,基于最大长度表的匹配

2.5,根据最大长度表求next数组

2.6,通过next数组匹配

3,代码实现

3.1,测试代码


1,什么是字符串匹配

假设现在随机输入一个长度为m的主串T,另外输入一个长度为n(n≤m)的字符串P,我们来判断字符串P是否是主串T的一个子串(即能否从T中随机取出与P同长的一段字符串,与P完全匹配)。这就是字符串问题的简单描述。

1.1,蛮力法匹配

蛮力法的思想:假设我们把子串和主串从第一个位置开始逐个字符进行匹配,如果相匹配的字符发现相等,也就是P[i]=T[j],那么我们就另i++,j++,逐个字符向后面进行匹配,但是如果P[i]!=T[j]的话,我们就重新设置j=0,i=i-j+1,重新开始匹配,也就是说,蛮力法解决的话如果发现字符串不匹配的话,那么主串每一次前进的位置是1,这就是蛮力法解决字符匹配的思想,顺着思路,我们很容易写出蛮力法解决的代码 。

public class ViolenceMatchDemo {
    public static void main(String[] args) {
        int index=violenceMatch("abdxhuncdsuhcd","hun");
        System.out.println(index);
    }

    public static int violenceMatch(String str1,String str2){
        char []s1=str1.toCharArray();
        char []s2=str2.toCharArray();
        int s1Len=s1.length;
        int s2Len=s2.length;
        int i=0;
        int j=0;
        while (i<s1Len && j<s2Len){
            if(s1[i] ==s2[j]){
                i++;
                j++;
            }else {
//从这里我们可以知道,如果没有匹配,那么每次前进的位置是1
                i=i-j+1;
                j=0;
            }
        }
//        判断是否匹配成功
        if(j == s2Len)
        {
            return i-j;
        }
        return -1;
    }
}

1.2,蛮力法匹配过程

下面我们以主串:BBCABCDAB ABCDABCDABDE,子串:ABCDABD,来看一下匹配过程,有助于我们下面理解KMP算法。

  • 第一次匹配:第一次匹配时,T[0]=B!=P[0]=A,所以,此时另i=i-j+1=1,j=0,也就是i指向主串的第一个位置,j指向子串的第二个位置。再说明白点就是此时子串和主串的下一个字符进行匹配。

  • 第二次匹配:此时T[1]=B!=P[0]=A,所以第二次还是没有匹配到,那么此时i=i-j+1=2,j=0,也就是说子串和主串的下一个字符接着进行匹配。

  • 第三次匹配:第三次匹配T[2]=C!=P[0]=A,所以第三次还是没有匹配成功,接着i=i-j+1=3,j=0,。

  • 第四次匹配:在第四次匹配,T[3]==P[0]=A,我们发现匹配成功一个字符,那么接着我们就让i++,j++,也即是说接着匹配下一个字符。

  • 第五次匹配:T[4]==P[1],第四次成功匹配,所以让i++,j++,此时i=5,j=2。接着进行下一次匹配。

  • 第六次匹配:T[5]==P[2],第四次成功匹配,所以让i++,j++,此时i=6,j=3。接着进行下一次匹配。

  • 第七次匹配:T[6]==P[3],第四次成功匹配,所以让i++,j++,此时i=7,j=4。接着进行下一次匹配。

  • 第八次匹配:T[7]==P[4],第四次成功匹配,所以让i++,j++,此时i=8,j=5。接着进行下一次匹配。

  • 第九次匹配:T[8]==P[5],第四次成功匹配,所以让i++,j++,此时i=9,j=6。接着进行下一次匹配。

  • 第十次匹配:T[9]=‘ ’!=P[6]=D,第四次不成功匹配,所以此时我们需要重新设置i和j的值,也就是令j=0,i=i-j+1=4,也就是说子串重新从主串的第4个字符开始和主串进行匹配,切记注意,这里是关键,我们在进行前3此匹配的过程中,因为子串和主串的第一个字符就发生不匹配,这种情况自然就是子串往后面移动一位,然后重新和子串进行匹配,但是在第四次的匹配过程中,发生了前6个字符都匹配成功的情况,但是只有最后一位没有匹配成功,按照暴力法的思想,那我们下次重新匹配时,需要从主串i=4开始重新和子串进行匹配,也就是主串中的B字符开始匹配。好了这里先不说太多,我们接着往下面匹配。

下面的过程和上面的匹配过程是一样的,基本就是这个思路,所以在这里我就直接给出匹配成功后的结果。

好了,暴力法最终匹配成功,接下来我们今天的主角登场,大名鼎鼎的KMP算法。

2,KMP算法

2.1,KMP算法的过程描述

假设现在主串T匹配到 i 位置,模式串P匹配到 j 位置 如果j = -1,或者当前字符匹配成功(即T[i] == P[j]),都令i++,j++,继续匹配下一个字符;如果j != -1,且当前字符匹配失败(即T[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失败时,模式串P相对于文本串T向右移动了j - next [j] 位。 换言之,当匹配失败时,模式串向右移动的位数为:失败字符所在位置 - 失败字符对应的next 值(next 数组的求解会在下文阐述),即移动的实际位数为:j - next[j],且此值大于等于1。 很快,你也会意识到next 数组各值的含义:若k=next[j],代表模式串P中当前字符之前的字符串中,最前面的k个字符和j之前的最后k个字符是一样的。

如果使用数学公式描述就是:P[0 ~ k-1] == P[j-k ~ j-1]。

此也意味着在某个字符匹配失败时,该字符对应的next 值会告诉你下一步匹配中,模式串应该跳到哪个位置(跳到next [j] 的位置)。如果next [j] 等于0或-1,则跳到模式串的开头字符,若next [j] = k 且 k > 0,代表下次匹配跳到j 之前的某个字符,而不是跳到开头,且具体跳过了k 个字符。

上面的暴力匹配过程中,在第十次匹配中,只有最后一个字符D没有匹配成功,而我们暴力法的解决方案是重新设置i=i-j+1,j=0,也就是子串仅仅进了一个位置,重新开始和主串匹配,现在我们来看看使用KMP算法的话,子串会前进到那个位置和主串进行匹配。当j=9,i=6的时候匹配失败,此时如果按照KMP算法,则令j不变,i = next[i]”,即i 从6变到2(后面我们将求得P[6],即字符D对应的next 值为2),所以相当于模式串向右移动的位数为i - next[i](i - next[i] =6-2 = 4),如果现在我们让子串向后移动4个位置在重新匹配的话:就是下面这个样子的。

从上面可以看到,子串的的AB直接省去了和主串BBCA后面的BCD进行比较的过程。因为即使进行比较,也是匹配不成功的,直接向后面移动了4位,为什么需要移动4位,因为移动4位,在主串中又有字符AB,可以继续跟T[7]T[8]对应着,从而不用让i 回溯。相当于在除去字符D的模式串子串中寻找相同的前缀和后缀,然后根据前缀后缀求出next 数组,最后基于next 数组进行匹配。KMP算法的核心就是在这里,每次匹配不成功后,可以直接向前面跳若干个位置,因为那几个位置即使进行比较的话,也是匹配不成功的,这就是和蛮力法主要的区别之处。

2.2,KMP算法的过程

  • 第一步:寻找模式串的每个子串前缀和后缀最长公共元素长度 
    • 对于P = p0 p1 ...pj-1 pj,寻找模式串P中长度最大且相等的前缀和后缀。如果存在p0 p1 ...pk-1 pk = pj- k pj-k+1...pj-1 pj,那么在包含pj的模式串中有最大长度为k+1的相同前缀后缀。举个例子,如果给定的模式串为“abab”,那么它的各个子串的前缀后缀的公共元素的最大长度如下表格所示:

现在来解释一下:对于模式串abab,当有一个字符a的时候,规定其前缀和后缀最大的公共元素长度是0,当有两个字符ab时候,其前缀是a,后缀是b,很明显是没有公共的字符,所以是0,这里需要注意一点,不能把ab也算作是前缀和后缀。当有3个字符aba的时候,前缀是a,ab,后缀是a,ba,很明显,公共字符最大的长度是0,也就是字符a,当有四个字符abab时候,前缀是:a,ab,aba,后缀是:b,ab,bab,所以最大公共元素的长度是2。

  • 第二步:求next数组

next 数组考虑的是除当前字符外的最长相同前缀后缀,所以通过第一步骤求得各个前缀后缀的公共元素的最大长度后,只要稍作变形即可:将第一步骤中求得的值整体右移一位,然后初值赋为-1,如下表格所示:

比如对于aba来说,第3个字符a之前的字符串ab中有长度为0的相同前缀后缀,所以第3个字符a对应的next值为0;而对于abab来说,第4个字符b之前的字符串aba中有长度为1的相同前缀后缀a,所以第4个字符b对应的next值为1(相同前缀后缀的长度为k,k = 1)。

  • 第三步:根据next数组进行匹配

若匹配失配,j = next [j],模式串向右移动的位数为:j - next[j]。换言之,当模式串的后缀pj-k pj-k+1, ..., pj-1 跟文本串si-k si-k+1, ..., si-1匹配成功,但pj 跟si匹配失败时,因为next[j] = k,相当于在不包含p[j]的模式串中有最大长度为k 的相同前缀和后缀,即p0 p1 ...pk-1 = pj-k pj-k+1...pj-1,故令j = next[j],从而让模式串右移j - next[j] 位,使得模式串的前缀p0 p1, ..., pk-1对应着文本串 si-k si-k+1, ..., si-1,而后让pk 跟si 继续匹配。

综上,KMP的next 数组相当于告诉我们:当模式串中的某个字符跟文本串中的某个字符匹配失配时,模式串下一步应该跳到哪个位置。如模式串中在j 处的字符跟文本串在i 处的字符匹配失配时,下一步用next [j] 处的字符继续跟文本串i 处的字符匹配,相当于模式串向右移动 j - next[j] 位。

2.3,寻找最长前缀后缀

使用我们的子串ABCDABD,给出最长前缀和后缀长度。

也就是说,原模式串子串对应的各个前缀后缀的公共元素的最大长度表为:

2.4,基于最大长度表的匹配

因为模式串的子串中首尾可能会有重复的字符,故可得出下述结论:匹配失败时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的最大长度值

  • 第一次匹配失败:因为主串和子串在匹配前三个字符的时候就没有匹配成功,所以直接让子串和主串的第四个字符开始匹配。主串在i=9,子串在j=6的时候匹配失败。根据KMP算法,子串已经匹配6个字符,匹配失败的字符是D,上一位字符B在最大长度表中对应的数字为2,所以6-2=4,也就是让子串前进4位继续和主串匹配。

  • 前进4位匹配结果:子串在第三个字符处匹配失败,此时已经成功匹配2个字符,匹配失败的字符C对应的上一个字符B在最大长度表中对应值是0,所以2-0=0,也就是此时子串需要向前移动两个位置。

  • 前进两位匹配结果:匹配失败,子串前进一位。

  • 前进一位匹配结果:此时我们发现子串在匹配第七位的时候失败,前面已经匹配成功6个字符,匹配失败字符D的前一个字符是B,在最大长度表中对应的数字是2,所以6-2=4,也就是说子串需要前进4个位置。

  • 前进4个位置匹配结果:最终匹配成功。

 

2.5,根据最大长度表求next数组

由上文,我们已经知道,字符串“ABCDABD”各个前缀后缀的最大公共元素长度分别为:

  • 根据这个表可以得出下述结论:

      失配时,模式串向右移动的位数为:已匹配字符数- 失配字符的上一位字符所对应的最大长度值

  • 上文利用这个表和结论进行匹配时,我们发现,当匹配到一个字符失配时,其实没必要考虑当前失配的字符,更何况我们每次失配时,都是看的失配字符的上一位字符对应的最大长度值。如此,便引出了next 数组。
  • 给定字符串“ABCDABD”,可求得它的next 数组如下:

  • 把next 数组跟之前求得的最大长度表对比后,不难发现,next 数组相当于“最大长度值” 整体向右移动一位,然后初始值赋为-1。意识到了这一点,你会惊呼原来next 数组的求解竟然如此简单:就是找最大对称长度的前缀后缀,然后整体右移一位,初值赋为-1(当然,你也可以直接计算某个字符对应的next值,就是看这个字符之前的字符串中有多大长度的相同前缀后缀)。

换言之,对于给定的模式串:ABCDABD,它的最大长度表及next 数组分别如下:

根据最大长度表求出了next 数组后,从而有:失配时,模式串向右移动的位数为:失配字符所在位置- 失配字符对应的next 值

2.6,通过next数组匹配

  • 第一次匹配失败:因为前几个字符第一位都不同,所以我直接从第4位开始匹配,在子串的第6个位置匹配失败,所以子串前进的位数为:6-2=4,也就是子串前进4个位置。

  • 第二次匹配:子串在第0个位置就没匹配成功,所以后移一位。

  • 第三次匹配:子串在第6个位置失败,所以移动的位数=6-2=4.

  • 第四次匹配:成功匹配。

3,代码实现

/**
     * kmp算法的实现
     * @param str1 原始字符串
     * @param str2 子串
     * @param next 子串对应的next数组
     * @return 返回匹配成功的位置
     */
    public static int KMP(String str1,String str2,int []next){
        for(int i=0,j=0;i<str1.length();i++){
//            修改j的值
            while (j>0&&str1.charAt(i)!=str2.charAt(j)){
                j=next[j-1];
            }

            if(str1.charAt(i)==str2.charAt(j)){
                j++;
            }
            if(j == str2.length()){
                return i-j+1;
            }
        }
        return -1;
    }

    /**
     * 获取子串的部分匹配表
     * @param dest 目标串
     */
    public static int[] kmpNext(String dest){
//        创建一个数组,存放部分匹配表
        int []next=new int[dest.length()];
    //        字符串长度为1的话,部分匹配值是0
        next[0]=0;
        for(int i=1,j=0;i<dest.length();i++){
    //这是KMP算法的核心点
            while (j>0 && dest.charAt(i) != dest.charAt(j)){
                j=next[j-1];
            }
//            当dest.charAt(i) == dest.charAt(j)时,部分匹配值就是1
            if(dest.charAt(i) == dest.charAt(j)){
                j++;
            }
            next[i]=j;
        }
        return next;
    }

3.1,测试代码

public class KMPDemo {
    public static void main(String[] args) {
        String str="ABAAB";
        int next[]=kmpNext(str);
        System.out.println(Arrays.toString(next));
//        int index=KMP("asdfgbcddfbgds","gbcd",kmpNext("gbcd"));
//        System.out.println(index);
    }
}

关于KMP算法理论并没有写多少,能力有限,还在研究中,大神请移步下面参考的播客,关于理论讲解的很深,哪里有问题还请多多指出。


参考资料:

[1] https://www.cnblogs.com/wkfvawl/p/9768729.html

[2] https://www.cnblogs.com/zhangboy/p/7635627.html

[3] https://www.cnblogs.com/zzuuoo666/p/9028287.html


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值