KMP算法那些事

KMP算法簡介
KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的,稱之爲 Knuth-Morria-Pratt 算法,簡稱 KMP 算法。該算法相對於 Brute-Force(暴力)算法有比較大的改進,主要是消除了主串指針的回溯,從而使算法效率有了某種程度的提高。

下面開始分步講解一下KMP算法的模式

	從模式串t中提取加速匹配的信息

在KMP算法中,通過分析模式串t,從中提取出加速匹配的有用信息。這種信息是對於每一個t的每個字符tj(0<=j<=m-1)存在一個整數k(k<j),使得模式串t中開頭的k個字符依次與tj的前面k個字符相同,如果k有多個值,k取其中最大的一個值。模式串t中的每一個位置j的字符都有這樣的信息,採用next數組表示,即是next[j]=Max(k);

下面來舉一個例子
模式串t=“aaab" ,對於j=3,就是模式串中下標爲3的字符t3=”b“,觀察前面的字符串,可以得到t2=t0=“a”。

  1. a aab 加了刪除線的爲t0
  2. aaa b 加了刪除線的爲t2

· 可得 k=1;

  1. aa ab 加了刪除線的爲t0t1
  2. aaa b 加了刪除線的爲t1t2

可得k=2;

所以next[3]=Max{1.2}=2;

歸納next數組取值的規律
在這裏插入圖片描述

next的求解過程如下(圖文並茂)

  1. next[0]=-1,next[1]=0,
  2. 如果next[j]=k,表示有”t0t1…tk-1“=“tj-ktj-k+1…tj-1”
    1.如果tk=tj, 即是”t0t1…tk-1tk“=“tj-ktj-k+1…tj-1tj”,顯然有next[j+1]=k+1.
    2.如果tk!=tj,說明tj之前不存在長度爲next[j]+1的字串和開頭字符起的子串相同,那麼是否存在一個 長度較短的子串和開頭字符起的子串相同呢?設k‘=next[k],則下一步是應該將tj與tk’比較:若兩者相同,則說明tj之前存在長度爲next[k’]+1的子串和開頭字符起的子串相同;否則依次類推尋找更短的子串,直到不存在可匹配的子串,置next[j+1]=0。所以當tk!=tj,置k=next[k]。

對應的代碼如下:

void getnext(sqstring t,int next[])
{
 int j,k;
 j=0;k=-1;next[0]=-1;
 while(j<t.length-1)
 {
  if(k==-1||t.data[j]==t.data[k])
  {
   j++;k++;
   next[j]=k;
  }
  else
  k=next[k];
 }
}
	KMP算法的模式匹配過程

求出模式串t的next數組後,你也許還不明白next數組的真正作用是什麼。但是下面我會一一講解,先總結一下,它是用來消除主串指針的回溯。下面我來舉一個例子。
以目標串s=“aaaaab” 模式串t=“aaab”
第一趟匹配:從i=0,j=0,開始,不匹配處是i=3,j=3,雖然這次匹配失敗了,可以得到部分匹配信息:s1s2與t1t2相同,如圖所示
在這裏插入圖片描述

而模式串t中有next[3]=2,表明t1t2=t0t1,所以有s1s2=t0t1;
在這裏插入圖片描述

原來第二趟匹配是需要從i=1,j=0開始的,即需要回溯,現在既然有s1s2=t0t1,第二趟匹配可以從i=3,j=2開始,即保持主串指針i不變,模式t右滑動1(=j-next[3])個位置,讓si和tnext[j]對齊進行比較。
在這裏插入圖片描述

下面來討論一般情況。設置目標串s=“s0s1…sn-1”,模式串t=“t0t1…tm-1”,在進行第i-j+1趟時出現的不匹配情況

  目標串s:  s0  s1....si-j  si-j+1 .. si-1    **si**     si+1.....sn-1
  							
  模式串t:             t0     t1.....tj-1     **tj**     tj+1......tm-1

發生不匹配的爲si!=tj,這時候發生部分匹配是“t0t1…tj-1”=“si-jsi-j+1…si-1”,

顯然在k<j時有:tj-ktj-k+1…tj-1=si-ksi-k+1…si-1
在這裏插入圖片描述

因爲next[j]=k,即:

t0t1…tk-1=tj-k tj-k+1…tj-1

由上面兩個公式可得“t0t1…tk-1”=“si-ksi-k+1…si-1”,
在這裏插入圖片描述

