算法 - KMP算法原理顿悟有感


中国开国七十一年三月十二日下午,解衣欲睡,雨声入耳,悟以往,思来日,惧无以为生计,遂至哔哩哔哩寻KMP算术,观后遂顿悟,以作文记之。

KMP?

​ KMP算法是在某一字符串中寻找给定子串的算法,比起逐一比较的方法,KMP算法快了不少。

KMP核心思想

​ 比较过程如图所示:

在这里插入图片描述

​ 此处的疑点:为什么有时候模式串一次往后移那么多?为什么有时候模式串又只往后移一格?到底是怎么个模式串移动法?

​ 这就是KMP的关键。

​ 答案:当发现模式串的第i个字符匹配错误时,将模式串向后移动N个位置。此处,N = 模式串第i个字符前的子串中的长度 - 模式串第i个字符前的子串最长且小于子串本身长度的公共前后缀的长度。

一句话总结:让i之前的公共前缀移动到公共后缀的位置上。

由此可知,KMP算法的移动主要和模式串有关,和待查找的字符串关系不大。

举个栗子

假设模式串为 ABABAAABABAA

在这里插入图片描述

假设模式串与带比较字符串的第1位比较不匹配:则模式串前移1位(1号位与主串下一位比较)

假设模式串与带比较字符串的第2位比较不匹配:二号位前的子串为A,公共前后缀长度是0,则模式串向前移1位(1号位与主串当前位比较)

假设模式串与带比较字符串的第3位比较不匹配:三号位前的子串为AB,公共前后缀长度是0,则模式串向前移1位(1号位与主串当前位比较)

假设模式串与带比较字符串的第4位比较不匹配:三号位前的子串为ABA,公共前后缀长度是1,则模式串向前移2位(2号位与主串当前位比较)

到这为之,如图所示:

在这里插入图片描述

我们得到一个规律,如果当前子串最大公共前后缀长度为N,那我们就移动模式串使N+1号位与主串当前位比较。

那么以此类推:

在这里插入图片描述

我们把第一句话标记为0,当我们看到0时,将1号位与主串下一位比较。

并且我们将后面的每一句话的第一个数字取出,结合上面的数组下标,将这些数字放在一个数组中,这样一来,根据数组所提供的信息,我们在模式串上任何一个位置匹配失败,就知道下一步该怎么做了:

在这里插入图片描述

这个数组就是传说中的next数组

上点代码

​ 循序渐进,若想知道KMP算法的代码实现怎么写,我们先要知道天真的傻瓜式比较法的代码怎么写。

//too simple, sometimes naive
//假设字符串位置从1开始
int naive(String str, String substr)
{
    int i = 1, j = 1, k = i; //i:主串游标,j:模式串游标,k:位置记录器
    while (i<=str.length && j<=substr.length){	//i、j都落在各自字符串的范围内
        if(str.ch[i] == substr.ch[j]){
            i++;
            j++;
        }
        else {
            j = 1;
            i = ++k;	//寻找初始比较位置的下一个位置
        }
    }
    if (j>substr.length) return k;
    else return -1;
}

现在用KMP的思想做一点修改:

int KMP(String str, String substr, int next[])
{
    int i = 1, j = 1; 	//KMP不需要回溯,不需要k
    while (i<=str.length && j<=substr.length){	
        if(j == 0 || str.ch[i] == substr.ch[j]){ //此处注意j=0的处理
            i++;
            j++;
        }
        else {
            j = next[j];	//i不需要回溯,j有next数组指导往哪走	
        }
    }
    if (j>substr.length) return i-substr.length;	//计算首字符存在的位置
    else return -1;
}

而关键在于:next数组怎么获得?

next数组

KMP所做的事的聪明之处在于:把之前工作的结果合理利用起来,减少重复劳动。

求解next数组需要继承这一思想。

假设一段模式串如下图所示:

在这里插入图片描述

P为模式串中的字符,P的下标代表每一个字符的位置,模式串的长度为m

现在我们把模式串复制一份,并凸显出1到t位置上的字符,也就是左端长度为t的子串

在这里插入图片描述

我们将上面Pj-t+1到Pj的子串与下面P1到Pt的子串对应起来,假设红色部分完全匹配,黄色部分暂时不知道,则next[j] = t。

在这里插入图片描述

现在我们需要求next[j+1]的值。

(1)若Pj == Pt

那么很容易求得next[j] = t + 1 = next[j] + 1

(2) 若Pj 和 Pt不相等

当Pj 不等于 Pt时,这时的情况似曾相识:主串某个位置与模式串某个位置发生不匹配的现象。

假如我们把上面的字符串称为假主串,下面的称为假模式串:

在这里插入图片描述

这不就是熟悉的KMP吗!

在我们已知next[j]求next[j+1]的情况下时,我们可以使用next[j]之前所有的next数组

所以当若Pj 和 Pt不相等时,则循环将t赋值为next[t],直到t=0或者满足(1)为止,当t = 0时,next[j+1] = 1。

总结:

在这里插入图片描述

我们发现,这种求法天生适合翻译成代码:

void getNext(string substr, int next[])
{
    int t = 0, j = 1;
    next[1] = 0;
    while (j<substr.length){
        if(t == 0 || substr.ch[j] == substr.ch[t]){
            next[j+1] = t + 1;
            t++; j++;
        }
        else 
            t = next[t];
    }
}

​ 在求完next数组之后,为了理解接下来的工作,以及不忘初心,我们必须回顾及总结next数组的含义到底是什么。

​ next[5] = 3意味着5这个位置之前的子串的公共前后缀长度为2。

​ 一句话:next[i] = j意味着i这个位置之前的模式串有j-1个字符是能够与主串匹配的。

改进上面的KMP算法

​ 这里就让人头疼了,本来上面那个就不怎么好理解,还要改进?

不合理的next

在求解next[j]时,上图其实做了很多重复的工作。

因为1到4上的字符串相等,因此next[5]直接赋值为0即可。

所以针对KMP算法的改进主要集中在求解next数组的改进上,我们把改进之后的数组称为nextval数组。

nextval

​ nextval的优化思路:求解nextval[j]时,移过来比较的字符必须与比较过不符合要求的Pj不相同。

nextval求解

​ 如上图所示:其中P代表模式串中的字符,我们若要求nextval[j],需不停将j赋值为next[j],再把这些位置上的字符与Pj进行比较,如果它们与Pj相等,那么意味着Pj与主串中的字符未匹配,则这些位置上的字符来到这也没用。若其中有一个字符与Pj不等,那nextval[j]则为对应位置(上图Pa中的)。

求解nextval数组的一般方法
  1. 当j等于1时,nextval[j]赋值为0,作为特殊标记
  2. 当j大于1时:
    • 若Pj不等于Pnext[j],则nextval[j]等于next[j]
    • 若Pj等于Pnext[j],则nextval[j]等于nextval[next[j]]

上代码
void getNextval(string substr, int nextval[])
{
    int t = 0, j = 1;
    nextval[1] = 0;		//这句很明显要加上
    while (j<substr.length){
        if(t == 0 || substr.ch[j] == substr.ch[t]){
            //计算nextval值
            if(substr.ch[j+1] != substr.ch[t+1]])
                nextval[j+1] = t + 1;
            else
                nextval[j+1] = nextval[t + 1];
            t++; j++;
        }
        else 
            t = nextval[t];		//用nextval代替next数组
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值