KMP(梅開三度之資料結構詳解版

扇與她盡失發表於2021-04-30

前言

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

  1. next[1] = 0

  2. next[2] = 1

  3. 求next[3] 則去判斷前一位的字元與前一位的next對應的字元,發現不相同,此時已經匹配到了第一位,還不相同,則next值為1

    S[2] != S[next[2]], 且匹配到了第一位,故next[3] = 1

    aba

    [0, 1, 1]

  4. 求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]

  5. 求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]

  6. 求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]

  7. 求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]

  8. 求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舉個例子:

  1. nextval[1] = 0

  2. 當第二個字元失配,說明第一個字元是完全相同

    S:aa

    T:aXYYYYYY(X為非a的任意字元, Y為任意字元)

    我們從T的第二位開始與S拿去比較:

    aXYYYY

    aa

    由於X不為a,故匹配失敗,繼續從T的第三位開始與S進行匹配

    因為從第三位開始都是X,故T有可能是aXaa……,也就能匹配成功,再根據我們上面假設的偏移量的定義,得到偏移量為0

  3. 第三個字元失配與第二個相同, nextval[3] = 0

  4. 第四個字元失配與第二個相同, nextval[4] = 0

  5. 當第五個字元失配時,說明前四個肯定完全相同,故:

    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}

  1. nextval[1] = 0
  2. S[2] != S[next[2]], 故nextval[2] = next[2] = 1
  3. S[3] = S[next[3]] ---> 跑到了第一個,故nextval[3] = 0
  4. S[4] != S[next[4]], 故nextval[4] = next[4] = 2
  5. S[5] = S[next[5]] ---> S[next[5]] != S[next[next[5]]], 故S[5] = next[next[5]] = 1
  6. S[6] != S[next[6]], 故 nextval[6] = next[6] = 3
  7. S[7] = S[next[7]], 且跑到了第一個位置,故next[7] = 0
  8. 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];
    }
}

我絕對不是資料結構課上因為摸魚沒聽課,才過來寫部落格滴⁄(⁄ ⁄ ⁄ω⁄ ⁄ ⁄)⁄

相關文章