KMP算法理解

文章大纲:
1.KMP算法概念
2.KMP算法中最核心的next[] 数组是如何生成的
3.使用KMP算法 匹配字符串 java实现
4.再次梳理记忆和理解KMP算法

KMP算法概念:

假设现在我们面临这样一个问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置,怎么查找呢?
如果用暴力匹配的思路,并假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置,则有:
如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符;
如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。

理清楚了暴力匹配算法的流程及内在的逻辑,咱们可以写出暴力匹配的代码,如下:

<pre>
<code>

  • int ViolentMatch(char* s, char* p)
    · {

  • int sLen = strlen(s);
    
  • int pLen = strlen(p);
    
  • int i = 0;
    
  • int j = 0;
    
  •   while (i < sLen && j < pLen)
    
  • {
    
  •     if (s[i] == p[j])
    
  •     {
    
  •         //①如果当前字符匹配成功(即S[i] == P[j]),则i++,j++
    
  •         i++;
    
  •         j++;
    
  •     }
    
  •     else
    
  •     {
    
  •         //②如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0
    
  •         i = i - j + 1;
    
  •         j = 0;
    
  •     }
    
  • }
    
  • //匹配成功,返回模式串p在文本串s中的位置,否则返回-1
    
  • if (j == pLen)
    
  •     return i - j;
    
  • else
    
  •     return -1;
    
  • }
    </pre>
    </code>

比如:你看当字符串匹配到这里的时候 此时不匹配了。

按照暴力的方法 上面一串将回到第5个位置 下面那一串回到0位置进行重新比较。


此时肯定是不匹配的,因为在前面的匹配过程中 已经按照顺序匹配成功才会走到最后D位置 ,由此可以推理得到第一个A肯定不能和它的下一个也就是B相匹配。

KMP算法就是 利用之前已经部分匹配这个有效信息,保持i(大串的坐标) 不回溯,通过修改j(待匹配串的坐标) 的位置,让模式串尽量地移动到有效的位置。

整个的算法流程:

假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置
如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符;
如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了j - next [j] 位。
换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值(next 数组的求解会在下文的3.3.3节中详细阐述),即移动的实际位数为:j - next[j],且此值大于等于1。
<pre>
<code>

/**

  • 匹配相应的字符串 Kmp
  • @param a 总字符串
  • @param b 待匹配字符串
  • @return 匹配成功返回 字符串在总串中的起始位置 失败返回-1
    */

