KMP算法解析

KMP算法

背景
在刷leetcode时,遇到了这样一个问题,给定两个字符串A和B,问你在A中是否有字符串B,并给出A中B出现的起始位置。
拿到这道题,首先想到的是使用暴力搜索的方法,直接在A中去搜索B,这种方法太暴力,当出现不匹配字符时,遍历A和B的指针需要同时回退,时间复杂度高,为O(m*n),其中m,n分别是字符串A和B的长度。而且,当字符串中重复的元素比较多时,这种解法会有很多不必要的操作。
暴力搜索的代码

int search(string A, string B) {
        if(B.empty())
            return 0;
        
        int i = 0,j = 0;
        int sizeA = A.size();
        int sizeB = B.size();
        while(i < sizeA && j < sizeB)
        {
            if(A[i] == B[j])
            {
                i++;
                j++;
            }
            else //只要遇到不匹配字符,两个指针都要回退
            {
                i = i - j + 1;
                j = 0;
            }
        }
        if(j == sizeB)
            return i - j;
        
        return -1;
    }

我们发现这样一位一位的匹配很浪费,没有利用到已经进行匹配的有用的信息。
KMP算法的不同之处在于它会花费一部分空间来记录一些信息。

介绍
KMP算法是一个改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,KMP算法的核心是尽量使用匹配失败后的信息,尽量减少模式串与主串的匹配次数来达到快速匹配的目的。

两个重要的概念

前缀:
一个字符串的前缀是指除最后一个字符以外,这个字符串的全部头部字符组成;
例如,字符串banana的前缀有b,ba,ban,bana,banan

后缀:
同理,一个字符串的后缀指除第一个字符以外,这个字符串的全部尾部字符组成;
例如,字符串banana的后缀有a,na,ana,nana,anana

next数组的解法

KMP算法中一个重要的步骤就是对模式串构建next数组,next数组的长度为模式串的长度,
(这里约定数组的下标i表示对模式串中前i个字符构成的子串进行计算的结果值),这个结果值是子串的相同前缀和后缀的最长长度。
例如,模式串为abcdabd,当字串为abcda时,其前缀组合为a,ab,abc,abcd,后缀组合为a,da,cda,bcda,相同的前缀后缀为a,其长度为1。

这里,演示一下,计算next前几个值的步骤(模式串为abcdabd):

当i = 0时,子串为空,为无前缀和后缀,next[0] = -1;
当i = 1时,子串为a,前缀和后缀均为空集,next[1] = 0;
当i = 2时,子串为ab,前缀为a,后缀为b,没有交集,next[2] = 0;
.
.
.
当i = 6时,子串为abcdab,前缀为a,ab,abc,abcd,abcda,后缀为b,ab,dab,cdab,bcdab,组合的交集为ab,长度为2,故next[6] = 2;
因此,next= {-1,0,0,0,0,1,2}

next数组的运用

这里的next数组就是事先对模式串计算的内部匹配信息,这个匹配信息可以在字符匹配失败时,对模式串进行最大的移动,以减少如暴力匹配中的那么多的匹配次数。
比如,在一次匹配失败后,我们会想将模式串尽量的右移和主串进行匹配,对模式串右移的距离计算方式为,在已经匹配的模式串子串中,找出最长的相同的前缀和后缀,然后移动使他们重叠。
例如:
主串A为 abcdababcdabde
模式串B为 abcdabd

匹配的过程如下图:
在这里插入图片描述
KMP算法每次都是在匹配失败的地方,根据已经事先计算好的内部匹配信息,来移动模式串,使匹配过程从匹配失败的地方继续进行匹配,从而减少了匹配的次数。
算法时间复杂度为O(m+n),事先计算内部匹配信息需要花费O(n),字符串匹配时需要花费O(min(m,n));
空间复杂度为O(n),需要一部分空间来存储对模式串计算的内部匹配信息。

实现代码如下:

vector<int> getNext(string str)
    {
        vector<int> next;
        int len = str.size();
        next.push_back(-1);//next数组的初始值设置为-1,第一个元素没有前缀
        
        int i = 0;
        int k = -1;
        while (i < len -1)
        {
            if (k == -1 || str[i] == str[k])//str[i]表示后缀,str[k]表示前缀
            {
                k++;
                i++;
                next.push_back(k);
            }
            else
            {
                k = next[k];
            }
        }
        return next;
    }
    int search(string haystack, string needle) {
        //方法2,使用KMP算法
        if (needle.empty())
        {
            return 0;
        }
        
        int size1 = haystack.size();
        int size2 = needle.size();
        int i = 0;
        int j = 0;
        vector<int> next;
        next = getNext(needle);

        while (i < size1 && j < size2)
        {
            if (j == -1 || haystack[i] == needle[j])
            {
                i++;
                j++;
            }
            else
            {
                j = next[j];//当当前字符不匹配时,匹配字符串需要重新定位下一个匹配字符的位置
            }
        }
        
        if (j == size2)//若匹配循环退出且匹配的字符串达到末尾,此时匹配成功
        {
            return i - j;
        }       
        return -1;//表明未匹配成功
    }

另外可以看另一篇将KMP算法的文章,容易看懂

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值