谈谈对KMP算法的一些理解

谈谈对KMP算法的一些理解

这里是我看的原文,大佬讲的很清楚
下面是我自己对KMP的一些感悟和理解

首先,定义一些概念:
1.在某个字符串中找自己想要的字符串,那么【自己想要的字符串】就被称为模式串;
2.字符串的前缀是指从左到右看,始于第一个字符,后缀也是从左到右看,终于最后一个字符,前后缀相等就相当于前缀可以通过平移的方式到达后缀的位置,与后缀相重合,相重合(注意这个和回文的区别);

说到字符串的匹配问题,
首先,不得不cue一下BF算法,所谓的暴力匹配法,但是由于其效率过于低下(具体表现为每次发现某一位不匹配的时候,都是将模式串往后移动【一位】)

扩写着地说,BF算法是一种"不论在哪个位置发生不匹配,都是把原字符串的指针右移一位,并把模式串的指针移到首位",然而"不论在哪个位置发生不匹配"就是其效率低的根本原因,因为它没有吸取之前某一位匹配成功和失败的教训,于是大佬们就从这一点出发,利用匹配成功或失败的教训,对算法进行了优化

**于是就对问题有了这样的探讨:**通过对之前匹配成功和失败信息的运用,失败后原字符串是不是可以移动多位呢?模式串的指针能不能不回到首位呢?

事实上,在KMP算法中,原字符串还是一位一位地移动,但是模式串的指针却可以从原来的每次都“从头开始”变成"回溯到之前的记忆位"

下面,我们来看一看KMP算法:(一共分为两步)

First and foremost:建立一个next_table

next_table有什么用?

前面说到,匹配失败后的模式串并不会回到首位,而是回到之前的记忆位,而next_table就相当于是一个用于记忆的数组,其与最长公共前后缀的大小有关,为前p - 1个字符串的最长公共前后缀 - 1;

next_table怎么形成?

这里本质也是一个模式串自己对自己的KMP运算
下面贴上大佬博客上的代码

void cal_next(char *str, int *next, int len)
{
    next[0] = -1;//next[0]初始化为-1,-1表示不存在相同的最大前缀和最大后缀
    int k = -1;//k初始化为-1
    for (int q = 1; q <= len-1; q++)
    {
        while (k > -1 && str[k + 1] != str[q])//如果下一个不同,那么k就变成next[k],注意next[k]是小于k的,无论k取任何值。
        {
            k = next[k];//往前回溯
        }
        if (str[k + 1] == str[q])//如果相同,k++
        {
            k = k + 1;
        }
        next[q] = k;//这个是把算的k的值(就是相同的最大前缀和最大后缀长)赋给next[q]
    }
}

我起初直接取看代码,结果是一脸懵逼的,下面我按照原文里的思路,添加一些细节:
第一步是给next[0]赋值,然后进入for循环
先看整体框架,发现每一次for循环都代表主字符串的一位,并且最后给next[q]赋值了,说明每一次循环,得到一个next_table的值,这样便很自然的明白,for循环里面的while循环和if语句都是用来操作k的值,得到“若某一位不匹配,则下一次模式串回溯的目标位置”(先看懂个大概,具体先不细究)

然后看while循环和if语句,发现条件中都是在比较k + 1位和q位是否相同(先不看k > -1,也别管whileif的区别)
刚开始k = -1,k + 1 == 0p == 1意思是第0位和第1位比,看是否匹配,假设它从始至终一直都匹配,那么k和p不断增大,这过程中next[p]也在不断增大。

当k等于某个值的时候,发现k + 1与p不匹配了,这时候就执行while语句:回溯。

Then:匹配字符串和模式串

先枯燥的贴上代码:

int KMP(char *str, int slen, char *ptr, int plen)
{
    int *next = new int[plen];
    cal_next(ptr, next, plen);//计算next数组
    int k = -1;
    for (int i = 0; i < slen; i++)
    {
        while (k >-1&& ptr[k + 1] != str[i])//ptr和str不匹配,且k>-1(表示ptr和str有部分匹配)
            k = next[k];//往前回溯
        if (ptr[k + 1] == str[i])
            k = k + 1;
        if (k == plen-1)//说明k移动到ptr的最末端
        {
            //cout << "在位置" << i-plen+1<< endl;
            //k = -1;//重新初始化,寻找下一个
            //i = i - plen + 1;//i定位到该位置,外层for循环i++可以继续找下一个(这里默认存在两个匹配字符串可以部分重叠),感谢评论中同学指出错误。
            return i-plen+1;//返回相应的位置
        }
    }
    return -1;  
}

具体怎么操作的呢?其他的视频和大佬博客已经讲解了很多,非常清楚了。

下面有一个小故事,可能可以更好的理解回溯:
国庆节的最后一天假期,小明肝了多天游戏,终于在今晚,他想起了自己还有大把的国庆作业没有写,随后,连续通宵七天的他心想:”唉,肯定做不完作业了,做几道题目就好睡觉了,身体要紧!“,于是他就写了前两道选择题,之后由于太过兴奋,又通宵打了一个晚上的游戏。
时间来到了第二天,小明背着书包来到教室,听着学霸A装X:“我第一天就把第三题写完了”,学霸B装X:“我第一天就把第四题写完了”,学霸C也来装X:“我第一天就把第五题写完了”……最后留着一个只写了前两道题目的小明在角落里瑟瑟发抖。

“叮铃铃!”铃声响了,老师请同学们回答一下作业的答案:老师先抽中了小明,让他回答,第一题(p == 1),小明回答(k + 1)得很顺利(原字符串和模式串的第一位匹配),第二题同理,但是第三题,由于小明没有写,就乱说了一个答案,结果当然和老师的答案(原字符串的第三位)不匹配。于是老师给小明一个机会,让他选一个人帮他回答,小明记得之前学霸们的装X言论,于是就求助学霸A(这里就是k = next[k]的过程,从后缀找前缀老师的指针是第三题,学霸A只要从第三题开始就行了,那么为了k + 1 = 3,那么k就是2,意思就是学霸A的第三题和老师的指针——第三题答案进行匹配,如果这里是BF算法,那么就一位这老师要从第一题开始问学霸,这个效率就低啦~)

假如说,学霸A也没有做的话,就再进行回溯(这就是为什么用while而不是if),如果某一道题,所有装X学霸都没有做的话,那么最后k就会回溯到-1位置,可以理解为最后有一位救世主学神,当然如果学神也匹配不了老师的正确答案,那老师迫不得已只能进行下一题了,也就是指针指向下一题

学霸的第三题答案与老师的第三题指针匹配正确之后,老师的指针指向了下一题——第四题,然而,学霸A并没有做第四题,这时候老师又给出了一次求助机会,学霸A又想起了之前装X的学霸B,于是向他求助(同理,这里也是k = next[k]的过程,学霸B不用从第一题开始回答~)

于是,学霸们和小明互相帮助,遇到没有做的题目,就求助于之前记忆中那个写过提爱装X的学霸来答本题(前缀的末尾凑上到老师的指针上)

因为自己没有完成全部题目,学霸们都秀红了脸。这天以后,小明和班里的学霸们不再热衷于装逼,形成了商业互吹的良好班风。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值