先上需求
給定一個 str 字符串和一個 value 字符串,在 str 字符串中找出 value 字符串出現的第一個位置 (從0開始)。其實就是C語言的 strstr() 以及 Java的 indexOf() 實現原理。
輸入案例:str:"hello" value=“ll”
輸出案例:2
暴力解法
用一個長指針i跟短指針j分別指向主串跟模式串,從左往右,一個一個匹配,如果匹配,如果出現不匹配,那就把長指針移回第 i - j + 1位(假設下標從0開始),j移動到模式串的第0位,然後又重新開始這個步驟:
…
…
…
…
直到匹配成功,返回出去就好了
於是上代碼
/**
* 暴力破解法
* @param str 主串
* @param value 模式串
* @return 如果找到,返回在主串中第一個字符出現的下標,否則爲-1
*/
int match(string str, string value) {
int i = 0;
int j = 0;
while (i < str.length() && j < value.length()) {
if (str[i] == value[j]) {
// 當兩個字符相同,就比較下一個
i++;
j++;
} else {
// i後退,j重頭開始
i = i - j + 1;
j = 0;
}
}
if (j == str.length()) {
return i - j;
} else {
return -1;
}
}
這樣的方法也可以得到結果,但是時間複雜度來到了(n*m),n爲主串的長度,m爲模式串的長度,一般這種暴力匹配的方法就會被老闆暴力地辭退。
這時候Donald Knuth、Vaughan Pratt、James H. Morris三大高叟就想到了能不能不讓i指針不回退,只移動短指針j阿,於是就有了牛逼(頭痛)的KMP算法
KMP算法
由於動畫效果不好製作,貼上知乎的大佬講解,裏面有動畫的呈現
知乎KMP
於是有了我自己對KMP算法的理解
其實KMP算法的基本操作流程如下:
- 假設現在文本串 S 匹配到 i 位置,模式串 P 匹配到 j 位置 如果 j = -1,或者當前字符匹配成功(即 S[i] ==
P[j] ),都令 i++,j++,繼續匹配下一個字符; - 如果 j != -1,且當前字符匹配失敗(即 S[i] != P[j] ),則令 i 不變,j =
next[j]。此舉意味着失配時,模式串 P相對於文本串 S 向右移動了 j - next [j] 位 - 換言之,將模式串 P 匹配位置的 next 數組的值對應的模式串 P 的索引位置移動到未重複出現位置
例如這樣的小串,如果用暴力解法 主串就得從A後的BC重新開始,此時i指針指向主串的B,j指向主串的A
這種情況還不算特別慢,如果是在主串“SSSSSSSSSSSSSA”中查找“SSSSB”,比較到最後一個才知道不匹配,然後i回溯,這個的效率是顯然是最低的。其實用人類的思維,其實只要讓i不動j回到AB第一次重複後的下一個位置就可以
此時i還是原來的指向,只是短指針j指向了C
所以,整個KMP的重點就在於當某一個字符與主串不匹配時,我們應該知道j指針要移動到哪?
據說這是有公式的,翻了很久資料(搜索引擎),終於找到了關於KMP的公式:
Value[0 ~ k-1] == Value[j-k ~ j-1]
公式的推導:
當str[i] != Value[j]時
有str[i-j ~ i-1] == Value[0 ~ j-1]
由Value[0 ~ k-1] == Value[j-k ~ j-1]
必然:str[i-k ~ i-1] == Value[0 ~ k-1]
這時候我們就列個表,看看這幾個公式在作甚
模式串value子串對應的各個前綴後綴的公共元素的 最大長度表 下圖。
公共元素的最長重複元素就得到是2了
這時候我們只需要的到當前匹配不等的元素在模型串的位置,只需要回溯到上次不等的位置,並且將索引一起移動
將該步驟翻譯成計算機步驟如下
1)找出前綴pre,設爲pre[0~m];
2)找出後綴post,設爲post[0~n];
3)從前綴pre裏,先以最大長度的s[0~m]爲子串,即設k初始值爲m,跟post[n-m+1~n]進行比較:
如果相同,則pre[0~m]則爲最大重複子串,長度爲m,則k=m;
如果不相同,則k=k-1;縮小前綴的子串一個字符,在跟後綴的子串按照尾巴對齊,進行比較,是否相同。
如此下去,直到找到重複子串,或者k沒找到。
這時候就引進了一個next數組來解決最大長度值的問題,並且要給初始0位置賦上-1(其實這一步是爲了next數組記錄下一個字串位置,並且確保匹配不成功,短指針能夠下移,這一步得看到最後再回來理解)
通過next數組我們就能夠記錄模型串的最大長度值
比如模式串的D 與文本串的 C 失配了,找出失配處模式串的 next數組 裏面對應的值,這裏爲 0,然後將索引爲 0 的位置移動到失配處。
貼個代碼,繼續講
int* getNext(string value) {
int *next = new int[value.length()];
next[0] = -1;
int j = 0;
int k = -1;
while (j < value.length() - 1) {
if (k == -1 || value[j] == value[k]) {
next[++j] = ++k;
} else {
k = next[k];
}
}
return next;
}
k其實扮演了很重要的作用,通過k,我們回到重複串上一次結束重複的位置
根據別的博主博客,總結出這樣的規律:
當value[k] == value[j]時,
有next[j+1] == next[j] + 1
其實這個是可以證明的:
因爲在P[j]之前已經有value[0 ~ k-1] == value[j-k ~ j-1]。(next[j] == k)
這時候現有value[k] == value[j],我們是不是可以得到value[0 ~ k-1] + value[k] == value[j-k ~ j-1] + value[j]。
即:value[0 ~ k] == value[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1。
其實說了這麼多,next數組的主要構建思路就是,
- 用k去臨時記錄重複串的個數,如果value[j]與value[k]相等,此時讓next[j]位置保存之前出現該值的位置,利於回溯
- 如果不等,將k值回到上一次不等的位置,繼續1操作,最後把next裏對應模型串的所有位置保存應該回溯的位置。
當理解了next數組其實剩下的就很容易了
int KMP(string str, string value) {
int i = 0;
int j = 0;
int* next = getNext(value);
while (i < str.length() && j < value.length()) {
if (j == -1 || str[i] == value[j]) {
// 當j爲-1時,要移動的是i,當然j也要歸0
i++;
j++;
} else {
// j回到指定位置
j = next[j];
}
}
if (j == str.length()) {
return i - j;
} else {
return -1;
}
}
所以整個KMP的算法其實就是在next數組,確保模串中相同子串回溯的位置相等
該算法有小小瑕疵,這裏有更優的算法
如果兩個字串已經相等,其實回溯是沒有意義的
int* getNext(string value) {
int *next = new int[value.length()];
next[0] = -1;
int j = 0;
int k = -1;
while (j < value.length() - 1) {
if (k == -1 || value[j] == value[k]) {
if (value[++j] == value[++k]) {
// 當兩個字符相等時要跳過
next[j] = next[k];
} else {
next[j] = k;
}
} else {
k = next[k];
}
}
return next;
}
下面貼上java的代碼,這是leecode上strStr()的用KMP解法
public int strStr(String haystack, String needle) {
if(needle.length() == 0) {
return 0;
}
if(needle.length() > haystack.length()){
return -1;
}
int i = 0;
int j = 0;
int[] next = getNext(needle);
char[] t = haystack.toCharArray();
char[] p = needle.toCharArray();
while(i < t.length && j <p.length) {
//1、j回到起點的時候或者值相等都應該下移
if(j == -1 || t[i] == p[j]) {
j++;
i++;
}else {
j = next[j];
}
}
if(j == p.length) {
return i-j;
}
return -1;
}
public int[] getNext(String needle) {
char[] p = needle.toCharArray();
int[] next = new int[p.length];
int j = 0;
int k = -1;
next[0] = -1;
while(j < p.length - 1) {
//兩者相等或者k處於next的初始位置
if(k == -1 || p[j] == p[k]) {
j++;
k++;
if(p[j] == p[k]) {
next[j] = next[k];
}else {
next[j] = k;
}
}else {
k = next[k];
}
}
return next;
}
如果有不正確的地方,希望大家能夠指出來,第一次看KMP算法,以前都是直接那indexOf套