前言
KMP演算法是一種字串匹配演算法,其重中之重是next陣列的構建,其程式碼的簡潔與神奇使其廣受關注。
但不難發現,acm中學到的KMP和資料結構裡面學到的KMP並不一樣o(︶︿︶)o
之前我寫過acm版的KMP,戳這裡
現在寫一篇資料結構版的KMP,便於應對即將到來的資料結構考試(艹
手撕next陣列
先來複習一下acm版next陣列:next[i]是部分匹配值,也就是字首和字尾的最長共有元素的長度
而資料結構版的next陣列指的是當匹配失效的時候,匹配串的 j 指標應該指向的位置(即next[j])
這兩種本質上來說,失配的時候都是指向next[j],但是由於acm輸入的字串的下標是從0開始,而資料結構都是從1開始,所有會有差別滴
這裡主要介紹在考試的時候給你一個字串時如何快速滴手撕next陣列
先看一下next陣列的公式:
這種鳥公式傻子才用
正解:
- 首先對於前兩個:next[1] = 0; next[2] = 1;(注意,下標從1開始)
- 後面每一位的next值求解:根據前一位進行比較
- 將前一位的字元 與前一位的next值作為下標對應的字元進行比較
- 相等,則該位的next值就是前一位的next值加上1
- 不等,向前繼續尋找next值對應的內容來與前一位進行比較,直到找到某個位上內容的next值對應的內容與前一位相等為止,則這個位對應的值加上1即為需求的next值
- 若找到第一位都不匹配,則改為的next值為1。
舉個例子:abaabcac
-
next[1] = 0
-
next[2] = 1
-
求next[3] 則去判斷前一位的字元與前一位的next對應的字元,發現不相同,此時已經匹配到了第一位,還不相同,則next值為1
S[2] != S[next[2]], 且匹配到了第一位,故next[3] = 1
aba
[0, 1, 1]
-
求next[4]則去判斷前一位字元a 與 前一位next[3] 對應的字元a比較,發現相同,則next[4] = next[3] + 1 = 2
S[3] = S[next[3]], 故S[4] = S[3] + 1 = 2
abaa
[0, 1, 1, 2]
-
求next[5] 則去判斷前一位(4)的a與前一位(4)的next[4]對應的字元b相比,發現不同,就繼續用前一位(4)的字元a 與 next[4]對應的字元的next值(2)對應的字元a比較,發現相同,則next[5] = next[next[4]] + 1, 也就是next[5] = next[2] + 1 = 2
S[4] != S[next[4]] --->. S[4] = S[next[next[4]]], 故 next[5] = next[next[4]] + 1 = 2
abaab
[0, 1, 1, 2, 2]
-
求next[6] 則去判斷第五位的b與第五位的next值對應的字元b,發現相同,則next[6] = next[5] + 1
S[5] = S[next[5]], 故next[6] = next[5] + 1 = 3
abaabc
[0, 1, 1, 2, 2, 3]
-
求next[7] 則去判斷第6位的c與第next[6]位對應的字元,發現不同,就拿第6位的c與第next[next[6]]對應的 a 相比, 發現不同,且匹配到了第一位,故next[7] = 1
S[6] != S[next[6]]--->next[6] != S[next[next[6]]], 且next[next[6]] = 1,即匹配到第一位還不同,則next[7] = 1
abaabca
[0, 1, 1, 2, 2, 3, 1]
-
求next[8] 則去判斷第7位的a 與 next[7]對應的a比較, 發現相同,則next[8] = next[7] + 1
S[7] = S[next[7]], 故next[8] = next[7] + 1 = 2
abaabcac
[0, 1, 1, 2, 2, 3, 1, 2]
手撕nextval陣列
nextval陣列是對next陣列的優化版
例如:
匹配串S:aaaab
模式串T:aaabaaaab
匹配串的 next[] = {0,1, 2, 3, 4}
當匹配串與模式串在第四個位置失配時,指向模式串的 i 是不變的,指向匹配串的 j 是需要變成next[j] ,就需要將 T[4] 與 S[3]進行比較,會發現,還是不同,就讓指標 j 繼續跳,一值下去,會發現 T[4] 與 S[3] S[2] S[1] 都進行了比較,但我們之間觀察的話會發現,S[1] = S[2] = S[3] = S[4] = a,根據S[4] != T[4],故S[1] 、S[2] 、S[3] 都不等於T[4],相當於這三次比較毫無卵用,這就是next陣列需要優化的地方,故提出了nextval陣列來優化
手撕nextval陣列有兩個方法:
法1.試想法:
試想匹配串S與模式串T在第 i 位(1<= i <= S.size())失配時,看看在最優的情況下,匹配串的頭能與模式串的尾能重疊的長度最大為多少,其實也就是偏移量(設S[1] 移動到 i + 1位置表示的偏移量為0,S[1] 移動到 i 位置表示的偏移量為1,以此類推)
拿aaaab舉個例子:
-
nextval[1] = 0
-
當第二個字元失配,說明第一個字元是完全相同
S:aa
T:aXYYYYYY(X為非a的任意字元, Y為任意字元)
我們從T的第二位開始與S拿去比較:
aXYYYY
aa
由於X不為a,故匹配失敗,繼續從T的第三位開始與S進行匹配
因為從第三位開始都是X,故T有可能是aXaa……,也就能匹配成功,再根據我們上面假設的偏移量的定義,得到偏移量為0
-
第三個字元失配與第二個相同, nextval[3] = 0
-
第四個字元失配與第二個相同, nextval[4] = 0
-
當第五個字元失配時,說明前四個肯定完全相同,故:
S:aaaab
T:aaaaXYYYY……(X
同樣的,我們從第二位開始比較,會發現:S[2] = T[2],S[3] = T[3],S[4] = T[4], 對於T[5] 他除了b以外都可以取,所以可以取a,則第五位也可以匹配,就匹配成功
aaaaXYYYYY
aaaab
此時偏移長度為4(偏移串:aaaa)
故nextval[5] = 4
法2:藉助next陣列求nextval
總的來說:不同則為next值,想同則繼續往前比較,直到找到不同或第一位,跑到了第一個則為0
舉個上面講過的第一個的例子來解釋:
abaabcac
next= {0, 1, 1, 2, 2, 3, 1, 2}
- nextval[1] = 0
- S[2] != S[next[2]], 故nextval[2] = next[2] = 1
- S[3] = S[next[3]] ---> 跑到了第一個,故nextval[3] = 0
- S[4] != S[next[4]], 故nextval[4] = next[4] = 2
- S[5] = S[next[5]] ---> S[next[5]] != S[next[next[5]]], 故S[5] = next[next[5]] = 1
- S[6] != S[next[6]], 故 nextval[6] = next[6] = 3
- S[7] = S[next[7]], 且跑到了第一個位置,故next[7] = 0
- S[8] != S[next[8]],故nextval[8] = next[8] = 2
對於這兩種方法,個人感覺法二簡單多遼,不過前提是得將next陣列算出來,且必須要算的正確,不然直接涼涼(>_<)
這裡再貼出next陣列和nextval陣列的程式碼:
void getnext(string s){
s = " " + s;//因為next陣列從1開始,串從0開始,所以加個空格字首
int i = 1, j = 0;
nextt[1] = 0;
while (i < s.size()) {
if(j == 0 || s[i] == s[j]){
nextt[++i] = ++j;
}
else j = nextt[j];
}
}
void getnextval(string s){
s = ' ' + s;//道理同上
int i = 1, j = 0;
nextval[1] = 0;
while (i < s.size()) {
if(j == 0 || s[i] == s[j]){
++i;++j;
if(s[i] != s[j])nextval[i] = j;
else nextval[i] = nextval[j];
}
else j = nextval[j];
}
}
我絕對不是資料結構課上因為摸魚沒聽課,才過來寫部落格滴⁄(⁄ ⁄ ⁄ω⁄ ⁄ ⁄)⁄