清晰易懂地介绍KMP算法

本文清晰易懂地介绍了KMP算法,通过分析KMP算法的创新之处——避免不必要的回退,提高字符串匹配效率。详细讲解了如何从前缀和后缀角度理解KMP算法,并通过LPS数组的构建及应用,展示KMP算法的工作原理。最后,通过实例对比暴力匹配,揭示KMP算法的时间复杂度为O(m+n)的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


清晰易懂地介绍KMP算法

一个简单的假设

例子:
正文: …
模式字符串: B A A A A
其中,正文长度为N,模式字符串长度为M,一般来说N>>M。
我们先来做一个假设,假设正文中仅有A和B两个字符组成。那么如果第五个字符匹配失败,可知正文对应部分肯定为B A A A B。那么此时,我们没有必要去回退文本指针i,因为正文对应部分的第2-4个字符均为A,都和模式字符串的第一个字符B不相匹配。这时我们可以直接将i加1,以比较文本的下一字符和模式字符串中的第二个字符。这样我们最多仅仅会进行N次字符串比较。这样我们最多仅仅会进行N次字符串比较。上述情况是很特殊的,但其思想是值得思考的,那么我们可以将这种思想抽象化以使得其可以适用于所有情况吗?
暴力求解法如图所示:
暴力求解法

可以看到,上述暴力算法中,很多指针i的移动时没有必要的。为了提升字符串匹配算法的性能,我们将介绍一个经典的且令人印象深刻的算法:Knuth-Morris-Pratt算法

KMP算法的创新

在介绍KMP算法的具体实现之前,先来介绍KMP算法的创新之处:
当出现不匹配时,就能知晓一部分文本的内容,可以利用这些信息避免讲指针回退到所有这些已知的字符之前。
此算法的通用性令人印象深刻:在匹配失败时从能将模式字符串指针j设置为某个值以使文本指针i不回退

(if the beginning part of a string is appearing again somewehere else in the string then don’t again compare the characters right avoid the comparison and don’t move i back again like it was happening in basic algorithm.)

详细介绍KMP算法

下面,我们会从前缀后缀的角度来详细讲解KMP算法。

对于一个模式字符串“ abcdabc ”来说,其前缀和后缀分别为:
prefix:a,ab,abc,abcd,abcda,abcdab
suffix:c,bc,abc,dabc,cdabc,bcdabc
对于这个字符串来说,其前缀和后缀中都有“abc”,即“abc”这个子字符串重复出现了。
为了记录这种重复性,生成一个数组。对于这个数组,我们称之为LPS(longest prefix which is same as some suffix)
下面举几个例子来说明LPS中的值怎么根据模式字符串来生成。

  1. a b c d a b e a b f
       0 0 0 0 1 2 0 1 2 0(LPS)
  2. a b c d e a b f a b c
        0 0 0 0 0 1 2 0 1 2 3(LPS)

在得到了LPS数组后,KMP算法的后续工作也就比较简单了。下面我们将从一个例子入手,去介绍剩下的过程。

tex: a b a b c a b c a b a b a b d

pattern: a b a b d
根据模式字符串pattern,我们得到LPS数组为:-1 0 0 1 2 0 (在最前面设为-1是为了编程方便,也可以不加)
设指向tex的指针为i,指向pattern的指针为j。
当i指向5,j指向5的时候,字符不匹配,根据LPS数组来回退j。此时LPS的第j个值为2,所以j回退到2,i不变,继续比较。字符仍不匹配,j回退到0,i不变,继续比较。重复上述过程,即可得到最后的匹配结果。

具体的代码实现:

    private int[] getNext(String pattern) {
        int j = 0;
        int k = -1;
        int len = pattern.length();
        int []next = new int[len+1];
        next[0] = -1;

        while (j < len - 1) {
            if (k == -1 || pattern.charAt(k) == pattern.charAt(j)) {
                ++k;
                ++j;
                next[j] = k;
            } else {
                k = next[k];
            }
        }
        return next;
    }
    private int kmp(String tex, String pattern) {
        int tlen = tex.length();
        int plen = pattern.length();
        int []next = getNext(pattern);
        int i=0,j=0;
        while(i<tlen && j<plen){
            if(tex.charAt(i)==pattern.charAt(j)){
                ++i;
                ++j;
            } else{
                if(next[j]==-1){
                    ++i;
                    j=0;
                }else {
                    j = next[j];
                }
            }
            if(j==plen) return i-j;
        }
        return -1;
    }

暴力匹配的代码实现:


	int strStr1(String haystack, String needle) {
        char []arrays = haystack.toCharArray();
        char []mapp = needle.toCharArray();
        if(mapp.length==0 && arrays.length==0)
            return 0;
        if(mapp.length==0)
            return 0;
        if(arrays.length==0 || arrays.length<mapp.length)
            return -1;
        int flag = 0;
        for(int i=0;i<arrays.length;i++){
            if(arrays[i]==mapp[0]){
                flag = 0;
                int k = i;
                for(int j=0;j<mapp.length;j++){
                    if(k<arrays.length && arrays[k]==mapp[j] ){
                        ++k;
                        ++flag;
                    }
                    else{
                        break;
                    }
                }
                if(flag == mapp.length){
                    return i;
                }
            }
        }
        return -1;
    }

时间复杂度:
KMP为O(m+n)
暴力匹配为O(mn)
其中,m为tex长度,n为pattern长度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值