数据结构和算法 <字符串>(九、字符串匹配 (KMP算法))

终于来到了KMP算法,之前以为字符串匹配算法就一个KMP,没想到原来这么多。不过也看完了,接下来就研究一波KMP算法。之前写过按着王争老师的想法,写了一些,发现有点难懂,然后看了一些网上的资料,感觉网上的讲法更容易懂一下,所以自己总结一波,写了一遍自己理解的KMP。

9.1 引入KMP

现在先不说KMP,我们回忆一下,BF算法(暴力解法)、RK算法、BM算法。

BF算法就是暴力解法,两个for循环,一个遍历主字符串,一个遍历模式串,然后判断是否一样,暴力又简单,想想都觉得好。

我们是觉得简简单单就好,但是前辈们不这么想。(有没有以前上学的时候,学那堆公式一样,就很想。。。,自己想)。经过前辈们励精图治,殚精竭力的去优化这个字符串匹配算法,才有后来的RK算法,BM算法。

RK算法是利用了哈希值来做到了优化,想法也很好,这不是这个重点。

BM算法就比较厉害,想的是怎么去一下子移动很多位,这样就不浪费时间去匹配那些一定不匹配的字符,暴力算法的问题就是一下子就移动一位,所以很费时,BM算法就从这个角度出发,先后出现了坏字符规则和好后缀。忘记说了,BM算法是从字符串后面开始匹配。

坏字符就是这个字符没在模式串中,其中包含这个字符的位置都不会匹配,所以模式串就可以坐上火车一样往坏字符后才开始匹配。如果这个字符在模式中咋办,就能把模式串中的那个字符移动到这个位置上。(详细看以前章节)

好后缀规则其实就是后面几个字符能匹配,但是前面的字符不匹配,所以也要想办法让模式串坐上火车,我们就在想如果好后缀前面的有字符跟好后缀匹配,那么就把这个匹配的移动到好后缀的地方,然后再次匹配。(因为有一个不匹配字符在,可以自己移动试试就知道,包含了这个字符的时候都不会出现)。这个其实就是今天说的KMP算法类似,为了引入这个,感觉水了好多字,好后缀还有其他两种情况,有空可以看看前一章。

主要也是想引入KMP的时候再复习复习。

9.2 KMP原理

上一节水这么多就是想引入,BM算法中有好后缀的情况下,会去找与好后缀匹配的字符,如果有,就可以快速移动。

很不巧,KMP也是这个原理。但是KMP比较直,是从前面往后面匹配的。我们就举个例子好好看看。

在这里插入图片描述

我举的例子:主串是:ababaeabac 模式串是:ababcd

算法是通过前面往后面走的,明显我们可以看到abab是匹配的。有没有想到BM想法的好后缀,这个只不多是好前缀,按照BF算法的话,我们接下来要移动到第二位b的地方开始匹配,但是我们KMP算法并不是这样的,通过找好前缀的规律,是不是发现好前缀的前缀字符串ab和好前缀的后缀字符串ab是一样的,我们是否也跟BM算法一样直接移动后缀字符串哪里呢?这个答案明显是可以的。(以前数学老师上课好像也是这样)。

证明我们是证明不了,只能通过移动试试,像之前的BM也是,是不是发现确实没匹配上:

在这里插入图片描述

感觉我这里例子有点偷懒,偷懒就偷懒把,加入中间加了一个a结果也是一样的,不信可以试试,所以KMP算法就可以直接移动2位,移动到这个位置:

在这里插入图片描述

是不是提速了不少,相当之前的BF算法。

在前辈们的努力下,把这种规律整理了出来,KMP算法的最长公共前后缀

前缀为ab后缀也是ab,这个就是最长公共前后缀,得出最长公共前后缀的长度,才能找到如果匹配了,下一步需要滑过几个字符。

9.3 最长公共前后缀

我们现在先手动计算最长公共前后缀,还是拿上面的例子,其实计算好前缀的公共前后缀,主要模式串就可以了,因为如果主串和模式串能匹配好前缀,自然模式串的好前缀和主串是一样的。

模式串:ababacd

因为我们在匹配过程中,完全不知道好前缀是纠结匹配了几个,所以把所有的前缀都当做好前缀,这样我们就所以的前缀的公共前后缀计算出来就好。(这就是下面说的next数组)

好前缀候选公共前后最next值
a0next[0] = 0
ab0next[1] = 0
aba1next[2] = 1
abab2next[3] = 2
ababa3next[4] = 3
ababac0next[5] = 0

第一个好前缀a的前缀后缀都是自己,自己不能等自己,所以为0

第二个好前缀ab,前缀是a,后缀是b,所以为0

第三个好前缀aba, 前缀是a,ab, 后缀子串ba ,a,最长可匹配的是a,所以是1

第四个好前缀abab,前缀子串a,ab,aba,后缀子串bab,ab,b,最长可匹配的是ab,所以是2

第五个好前缀ababa,前缀子串a,ab,aba,abab,后缀子串baba,aba,ba,a,最长可匹配的是aba,所以是3

第六个好前缀ababac,前缀子串a,ab,aba,abab,ababa,后缀子串babac,abac,bac,ac,c,没有所以为0

我们通过手动计算出来的最长公共前后缀,其实就是next数组,我们这个next数组的值:next[] = {0,0,1,2,3,0};

