KMP算法详解

原链接:KMP算法详解|CloudWong

传统的字符串匹配模式(暴力循环)

子串的定位操作通常称作串的串的匹配模式,也就是在主串S中查找模式串(子串)T第一次出现的位置。如比较以下两个串:

主串S:ABCDABX
子串T:ABX 

我们可以通过暴力循环的方式依次的比较S[i]和T[j],若匹配失败,则子串向前移位1步,重新开始匹配,直至匹配完成。

主串S:ABCDABX
子串T:    ABX(匹配成功)


传统的暴力循环代码如下:

int index(String S,String T){
  int i,j;
  i =  j = 0;
  while(i<StrLength(S) && j<StrLength(T)){
    if(S[i] = T[i]){
      i++;
      j++;
    }           //继续比较后续字符
    else{
      i = i - j + 1;
      j = 0;
    }           //指针后退重新开始匹配
  }
  if(j > StrLength(T)) return i - StrLength(T) + 1; //返回定位
  return 0;       //匹配失败
}

这种传统的模式匹配方式最坏的情况下需要循环mxn次,时间复杂度为O(mxn),因为主串中可能存在多个和模式串“部分匹配”的子串,因此指针多次回溯,效率极低。

KMP算法的匹配过程

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。KMP算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。用以下例子说明:

  • 主串:abcababca...(假设主串很长,我们就先看前9位)
  • 子串:abcabx

按照传统的匹配模式的过程就应该如下:

传统的匹配模式,就应该是按照上面的方式一步一步的匹配下来,一旦匹配失败,主串指针i就要回溯,效率非常低!而KMP算法的匹配过程只需要两步

为什么一下次就可以跳过中间的比较来到这一步呢,下面就来探究KMP算法的匹配方式。

先来对比一下传统的匹配模式,可以发现主串的指针i值的变化:

第一次遍历到了i=6,匹配失败;

第二次遍历到了i=2,匹配失败;

第三次遍历到了i=3,匹配失败;

第四次遍历到了i=4,匹配失败;

第五次遍历到了i=5,匹配失败;

直到第五次i值终于又回到了i=6。

i值的变化情况:6->1->2->3->4->5->6

在传统的匹配算法中,可以发现i值是不断回溯的。

反观KMP算法,只需对主串一次遍历,i值不会回溯,即遍历过程中i值是不会变小的。

那么既然KMP算法的i值遍历只需一次,那么就要考虑j是如何变化的了,为什么第一次匹配失败后j可以从j=3开始匹配,而不像传统遍历算法那样每当匹配失败就要从j=1重新开始匹配。

再看看一开始对KMP算法的定义:KMP算法的关键是<u>利用匹配失败后的信息</u>,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。

划重点:<u>利用匹配失败后的信息</u>。什么是匹配失败后得到的信息呢?

于是回到刚刚的第一次匹配,看看能从这次失败的匹配中得到什么信息。

因为S[1...5] = T[1…5] 所以有 S[1,2] = T[1,2] S[4,5] = T[4,5]

又因为子串T有 T[1,2] = T[4,5],所以S[4,5] = T[1,2]

那下一次滑动到直接让S[4,5] = T[1,2],然后继续比较下一个元素就行啦。

这是简化模型第一次匹配的情况,根据传统的匹配算法,当匹配失败时模式串T移动一格,和S串比较。但是由于绿色部分在第一次匹配的时候发现了额外的信息:

就像刚刚那个例子,T[1,2] = S[4,5],都这样了,难道T还需要一格格的移动吗,直接滑过去就行啦。

这就是KMP算法的匹配过程。

如何确定模式串的滑动区间

知道了KMP算法的匹配过程,接下来就要考虑计算机是如何知道匹配失败时,指针j下一次指向的位置。由于KMP算法中指针i是不减的,因此j的指向位置只与模式串本身的结构有关。j的滑动位置的信息存放在next数组中。当匹配失败,就可以通过查询next数组的值得到下一次j滑动的位置。

next数组存放的是模式串的移位信息,具体就是模式串的部分匹配值,next数组大小与模式串T等长。

部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,

- "A"的前缀和后缀都为空集,共有元素的长度为0;

- "AB"的前缀为[A],后缀为[B],共有元素的长度为0;

- "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

- "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

- "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;

- "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;

- "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

下面是模式串T:a b c d a b c a | next数组的推导过程


      j i
模式串:a b c d a b c a
串下标:0 1 2 3 4 5 6 7
next :0 0
T[j]≠T[j] i++ next[i] = 0
         
       j   i 
模式串:a b c d a b c a
串下标:0 1 2 3 4 5 6 7
next :0 0 0
T[j]≠T[j] i++ next[i] = 0
         
       j     i 
模式串:a b c d a b c a
串下标:0 1 2 3 4 5 6 7
next :0 0 0 0
T[j]≠T[j] i++ next[i] = 0
         
       j       i 
模式串:a b c d a b c a
串下标:0 1 2 3 4 5 6 7
next :0 0 0 0 1
T[j]=T[j] i++  j++ next[i] = j+1
        
         j       i 
模式串:a b c d a b c a
串下标:0 1 2 3 4 5 6 7
next :0 0 0 0 1 2
T[j]=T[j] i++  j++ next[i] = j+1 
         
           j       i 
模式串:a b c d a b c a
串下标:0 1 2 3 4 5 6 7
next :0 0 0 0 1 2 3
T[j]=T[j] i++  j++ next[i] = j+1 
         
             j       i 
模式串:a b c d a b c a
串下标:0 1 2 3 4 5 6 7
next :0 0 0 0 1 2 3
T[j]≠T[j] j = next[j-1] 
         
       j             i 
模式串:a b c d a b c a
   j :0 1 2 3 4 5 6 7
next :0 0 0 0 1 2 3 1
T[j]=T[j] next[i] = j+1
  
最后得到 next[] = {0,0,0,0,1,2,3,1}

C语言的next数组实现如下:

void get_next(String T,int next[]){
  int j = 0;    
  int i = 1;
  next[0] = 0;
  while(i<StrLength(T)){
    if(T[i] == T[j]){
      next[i] = j + 1;
      ++j;
      ++i;
    }
    else{
      if(j!=0){
        j = next[j-1];
      }
      else{
        next[i] = 0;
        ++i;
      }
    }
  }
}

有了next数组,我们就可以知道每当KMP匹配过程中,一旦匹配失败,我们就令指针 j = next[j-1] ,然后继续与S[i]比较。

KMP完整算法如下:

int KMP(String S,String T){
  int length_S = StrLength(S);
  int length_T = StrLength(T);
  int next[length_T];
  get_next(T,next);
  int i = 0;
  int j = 0;
  while(j<length_T && i<length_S){
    if(T[j] == S[i]){
      ++i;
      ++j;
    }
    else{
      if(j!=0)
        j = next[j-1];
      else
        ++i;
    }
  }
  if(j==length_T) return i - length_T +1;
  return 0;
}

文章参考

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

推荐阅读更多精彩内容