KMP字符串匹配算法解析

kmp算法为非常经典的字符串匹配算法。下面先讲解下最原始的字符串匹配方法,也就是暴力求解。时间复杂度为O(m*n)
下面举个例子讲解下暴力大法
如  12314123123b   match为123123
首先从第一个位置str[0]开始依次与match开始进行比较
str的第0、1、2、3与match 0、1、2、3匹配,但是4与match[3]不匹配,即str[0]位置出发不与match匹配
于是接下来从str[1]开始于match[0]重新开始匹配。这样的话str每到一个位置都要与match的开始进行匹配。所以str每一个位置的时间复杂度为match.length
所以暴力大法的时间复杂度为 str.length*match.length为N*m

下面讲下kmp提速的方法
例子1
比如  1 4 3 1 4   3 1 2 3 b
                      no
          1 4 3 1 2
下面我们如果从match[1]继续匹配的话将会节省大量时间。
1 4 3 1 4                 3 1 2 3 b
             比对该位置         
         1 4 3 1 2
大家可能会问,kmp为什么会那么做,为什么对呢?等会给大家讲解下。先引入kmp算法中必备的next数组
next数组中i位置存储为0-i-1中前缀子串和后缀子串最大匹配长度。

前缀子串就是 从0开始不包含i-1的串(必须从0开始,长度小于i-2)
后缀子串就是以i-1结尾,不包含0位置的串(必须以i-1结尾,长度为i-2),
首先0位置没用前缀和后缀则为-1,1位置也没有则为0,从2开始。下面举个例子
如  1  2  3  1  2  3 
                         i位置   则前缀子串可以为 1,12,123,1231,12312
                                       后缀子串可以为 3,23,123,3123,23123
next为 -1 0 0 0 1 2
可以看出前缀子串和后缀子串最大匹配长度为3。当然前缀和后缀可以重合 如下面的例子
 1 2 3 1 2 3 1 4
                    i位置  前缀为:1,12,123,1231,12312,123123
                               后缀为:  1,31,231,1231,31231,231231
则最大匹配长度为4
next为 -1 0 0 0 1 2 3 1
有了next的概念之后 讲解下为什么可以提速。也就是为什么例子1中为什么kmp会跳过str[1 2 3]的比对而直接str[4]比对match[1]
这是因为在这个不检查区域中,从任何一个字符出发都 肯定不会匹配出match,为什么呢?下面解释一下。
首先我们必须假设next数组都求的正确,都是各个位置的前缀和后缀子串的最大匹配长度。
下面画图解释一下
    
从str[i]开始到A处与match的B处不匹配(str[i]到A之前与match开始到B之前完全匹配)。这时我们要跳过不检查区域直接将match前移。移动为图3位置继续与图1比较。
跳过不检查区域X是因为从这个不检查区域中,从任何一个字符出发都肯定不会匹配出match。
我们假设在该区域某字符出发匹配到match字符子串d。d肯定比c长,以d为后缀所以对应match的前缀子串区域e,后缀d'。
e,d'与d不光长度相同,同时字符也完全相同。我们可以看出e比a大,也就是比b大。但是match中B的位置存的是其之前前缀与后缀最大匹配长度
但是e居然比a要大,显然不对。所以区域x不可能存在匹配出match串,所以忽略不检查区域x。match之间前移为图3位置。匹配D是否与A相等(a和b为最大匹配的前后缀)。
下面讲解下代码实现。
首先是next数组的获得
public static int[] getNext(char[] match){
        int cn=0,post=0;
        int[] next=new int[match.length];
        next[0]=-1;
        next[1]=0;
        post=2;
        while(post<match.length){
            if(match[post-1]==match[cn]){
                next[post++]=++cn;

            }else if(cn>0){
                cn=next[cn];
            }else next[post++]=0;
        }
        return next;
    }
next第一位没用前缀和后缀,初始化为-1,第二位也没有初始化为0.然后从第三位2开始求。
cn为前移标志位。post指当前位置
当cn前跳到0号位置,说明该post位置的前缀和后缀子串没有匹配,所以该位置赋值为0.
举个例子讲解下的吧
比如      
0 1 2 3 4 5 6 7 8
1 2 3 1 2 3 1 4 3
首先前两位默认-1 和0
然后从post=2,cn=0开始
首先它的前一位不等于match[cn=0],并且cn=0所以next[post]=0,post+1
post=3    match[post-1]!=match[cn=0],并且cn=0,所以next[post=3]=0,post+1
post=4   match[post-1]=match[cn],故next[post]=1(前一个位置的前缀1,后缀1所以最大匹配为1),post+1 cn+1
post=5   match[post-1]=match[cn],故next[post]=2(前一个位置的前缀为12,后缀为12,最大匹配为2。实际上cn已经记录最大匹配长度),post+ cn+1
post=6  match[post-1]=match[cn],故next[post]=3 (前一个位置的前缀为123,后缀为123,最大匹配为3)post+ cn+1
post=7   match[post-1]=match[cn],故next[post]=4 (前一个位置的前缀为1231,后缀为1231,最大匹配为4)post+ cn+1
post=8   match[post-1]!=match[cn],并且cn>0,故cn前移为cn=next[cn]
(前移的这个位置实际是已匹配最长前缀串的缩减, 比如该位置前已经匹配最长前缀实际为1231,在下一个位置元素2不匹配,于是把前缀缩小,缩小到该位置之前元素与post之前位置完全匹配,cn变为next[cn](=1)比对。
有点绕口,实际就是  原本4元素之的前缀1231和后缀的1231匹配了,但是3的后缀12314与前缀的12312不匹配,这时候我们不应该从头开始,而是缩减元素3之前的前缀和后缀子串,缩减为前缀12与后缀14比较,还是不相等于是继续缩减前缀为1后缀为4,仍不相等(但是cn=0了)所以next[post=8]的值为0,意思为该位置之前的前缀和后缀无相同的匹配长度
next为 -1 0 0 0 1 2 3 4 0    
next数组求完,下面就是字符串匹配了,直接上代码
 public static int kmpCom(String s,String m){
    	char[] str=s.toCharArray();
    	char[] match=m.toCharArray();
    	int si=0;
    	int mi=0;
    	int[] next=new int[match.length];
    	next=getNext(match);//首先得到next数组,用上面的函数
    	while(si<str.length&&mi<match.length){
    		if(str[si]==match[mi]){
    			si++;
    			mi++;
    		}else if(next[mi]==-1){
    			si++;
    		}else{
    			mi=next[mi];
    		}
    	}
    	return mi==match.length?si-mi:-1;
    }
si为s源字符串位置标记,mi为match字符串标记
如果str[si]=match[mi],则二者都自加
若不相等并且next[mi]=-1说明该位置为不检查区域。
若不相等并且next[mi]!=-1,这时就是kmp提速的操作如图3所示,match字符串向前跳,继续比对。
kmp算法可以将暴力破解的时间复杂度从N*m降到O(N)

以上个个人的对kmp算法的理解。参考主要看的是牛客网大神左程云老师的书进行的整理。可以推荐大家看下。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值