其实有的版本是把next数组全部-1:得到next[] = {-1,-1,0,1,2,-1};然后取的时候,再加1,这个好处就是处理边界条件会好,计算next数组都是为了,知道下一次要跳过哪个位置,哪个版本的都不要紧。

现在我们也知道了,next数组跟模式串有关,所以我们会在预处理的时候,把next数组构建出来,不会留着在匹配的时候,熟悉BM和RK算法都知道,都需要预处理一波。

9.4 next数组求解

看着王争老师写的next数组求解,简直怀疑人生,感觉这辈子都学不会next数组求解了。

甚至放弃了好几天,然后想着,去B站碰碰运气,觉得看到了 代码随想录 的大神,讲的KMP算法很好,一下子就就懂了。感谢大神,我这里为了让MKP更熟悉,所以我自己再总结一波,大神总结是大神的,自己也需要总结一波。

// i表示后缀末尾字符下标
// k表示前缀末尾字符下标,同时也是最长公共前后缀
// 接下来就是见证奇迹的时候,怎么由两个下标计算next数组
// 参数说明:pat:模式串
//          next:next数组,为什么没长度,是跟模式串长度一样,就少传了一个参数
int getNext(std::string pat, int *next)
{
    int N = pat.length();   // 获取模式串长度N
    int k = 0;              // k是前缀末尾字符串,指向字符为0的
    next[0] = 0;            // next数组初始化,0的最长公共前后缀肯定是0,因为就一个字符

    // 接下来上核心,i是后缀末尾下标,我们需要模式串中所有的子串,所以i(后缀末尾下标要加加)
    for (int i = 1; i<N-1; i++) 
    // 比如有一个模式串:ababacd,
    // i=1,后缀子串:b,i=2,后缀子串:ba,i=3,后缀子串:bab,i=4,后缀子串:baba,
    // i=5,后缀子串:babac,   i=6,后缀子串babacd
    {
        // 第一次循环,比较的是ab的最长公共前后缀,明显没有,进入1,进行回滚,顺便到3,保存一下next[1] = 0;
        // 第二次循环,k=0,i=2,这次比的子串是:aba,前后缀a都一样,进入2,k=1,保存next[2]=1;
        // (这里是不是懵逼了,为什么可以直接得出next[2]=1.那是因为你上一循环中不相等,如果3个aaa相等,又会不一样了,可以自己试试)
        // 第三次循环,因为上一次的前后缀一样,接着比k=1,i=3,子串abab,next[3]=2
        // 第四次循环,前面两个字符都一样了,接着看k=2,i=4的时候,是否一样,明显ababa,明显是一样的next[4]=3
        // 第五次循环,k=3,i=5美好被打破了,不一样了,滑动到上一次匹配的地方,一直不相等一直退(总感觉不会匹配了,就是滑回0)
        // 如果有下一次的话,就感觉上一个不一样的字符是坏字符,需要重新匹配
        
        // 如果不匹配,就需要倒退回去匹配的时候,这样好再次匹配
        while(k > 0 && pat[i] != pat[k])
        {
            k = next[k-1];            // 1
        }

        // 如果相等就相等与前面的字符相等了,赶紧接着比
        if(pat[i] == pat[k])
        {
            k++;                    // 2
        }
        
        // 然后把这个最长公共前后缀先保存起来
        next[i] = k;                // 3
    }

    return 0;
}

能力有限了,就先这样,如果有写错的地方,可以私信找我,大家一起学习。

9.5 KMP整体框架代码

有了next数组,我们来实现一下kmp算法。

// 有了next数组,我们实现一波kmp
// 再次回忆一下kmp算法,kmp算法其实是也是需要两层循环的
int kmp(std::string str, std::string pat)
{
    int N = str.length();
    int M = pat.length();

    int *next = new int[M];
    // 获取next数组
    getNext(pat, next);

    int j = 0;      //子串的下标
    // 开始主串for循环,然后在其中匹配子串
    for(int i =0; i<N; i++)     // 主串的下标
    {
        // 如果不匹配,需要借助next数组回退
        while(j > 0 && str[i] != pat[j])
        {
            j = next[j - 1];        // j-1就是取不匹配字符的上一个next数组,就是上一个最长公共前后缀
        }

        // 如果匹配,就继续移动
        if(str[i] == pat[j])
        {
            ++j;
        }

        if(j == M)      //如果j == M就说明匹配上了
        {
            return i - M + 1;       //返回首字母下标
        }
    }

    return -1;
}

接下来就可以自己编译试试了。

9.6 性能分析

KMP算法的空间复杂度就是next数组,也就是O(m),m表示模式串长度。

然后时间复杂度就有点难度,分为两个部分,第一部分是next数组的,第二部分才是匹配的,但是每次部分都是外面一个for循环,内部一个while,并且这个while又不是每次都回退,所以简单起见,都看做是O(m+n)。具体的不分析了,有兴趣自己分析。

这个KMP算法耗尽脑细胞啊,越感觉自己懂的时候,然后越去分析就觉得还是不懂,算法确实难度不小。如果有哪里错误的地方,可以私信我,大家一起进步,还有另一种是用动态规划去推导next数组,谁叫我不懂动态规划呢。所以只能自己分析,不过接下来的目标就是动态规划了,早日搞定动态规划,加油。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值