要解決的問題
假設字串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
陣列資訊,假設y
的next
陣列資訊是2,如下圖
如果y
的next
陣列資訊是2,那麼0...k
這一段完全等於f...m
這一段,那麼對於match來說,當y
位置匹配不上x
位置以後, 可以直接讓x
位置匹配y
的next
陣列位置p
上的值,如下圖
如果匹配上了,則x
來到下一個位置,p
來到下一個位置繼續匹配,如果再次匹配不上,假設p
位置的next陣列值為0, 則繼續用x
匹配p
的next
陣列位置0
位置上的值,如下圖
如果x
位置的值依舊不等於0
位置的值,則宣告本次匹配失敗,str串來到x
下一個位置,match串從0
位置開始繼續匹配。
next陣列求解
next
陣列的求解是KMP演算法中最關鍵的一步,要快速求解next
陣列,需要做到當我們求i
位置的next
資訊時,能通過i-1
的next
陣列資訊加速求得,如下圖
當我們求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++;
}
}
我們考慮這三個分支對於y
和y - x
變化範圍的影響
分支 | y | y - x |
---|---|---|
x++; y++ | 推高 | 不變 |
x = next[x] | 不變 | 推高 |
y++ | 推高 | 推高 |
如上分析,y
和y-x
都不可能降低,且三個分支只能中一個,所以,而y
和y-x
的最大值均為N,所有分支執行總推高的次數不可能超過2N。即得出主流程的複雜度O(N)
KMP演算法應用
求一個字串的旋轉詞(詳見:LeetCode 796)
思路
將這個字串拼接一下, 比如原始串為:123456,拼接成:123456123456
如果匹配的字串是這個拼接的字串的子串,則互為旋轉詞。
一棵二叉樹是否為另外一棵二叉樹的子樹(詳見:LeetCode 572)
思路
先將兩棵樹分別序列化為陣列A和陣列B,如果B是A的子串,那麼A對應的二叉樹中一定有某個子樹的結構和B對應的二叉樹完全一樣。