下一趟就不在從si-j+1開始匹配,而是從si-k開始匹配,並且直接將si與tk進行比較,這樣就可以把i-j+1趟比較失敗時的模式串t從當前位置直接右滑動j-k個字符,如下圖所示。
在這裏插入圖片描述

上述過程中,從第i-j+1趟直接轉到第i-k+1趟匹配,中間可能遺漏一些匹配趟數,那麼KMP算法是否對呢?實際上,因爲next數組next[j]=k,容易證明中間的匹配趟數是沒有必要的。

下面我們通過一個實例驗證,設目標串s=“s0 s1 s2 s3 s4 s5 s6”,模式串t=“t0 t1 t2 t3 t4 t5”,next[5]=2,從s1開始匹配,不匹配處爲s6!=t5,這裏 i=6,j=5,k=2,下面說明第i-j+2(=3)到第i-k(=4)趟是不必要的。

如下圖所示,部分匹配信息有“t1t2t3t4”=“s2s3s4s5“,因爲next[5]=2,有”t0t1“=“t3t4”,同時有”t1t2t3t4“!=“t1t2t3t4”(若不相同,則next[5]=4,而不是2),從而推出”s2s3s4s5“!=“t0t1t2t3”。所以從s2開始匹配是不必要的。
在這裏插入圖片描述

同樣的道理,因爲next[5]=2,有“t0t1t2”!=“t2t3t4”,(若相同則next[5]=3),推出從s3開始匹配是不必要的,下一趟應該從s4開始匹配,而且是直接將s6與t2進行比較。
所以,當模式串t中t0與目標串s中某個字符si不匹配時,用next[0]=-1,表示t中已經沒有字符與當前字符si進行比較了。i應該移動到目標串s的下一字符,再和模式串t中的第一個字符進行比較。

KMP算法的過程如下:

int KMPIndex(sqstring s,sqstring t)
{
 int i=0,j=0;
 int next[MaxSize];
 getnext(t,next);
 while(i<s.length&&j<t.length)
 {
  if(j==-1||s.data[i]==t.data[j])
  {
   i++;
   j++;
  }
  else
  j=next[j];
 }
 if(j>=t.length)
 return i-t.length;
 else
 return -1;
}

設主串s的長度爲n,子串t的長度爲m,KMP算法的平均時間複雜度爲O(n+m)。

		改進後的KMP算法

首先我們來看一下上面算法的缺陷
在這裏插入圖片描述

上述的模式串的next[4]={-1,0,0,1}; 當比較到i,j時發生不匹配,此時next[j]=1,所以應該將i保持不變,j移動到1的位置。
在這裏插入圖片描述

可以發現這裏明顯還是不匹配,完全沒有意義,因爲前一個步驟中後面的B已經不匹配了,前面一個B也是不可能匹配的,同樣的情況其實還發生在第2個元素A上。

這裏我們可以觀察到問題的原因:t[j]==t[next[j]]
這也就是說,按照我們前面的原因得到next[j]=k,在模式串中有t[j]=t[k],當目標串中的字符s[i]與模式串中的t[j],比較不同時,s[i]!=t[k],所以沒有必要再將s[i]和t[k]進行比較,而是直接將s[i]與t[next[k]]進行比較,爲此這裏我們把next[j]修改爲nextval[j].
nextval數組的定義是nextval[0]=-1,當t[j]=t[next[j]],nextval[j]=nextval[next[j]],否則nextval[j]=next[j];

用nextval取代next,得到改進的KMP算法如下:

void Getnextval(int nextval[],String t)
{
   int j=0,k=-1;
   nextval[0]=-1;
   while(j<t.length)
   {
      if(k == -1 || t[j] == t[k])
      {
         j++;k++;
         if(t[j]!=t[k])//當兩個字符相同時,就跳過
            nextval[j] = k;
         else
            nextval[j] = nextval[k];
      }
      else k = nextval[k];
   }
}

int KMPIndex(sqstring s,sqstring t)
{
int nextval[Maxsize];
 int i=0,j=0;
 getnext(t,nextval);
 while(i<s.length&&j<t.length)
 {
  if(j==-1||s.data[i]==t.data[j])
  {
   i++;
   j++;
  }
  else
  j=nextval[j];
 }
 if(j>=t.length)
 return i-t.length;
}
else
return -1;

算法複雜度與前者一樣都爲O(n+m)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章