作者:Grey
原文地址: KMP算法解決字符串匹配問題
要解決的問題
假設字符串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對應的二叉樹完全一樣。