KMP 模式匹配

微信公众号:编程笔记本

点击上方蓝字关注我,我们一起学编程
欢迎小伙伴们分享、转载、私信、赞赏

昨晚逛 CSDN 的时候,偶然间看到了 KMP 算法的一篇文章,就想起了之前在学习算法与数据结构时留下的未解之谜,所以就决定把这个问题给它解决掉,彻底弄懂。

现在我就将我所理解的 KMP 算法分享给大家,且听!

  • Brute-Force 算法
  • KMP 算法
  • 改进 KMP 算法

Brute-Force 算法

要引入 KMP 算法,我们还得从 Brute-Force 算法开始说起。

Brute-Force 算法,又称 BF 算法,中文翻译过来就是穷举硬算的意思。是一种简单匹配算法,其基本思路如下:

从目标串 s="s0s1…sn-1" 的第一个字符开始,与模式串 t="t0t1…tm-1" 中的第一个字符比较,若相等,则继续逐个比较后续字符;否则从目标串 s 的第二个字符开始重新与模式串 t 的第一个字符进行比较。依此类推,若从模式串 s 的第 i 个字符开始,每个字符依次和目标串 t 中的对应字符相等,则匹配成功,该算法返回 i ,即目标串中字串的首字符下标;否则,匹配失败,函数返回 -1 。

匹配过程如下动图所示:

图片

这个算法很简单,思路也很清晰,这里我直接贴出参考代码:

int index(char s[], int sLen, char p[], int pLen)
{
    int i = 0, j = 0;
    
    if (sLen < pLen)
    {
        return -1;
    }

    while (i < sLen && j < pLen)
    {
        if (s[i] == p[i])     // 继续匹配下一个字符
        {
            ++i;
            ++j;
        }
        else                  // 重新开始下一次匹配
        {
            i = i - j + 1;    // 目标串从下一个位置开始
            j = 0;            // 模式串从头开始
        }
    }

    if (j == pLen)            // 模式串全部位于目标串当中
    {
        return i - pLen;
    }
    else
    {
        return 0;
    }
}

这个算法很简单,易于理解,但效率不高,主要原因是目标串指针 i 在若干个字符序列比较相等后,若有一个字符比较不相等,仍需回溯,即 i=i-j+1。BF 算法在最好情况下的时间复杂度为 O(m),即目标串的前 m 个字符正好等于模式串的 m 个字符。在最坏情况下的时间复杂度为 O(n*m),即模式串依次与目标串的所有字符进行比较。

KMP 算法

KMP 算法是 D.E.Knuth、J.H.Morris 和 V.R.Pratt 共同提出的,简称 KMP 算法。该算法较 BF 算法有较大改进,主要是消除了目标串指针的回溯,从而使算法效率有了某种程度的提高。

让我们先来看一个例子:

图片

现在我们来想一想,在第二次匹配的时候,真的有必要从 s[1]开始吗?
既然 s[0]==p[0], s[1]==p[1], s[2]=p[2] ,那么我们何不从 s[3] 开始呢?由于我们在 s[3] 处失配了,现在又能在 s[3] 处开始新的匹配,这说明了什么?这不就是目标串指针是只进不退的嘛!也就是说,目标串指针不需要回溯

那么,我们怎么知道该从何处开始新的匹配呢?
这就需要引入一个指引,指引我们从失配处寻找新的位置重新开始匹配。业界常将其称作 next[j] 函数

下面的重点就是如何求出这个 next 函数。

所谓的 next 函数,其实本质上就是在模式串第 j 个索引处失配了,此前 j-1 个字符有没有首尾相同的子串。如果有,我们下一次可以跳过这些首尾相同的字串。这其中的原理是什么呢?试想一下,若这 j-1 个字符中有首尾相同的字串,那么目标串中这 j-1 个字符也同样有首尾相同的字串,那么我们就可以将模式串的尾串目标串的首串直接匹配。

下面我们就来计算一下模式串的 next 函数。

图片

上述过程可以用如下代码表示:

void GetNext(char p[], int pLen, int next[])	 
{  
    int j = 0, k = -1;

    next[0] = -1;

    while (j < pLen - 1)
    {
        if (k == -1 || t.data[j] == t.data[k])
        {
            ++j;
            ++k;
            next[j] = k;
        }
        else 
        {
            k = next[k];
        }
   }
}

有了 next 函数的辅助,现在很容易就得出 KMP 算法啦!

int KMPIndex(char s[], int sLen, char p[], int pLen, int next[]) 
{  
    int i = 0, j = 0;
    
    while (i < sLen && j < pLen) 
    { 
        if (j == -1 || s[i] == p[j]) 
        {  
            ++i;
            ++j;
        }
        else 
        {
            j = next[j];      // 目标串指针不变,模式串倒退
        }
    }
    
    if (j == pLen)
	{
        return i - pLen;      // 返回匹配模式串的首字符下标
    }
    else
    {
        return -1;            // 返回不匹配标志
    }
}

下面我们来看一下,用这个 KMP 算法来进行模式匹配的过程:

图片

我们惊奇地发现,废了这么半天劲折腾出来的 KMP 算法竟然没啥用?
哈哈,这就是后面我们需要再改进的地方。当然了,这只是一类情况,针对很多其他情况,现在的 KMP 算法已经很高效了。看一个例子:

图片

可以看到,我们在第二次就匹配成功了,而 BF 算法需要到第三次才会匹配成功。

设目标串 s 的长度为 n ,模式串 p 长度为 m 。在 KMP 算法中求 next 数组的时间复杂度为O(m) ,在后面的匹配中因目标串 s 的下标不减即不回溯,比较次数可记为 n ,所以 KMP 算法总的时间复杂度为 O(n+m)

下面我们针对上面的不足之处做一下改进。

主要就是修改 next 函数:

void GetNext(char p[], int pLen, int next[])
{  
    int j = 0, k = -1;
    
    next[0] = -1;
    
    while (j < pLen)
    {
        if (k == -1 || p[j] == p[k])
	    {
            ++j;
            ++k;
            
	        if (p[j] != p[k])
            {
	 	        next[j] = k;
	        }
            else
		    {
                next[j] = next[k];
            }
	    }
	    else
	    {
            k = next[k];
        }
    }
}

此时,KMP 算法已经完整。

这节内容整理得不是很好,初次接触 KMP 算法的小伙伴应该是读不懂的哈哈。以后有好的讲解思路我再分享过来,今天先到这。

散会!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值