int Kmpsearch(String a,String b){

char [] chars_1=a.toCharArray();
char [] chars_2=b.toCharArray();

int len_1=a.length();
int len_2=b.length();
this.next=getNext(b);//根据b要匹配的串 生成相应的next数组
int i=0,j=0;
while (i<len_1&&j<len_2){
    if (j==-1||chars_1[i]==chars_2[j]){
        i++;
        j++;
    }else {
        j=next[j];
    }
}
if (j==len_2){
    return i-j;
}else {
    return -1;
}

</pre>
</code>

接下来我们就只需要去考虑如何生成next数组即可

KMP算法中最核心的next[] 数组是如何生成的:

我们先来假设一下next[j]=k 那么这是什么意思呢?
意思就是在第j个位置之前的所有字符串中存在前缀后缀相等的字符串长度为k,所以如果当你匹配到第j个位置是匹配失败,那么你直接使用next去求得k位置重新进行和总字符串这个同一位置比较时,就把前面相同匹配过的过程给节约了。因为你的前后缀相同。如果解释不明白,后面还会有图帮助理解。

下面这张图是来理解前缀后缀的概念:

我们用一个网上的例子进行讲解next的生成:

如下图所示,假定给定模式串ABCDABCE,且已知next [j] = k(上面说过这个的含义 那么在这里 我们可以看到就是Pj之前 ABCDAB的最大相同前后缀为AB 也就是k=2),现要求next [j + 1]等于多少?因为pk = pj = C,所以next[j + 1] = next[j] + 1 = k + 1(可以看出next[j + 1] = 3)。代表字符E前的模式串中,有长度k+1 的相同前缀后缀。

如果pk != pj 呢?说明“p0 pk-1 pk” ≠ “pj-k pj-1 pj”。换言之,当pk != pj后,字符E前有多大长度的相同前缀后缀呢?很明显,因为C不同于D,所以ABC 跟 ABD不相同,即字符E前的模式串没有长度为k+1的相同前缀后缀,也就不能再简单的令:next[j + 1] = next[j] + 1 。所以,咱们只能去寻找长度更短一点的相同前缀后缀。


用什么方法去寻找更短一点的相同前后缀呢?
递归前缀索引k = next[k],就能找到长度更短的相同前缀后缀 。这又归根到next数组的含义。我们拿前缀 p0 pk-1 pk 去跟后缀pj-k pj-1 pj匹配,如果pk 跟pj 失配,下一步就是用p[next[k]] 去跟pj 继续匹配,如果p[ next[k] ]跟pj还是不匹配,则需要寻找长度更短的相同前缀后缀,即下一步用p[ next[ next[k] ] ]去跟pj匹配。此过程相当于模式串的自我匹配,所以不断的递归k = next[k],直到要么找到长度更短的相同前缀后缀,要么没有长度更短的相同前缀后缀。如下图所示:

这幅图一开始我也没太认真去看,觉得有点复杂。后面仔细想了一下回过头再看确实对理解k=next[k]有很大的帮助

我的个人理解是这样的:
首先我们可以看到Pj位置,前面有两块相同的k块,在图片上方用大括号括起来代表相同的前后缀长度为k 用公式表示就是next[j]=k ,
然后此时Pk和Pj不相同。一块红色一块黄色。我们要去寻找短的前缀,
根据k=next[k]我们到了Pnext[k]位置 也就是Pk前面存在的最大相同前缀。
看前半部分,是两块蓝色的相等部分。然后此时去比较Pnext[k]和Pj看是否相同,如果相同就有next[j+1]=next[k]+1,
那么为什么j+1的位置前面会有next[k]+1的相同前后缀呢。我们看图,两块k区域是相等的,然后Pk前面的两块蓝色是相等的,所以四块蓝色的区域也就相等了。
那么P0到Pnext[k]这一块也就和Pj相邻的蓝色块相等。
如果一直找到头都没有短一点的前后缀,则next[j+1]=0

现在我们再总结一下next数组的生成,假设:next[j]=k 那么next[j+1]根据Pk和Pj是否相同有两种情况,如果相同 则next[j+1]=k+1,如果不相同则k=next[k]继续去判断此时的Pk和Pj是否相同。
我们初始化化赋值为 j=0 k=-1 next[0]=-1 下面用代码写一遍:
<pre>
<code>

/**

  • 获取next数组的函数
  • @param string 模式串 待匹配的字符串
  • @return 生成的next函数
    */

public int [] getNext(String string){

char [] chars=string.toCharArray();
int len=string.length();
int k=-1;
int j=0;
int []next=new int[len];
next[0]=-1;
while (j<len-1){
    if (k==-1||chars[j]==chars[k]){//chars[j]前缀 chars[k]后缀 相等 或者k==-1
       next[++j]=++k;
    }else {
        k=next[k];//当不匹配时 去前面最大匹配的位置那再次进行比较看是否能匹配
    }
}
return next;

}

</pre>
</code>

next数组优化

如果用之前的next 数组方法求模式串“abab”的next 数组,可得其next 数组为-1 0 0 1,当它跟下图中的文本串去匹配的时候,发现b跟c失配,于是模式串右移j - next[j] = 3 - 1 =2位。

右移2位后,b又跟c失配。事实上,因为在上一步的匹配中,已经得知p[3] = b,与s[3] = c失配,而右移两位之后,让p[ next[3] ] = p[1] = b 再跟s[3]匹配时,必然失配。问题出在哪呢?

问题出在不该出现p[j] = p[ next[j] ]。为什么呢?理由是:当p[j] != s[i] 时,下次匹配必然是p[ next [j]] 跟s[i]匹配,如果p[j] = p[ next[j] ],必然导致后一步匹配失败(因为p[j]已经跟s[i]失配,然后你还用跟p[j]等同的值p[next[j]]去跟s[i]匹配,很显然,必然失配),所以不能允许p[j] = p[ next[j ]]。如果出现了p[j] = p[ next[j] ]咋办呢?如果出现了,则需要再次递归,即令next[j] = next[ next[j] ]。

所以,咱们得修改下求next 数组的代码。

<pre>
<code>

/**

  • 获取next数组的函数
  • @param string 模式串 待匹配的字符串
  • @return 生成的next函数
    */

public int [] getNext(String string){

char [] chars=string.toCharArray();
int len=string.length();
int k=-1;
int j=0;
int []next=new int[len];
next[0]=-1;
while (j<len-1){
    if (k==-1||chars[j]==chars[k]){//chars[j]前缀 chars[k]后缀 相等 或者k==-1
        j++;
        k++;
        if (chars[j]!=chars[k]){//如果此时你让 next[j]=k 那么进行位移的时候 把chars[k]拿去匹配还是失败 因为chars[j]已经失败过
            next[j]=k;
        }else {  //因为不能出现p[j] = p[next[j]],所以当出现时需要继续递归,k = next[k] = next[next[k]]
            next[j]=next[k];
        }
    }else {
        k=next[k];//当不匹配时 去前面最大匹配的位置那再次进行比较看是否能匹配
    }
}
return next;

}

</pre>
</code>

使用KMP算法 匹配字符串 java实现

<pre>
<code>

/**

  • 获取next数组的函数
  • @param string 模式串 待匹配的字符串
  • @return 生成的next函数
    */

public int [] getNext(String string){

char [] chars=string.toCharArray();
int len=string.length();
int k=-1;
int j=0;
int []next=new int[len];
next[0]=-1;
while (j<len-1){
    if (k==-1||chars[j]==chars[k]){//chars[j]前缀 chars[k]后缀 相等 或者k==-1
        j++;
        k++;
        if (chars[j]!=chars[k]){//如果此时你让 next[j]=k 那么进行位移的时候 把chars[k]拿去匹配还是失败 因为chars[j]已经失败过
            next[j]=k;
        }else {  //因为不能出现p[j] = p[next[j]],所以当出现时需要继续递归,k = next[k] = next[next[k]]
            next[j]=next[k];
        }
    }else {
        k=next[k];//当不匹配时 去前面最大匹配的位置那再次进行比较看是否能匹配
    }
}
return next;

}

/**

  • 匹配相应的字符串 Kmp
  • @param a 总字符串
  • @param b 待匹配字符串
  • @return 匹配成功返回 字符串在总串中的起始位置 失败返回-1
    */

int Kmpsearch(String a,String b){

char [] chars_1=a.toCharArray();
char [] chars_2=b.toCharArray();

int len_1=a.length();
int len_2=b.length();
this.next=getNext(b);//根据b要匹配的串 生成相应的next数组
int i=0,j=0;
while (i<len_1&&j<len_2){
    if (j==-1||chars_1[i]==chars_2[j]){
        i++;
        j++;
    }else {
        j=next[j];
    }
}
if (j==len_2){
    return i-j;
}else {
    return -1;
}

}

</pre>
</code>

再次梳理记忆和理解KMP算法

KMP算法的使用过程:两个待比较的字符串进行单个字符一一比较,如果相等则进行下一个位置的比较,否则模式串(小的那串)的位置移动到next[当前位置]。

关键的next数组生成:
有两个指针 j和k。 当k=-1或者在J和K位置的字符相等时,两指针同时向前一位。否则k回退到next[k]位置(寻找更短的前后缀相等位置)。若两指针同时向前一位时 ,比较新的J和K位置字符是否相等,若相等则next[j]=next[k] ,不等 next[j]=k.

文章参考来源:
从头到尾彻底理解KMP

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • KMP的由来 在KMP算法之前,对文本进行匹配时使用的是朴素模式匹配算法,也就是最简单匹配算法.当然运行效率也是让...
    圣光忏悔阅读 1,742评论 2 13
  • 数据结构与算法--KMP算法查找子字符串 部分内容和图片来自这三篇文章: 这篇文章、这篇文章、还有这篇他们写得非常...
    sunhaiyu阅读 1,723评论 1 21
  • 引言 字符串匹配一直是计算机科学领域研究和应用的热门领域,算法的改进研究一直是一个十分困难的课题。作为字符串匹配中...
    潮汐行者阅读 1,624评论 2 6
  • 数据结构 第8讲 KMP算法 讲这个算法之前,我们首先了解几个概念: 串:又称字符串,是由零个或多个字符组成的有限...
    rainchxy阅读 1,268评论 0 3
  • 前阵子部门内招产品助理,在面试过程中,主面试官提了一个问题:你生活中遇到过哪些不好的设计? 我自己也顺便回忆了一些...
    威米阅读 957评论 0 1