KMP演算法中我對獲取next陣列的理解

onlyblues發表於2021-05-31

  之前在學KMP演算法時一直理解不了獲取next陣列的函式是如何實現的,現在大概知道怎麼一回事了,記錄一下我對獲取next陣列的理解。

  KMP演算法實現的原理就不再贅述了,先上KMP程式碼:

 1 void getNext(char *pat, int *next) {
 2     next[0] = -1;
 3     int m = strlen(pat);
 4     for (int i = 1; i < m; i++) {
 5         int j = next[i - 1];
 6         while (j != -1 && pat[j] != pat[i - 1]) {
 7             j = next[j];
 8         }
 9         next[i] = j + 1;
10     }
11 }
12 
13 int KMP(char *str, char *pat) {
14     int n = strlen(str), m = strlen(pat);
15     int next[m];
16     getNext(pat, next);
17     
18     int i = 0, j = 0;
19     while (i < n && j < m) {
20         if (j == -1 || str[i] == pat[j]) i++, j++;
21         else j = next[j];
22     }
23     
24     return j == m ? i - j : -1;
25 }

  先說明,為了描述更清晰,我特意在主串和模式串後面緊接str,pat,s,p等標識,以區分不同的主串和模式串。

  我對next陣列的定義是這樣的,next[j]存放的值是模式串pat的某個位置,而這個位置是:在匹配過程中,模式串pat在 j 這個位置發生失配時,接下來要與主串str進行匹配的位置。也就是說next[j]存放的值是模式串pat在 j 這個位置發生失配時回退到的位置,然後從這個位置開始繼續與主串str比較。

  而根據KMP演算法的定義,當發生失配時,主串的指標 i 不需要移動,而只需要移動模式串pat的指標 j 。這是因為主串str和模式串pat有部分是相同的,也就是在模式串pat中,next[j]這個位置之前的部分(不包括next[j]這個位置)和主串str在 i 位置之前對應的部分(不包括i這個位置)是相同的。

  而要獲取模式串pat每個位置對應的next值,一個很重要的思想是,把模式串pat既看作是主串s,也看作是模式串p。這裡所說的主串s和模式串p都是指同一字串——模式串pat。

  所以如果我們要獲取模式串pat中某個位置 i 的next值,可以理解為當模式串pat在 i 這個位置發生失配時,應該回退到的那個位置,也就是next[i]這位置。

  這時,我們把模式串pat從 0 到 i 這部分暫時理解為主串s,然後把模式串pat最前面的部分也就是從 0 到某個位置 j (其中j < i - 1)這部分理解為模式串p。我們要得到next[i],就要知道 j 這個位置,使得模式串p從位置 0 到 j ,與主串s從位置 i - 1 開始之前的相同數量(j + 1)的那部分完全匹配。也就是滿足 p0 p1 ... pj == si-1-j si-j ... si-1 。所以,在匹配時,當模式串pat在 i 這個位置發生失配,就可以回退到 j + 1 這個位置,再從 j + 1 這個位置繼續與主串str比較。這樣,就知道 next[i] = j + 1 。

  舉個例子:

  怎麼去找到模式串p中 j 那個位置呢?其實很簡單,一開始就為 j 賦初值 j = next[i - 1] 。這裡再重複一遍,next[i - 1]是模式串pat在 i - 1 這個位置發生失配時,應該回退到的位置。接著我們比較判斷模式串pat在 i - 1 這個位置的字元是否與在 j 這個位置的字元相同。也就是主串s在 i - 1 這個位置是否與模式串p在 j這個位置的字元相同。即 pat[i - 1] == pat[j] ? 或是 s[i - 1] == p[j] ? 。

  如果相同,那麼直接就有 next[i] = j + 1 。否則我們可以理解為模式串p在 j 這個位置發生失配,就要進行回退。回退到哪裡呢?當然是回退到next[j]了。所以說,如果發生失配,模式串p的指標j就一直回退,即有 j = next[j] ,直到滿足i - 1這個位置與j這個位置的字元相同,才停止回退匹配,然後同樣有會 next[i] = j + 1 。

  想一下,如果在回退的過程中,始終沒有發現匹配成功的情況,難道一直這樣回退下去嗎?答案肯定不是的。我們有個終止條件,就是一開始就規定 next[0] = -1 ,意味著當在模式串pat在 0 這個位置,都與不能夠與主串str匹配,那麼就沒有再可以回退的位置了,這時主串str的指標與模式串pat的指標都要向後移動1位,然後繼續匹配。

  所以當發現 j == -1 時,就停止回退,說明當模式串pat在 i 這個位置發生失配時,只能夠回退到 0 這個位置繼續與主串str匹配。同時讓主串s指標 i 與模式串p指標 j 同時向後移動1位。這時主串s指標在 i + 1 這個位置,模式串p指標在 j + 1 這個位置,也就是 0 這個位置,然後繼續求後面位置的next值。

  好了,現在我已經把我對KMP演算法中獲取next陣列的理解表述完了,我們回到程式碼中看看。

 1 void getNext(char *pat, int *next) {
 2     next[0] = -1;       // 一開始確定回退最終條件 
 3     int m = strlen(pat);
 4     
 5     // 把模式串pat同時看作主串s和模式串p 
 6     for (int i = 1; i < m; i++) {   // i就是主串s的指標 
 7         int j = next[i - 1];        // j就是模式串p的指標 
 8         
 9         // 我們就是要找到模式串p的某個位置j,使得模式串p從0到j這部分,與主串s從位置i - 1到前面的i - 1 - j的那部分完全匹配  
10         while (j != -1 && pat[j] != pat[i - 1]) {   // 只要j != -1,也就是還可以回退,同時與主串s的i - 1這個位置不匹配 
11             j = next[j];    // j就一直回退 
12         }
13         
14         // 其中如果因為j == -1而退出迴圈,意味著找不到可以匹配的位置,則模式串pat在i這個位置發生失配時,只能夠回退到最開始的位置0 
15         next[i] = j + 1;    // 不管最後是因為匹配成功還是無法回退而退出迴圈的,都有next[i] = j + 1
16     }
17 }

  還有另外一種寫法,是大多數人的寫法,其實實現的原理與我上述的幾乎相同,只不過程式碼寫起來不同,效率也相差不多,只不過每輪迴圈時,下面的程式碼會比上面的程式碼少了次 j = next[i - 1] 的賦值操作。

 1 void getNext(char *pat, int *next) {
 2     next[0] = -1;
 3     int m = strlen(pat);
 4     
 5     // 這裡的i並不是我們要求的next[i]中的那個i,而是要求next值那個位置的前一個位置
 6     // 這裡的j和上面程式碼的j相同,都是使得從0到j這部分,與從上面的那個i開始到i - j的那部分完全匹配 
 7     for (int i = 0, j = -1; i < m - 1; ) {  // 由於這裡的i是指要求next值那個位置的前一個位置,所以i最大為m - 2 
 8     
 9         // 如果發現匹配,就知道i的下一個位置i + 1的next值,也就是next[i + 1] = j + 1
10         // 如果j無法回退,i + 1的next值就為0,同樣可以表示為next[i + 1] = j + 1
11         // 每次得到next[i + 1],都會從++後的位置,也就是i + 1和j + 1這個位置繼續接下來的匹配 
12         if (j == -1 || pat[i] == pat[j]) next[++i] = ++j;
13         else j = next[j];   // 失配j就回退 
14     }
15 }

相關文章