要解決的問題

假設字串str長度為N,字串match長度為M,M <= N, 想確定str中是否有某個子串是等於match的。返回和match匹配的字串的首字母在str的位置,如果不匹配,則返回-1

OJ可參考:LeetCode 28. 實現 strStr()

暴力方法

從str串中每個位置開始匹配match串,時間複雜度O(M*N)

KMP演算法

KMP演算法可以用O(N)時間複雜度解決上述問題。

流程

我們規定陣列中每個位置的一個指標,這個指標定義為

這個位置之前的字元字首和字尾的匹配長度,不要取得整體。

例如: ababk 這個字串,k位置的指標為2, 因為k之前位置的字串為abab

字首ab 等於 字尾ab,長度為2,下標為3的b的指標為1,因為b之前的字串aba ,字首a 等於字尾a, 長度為1。

人為規定:0位置的指標是-1,1位置的指標0

假設match串中每個位置我們都已經求得了這個指標值,放在了一個next陣列中,這個陣列有助於我們加速整個匹配過程。

我們假設在某個時刻,匹配的到的字元如下

其中str的i..j一直可以匹配上match串的0...m, str中的x位置和match串中的y位置第一次匹配不上。如果使用暴力方法,此時我們需要從str的i+1位置重新開始匹配match串的k位置,而KMP演算法,利用next陣列,可以加速這一匹配過程,具體流程是,依據上例,我們可以得到y位置的next陣列資訊,假設ynext陣列資訊是2,如下圖

如果ynext陣列資訊是2,那麼0...k 這一段完全等於f...m這一段,那麼對於match來說,當y位置匹配不上x位置以後, 可以直接讓x位置匹配ynext陣列位置p上的值,如下圖

如果匹配上了,則x來到下一個位置,p來到下一個位置繼續匹配,如果再次匹配不上,假設p位置的next陣列值為0, 則繼續用x匹配pnext陣列位置0位置上的值,如下圖

如果x位置的值依舊不等於0位置的值,則宣告本次匹配失敗,str串來到x下一個位置,match串從0位置開始繼續匹配。

next陣列求解

next陣列的求解是KMP演算法中最關鍵的一步,要快速求解next陣列,需要做到當我們求i位置的next資訊時,能通過i-1next陣列資訊加速求得,如下圖

當我們求i位置的next資訊時,假設j位置的next資訊為6,則表示

m...n這一段字串等於s...t這一段字元,此時可以得出一個結論,如果:

x位置上的字元等於j位置上的字元,那麼i位置上的next資訊為j位置上的next資訊加1,即為7。如果不等,則繼續看x位置上的next資訊,假設為2,則有:

此時,判斷q位置的值是否等於j位置的值,如果相等,那麼i位置上的next資訊為x位置上的next資訊加1,即為3,如果不等,則繼續看q位置上的next資訊,假設為1,那麼有

此時,判斷p位置的值是否等於j位置的值,如果相等,那麼i位置上的next資訊為q位置上的next資訊加1,即為2,如果不等,則繼續如上邏輯,如果都沒有匹配上j位置的值,則i位置的next資訊為0。

主流程程式碼複雜度估計

public class LeetCode_0028_ImplementStrStr {
public static int strStr(String str, String match) {
if (str == null || match == null || match.length() > str.length()) {
return -1;
}
if (match.length() < 1) {
return 0;
}
char[] s = str.toCharArray();
char[] m = match.toCharArray();
int l = m.length;
int[] next = getNextArr(m, l);
int x = 0;
int y = 0;
while (y < s.length && x < l) {
if (s[y] == m[x]) {
y++;
x++;
} else if (x != 0) {
x = next[x];
} else {
y++;
}
}
return x == l ? y - x : -1;
} // 求解next陣列邏輯
private static int[] getNextArr(char[] str, int l) {
if (l == 1) {
return new int[]{-1};
}
int[] next = new int[l];
next[0] = -1;
next[1] = 0;
int i = 2; // 目前在哪個位置上求next陣列值
int cn = 0; // 前後綴最長字元的長度,也表示下一個要比的資訊位置
while (i < next.length) {
if (str[i - 1] == str[cn]) {
next[i++] = ++cn;
} else if (cn > 0) {
cn = next[cn];
} else {
next[i++] = 0;
}
}
return next;
}
}

next陣列的求解流程時間複雜度顯然為O(N),現在估計主流程的複雜度,主流程中,x能取得的最大值為str字串的長度N,定義一個變數x-y,能取得的最大值不可能超過N(即當x = N,y=0時候),在主流程的wile迴圈中,有三個分支

        while (y < s.length && x < l) {
if (s[y] == m[x]) {
y++;
x++;
} else if (x != 0) {
x = next[x];
} else {
y++;
}
}

我們考慮這三個分支對於yy - x變化範圍的影響

分支 y y - x
x++; y++ 推高 不變
x = next[x] 不變 推高
y++ 推高 推高

如上分析,yy-x都不可能降低,且三個分支只能中一個,所以,而yy-x的最大值均為N,所有分支執行總推高的次數不可能超過2N。即得出主流程的複雜度O(N)

KMP演算法應用

求一個字串的旋轉詞(詳見:LeetCode 796)

思路

將這個字串拼接一下, 比如原始串為:123456,拼接成:123456123456

如果匹配的字串是這個拼接的字串的子串,則互為旋轉詞。

一棵二叉樹是否為另外一棵二叉樹的子樹(詳見:LeetCode 572)

思路

先將兩棵樹分別序列化為陣列A和陣列B,如果B是A的子串,那麼A對應的二叉樹中一定有某個子樹的結構和B對應的二叉樹完全一樣。

更多

演算法和資料結構筆記

參考資料