KMP演算法(Leetcode第28題)

okumiko發表於2020-12-07

實現 strStr() 函式。

給定一個 haystack 字串和一個 needle 字串,在 haystack 字串中找出 needle 字串出現的第一個位置 (從0開始)。如果不存在,則返回 -1。

示例 1:

輸入: haystack = “hello”, needle = “ll”
輸出: 2

示例 2:

輸入: haystack = “aaaaa”, needle = “bba”
輸出: -1

說明:

needle 是空字串時,我們應當返回什麼值呢?這是一個在面試中很好的問題。

對於本題而言,當 needle 是空字串時我們應當返回 0 。這與C語言的 strstr() 以及 Java的 indexOf() 定義相符。

int *getNext(const string &s) {
    int *next = new int[s.length()];
    next[0] = -1;
    if (s.length() == 1) return next;//如果子串只有一位,直接輸出next陣列
    next[1] = 0;//第二位固定是0,就是要回溯到首位
    int i = 1, j = 0;
    while (i < s.length() - 1) {//每次要確定的其實是i+1處不匹配,要移動到何處
        while (j > 0 && s[i] != s[j]) {
            j = next[j];//字首和字尾匹配與子串和主串匹配很相似
        }
        if (s[i] == s[j]) {//如果這一位相等,字首後移一位
            ++j;
        }
        ++i;//確定i+1處不匹配的情況
        next[i] = j;//如果是因為相等跳出遞迴,移動到j+1位,如果是因為遞迴到了0位跳出遞迴,移動到0位
    }
    return next;
}

int strStr(string haystack, string needle) {
    if (needle.empty()) return 0;
    int j = 0;
    int *next = getNext(needle);
    for (int i = 0; i < haystack.length(); ++i) {//遍歷主串
        while (j > 0 && haystack[i] != needle[j]) {
            j = next[j];//遞迴呼叫next陣列,直到回溯到0處或回溯到相等的位置
        }
        if (haystack[i] == needle[j]) {//如果這一位相等,子串對應位置後移
            ++j;
        }
        if (j == needle.length()) {//因為前面進行了後移操作,所以這裡比較的是length而不是length-1
            return i - j + 1;
        }
    }
    return -1;
}

以下文字轉載自:www.cnblogs.com/tangzhengyue/p/431...
作者:唐小喵

KMP的next陣列求法是很不容易搞清楚的一部分,也是最重要的一部分。我這篇文章就以我自己的感悟來慢慢推導一下吧!保證你看完過後是知其然,也知其所以然。

如果你還不知道KMP是什麼,請先閱讀上面的連結,先搞懂KMP是要幹什麼。
下面我們就來說說KMP的next陣列求法。
KMP的next陣列簡單來說,假設有兩個字串,一個是待匹配的字串strText,一個是要查詢的關鍵字strKey。現在我們要在strText中去查詢是否包含strKey,用i來表示strText遍歷到了哪個字元,用j來表示strKey匹配到了哪個字元。
如果是暴力的查詢方法,當strText[i]和strKey[j]匹配失敗的時候,i和j都要回退,然後從i-j的下一個字元開始重新匹配。
而KMP就是保證i永遠不回退,只回退j來使得匹配效率有所提升。它用的方法就是利用strKey在失配的j為之前的成功匹配的子串的特徵來尋找j應該回退的位置。而這個子串的特徵就是前字尾的相同程度。
所以next陣列其實就是查詢strKey中每一位前面的子串的前字尾有多少位匹配,從而決定j失配時應該回退到哪個位置。

我知道上面那段廢話很難懂,下面我們看一個彩圖:

這個圖畫的就是strKey這個要查詢的關鍵字字串。假設我們有一個空的next陣列,我們的工作就是要在這個next陣列中填值。
下面我們用數學歸納法來解決這個填值的問題。
這裡我們借鑑數學歸納法的三個步驟(或者說是動態規劃?):

  1. 初始狀態
  2. 假設第j位以及第j位之前的我們都填完了
  3. 推論第j+1位該怎麼填

初始狀態我們稍後再說,我們這裡直接假設第j位以及第j位之前的我們都填完了。也就是說,從上圖來看,我們有如下已知條件:
next[j] == k;
next[k] == 綠色色塊所在的索引;
next[綠色色塊所在的索引] == 黃色色塊所在的索引;
這裡要做一個說明:圖上的色塊大小是一樣的(沒騙我?好吧,請忽略色塊大小,色塊只是代表陣列中的一位)。

我們來看下面一個圖,可以得到更多的資訊:

  1. 由”next[j] == k;”這個條件,我們可以得到A1子串 == A2子串(根據next陣列的定義,前字尾那個)。

  2. 由”next[k] == 綠色色塊所在的索引;”這個條件,我們可以得到B1子串 == B2子串。

  3. 由”next[綠色色塊所在的索引] == 黃色色塊所在的索引;”這個條件,我們可以得到C1子串 == C2子串。

  4. 由1和2(A1 == A2,B1 == B2)可以得到B1 == B2 == B3。

  5. 由2和3(B1 == B2, C1 == C2)可以得到C1 == C2 == C3。

  6. B2 == B3可以得到C3 == C4 == C1 == C2

上面這個就是很簡單的幾何數學,仔細看看都能看懂的。我這裡用相同顏色的線段表示完全相同的子陣列,方便觀察。

接下來,我們開始用上面得到的條件來推導如果第j+1位失配時,我們應該填寫next[j+1]為多少?

next[j+1]即是找strKey從0到j這個子串的最大前字尾:

#:(#:在這裡是個標記,後面會用)我們已知A1 == A2,那麼A1和A2分別往後增加一個字元後是否還相等呢?我們得分情況討論:

  1. 如果str[k] == str[j],很明顯,我們的next[j+1]就直接等於k+1。

  用程式碼來寫就是next[++j] = ++k;

  1. 如果str[k] != str[j],那麼我們只能從已知的,除了A1,A2之外,最長的B1,B3這個前字尾來做文章了。

那麼B1和B3分別往後增加一個字元後是否還相等呢?

由於next[k] == 綠色色塊所在的索引,我們先讓k = next[k],把k挪到綠色色塊的位置,這樣我們就可以遞迴呼叫”#:”標記處的邏輯了。

由於j+1位之前的next陣列我們都是假設已經求出來了的,因此,上面這個遞迴總會結束,從而得到next[j+1]的值。

我們唯一欠缺的就是初始條件了:

next[0] = -1, k = -1, j = 0

另外有個特殊情況是k為-1時,不能繼續遞迴了,此時next[j+1]應該等於0,即把j回退到首位。

即 next[j+1] = 0; 也可以寫成next[++j] = ++k;

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章