搞了一整天的KMP,自己動手寫,先是感覺自己搞懂了,寫完提交又崩潰了。反反覆覆一整天,剛剛總算是半抄半寫過去了。
那現在,我就來看看自己能不能把這個算法講清楚,當然,觀衆得有一定的基礎,我語文不好,有的東西大家意會吧。
這篇不想用什麼華麗的圖片啊、辭藻啊堆砌,要堆砌上一篇已經堆砌過了,這篇更側重於重難點突破。
寫完語:我已經盡我所能讓這篇不那麼枯燥了,如果願意看下去,你可能會收藏起來。
爲什麼需要KMP算法
這個·不想多廢話,它的時間複雜度是線性的。
KMP算法爲什麼快
暴力算法爲什麼慢
首先我要講在前面,字符串匹配算法,不論是暴力破解,還是KMP這種高級算法,基礎都是使用快慢指針的,如果對快慢指針不瞭解建議趕緊去刷題。
下面講的,主串上的那個指針是慢指針,子串上那個是快指針。別糾結爲什麼叫快慢,一個名字而已。
暴力算法,在匹配失敗之後,會將快慢指針都回溯。
KMP算法爲什麼快
因爲它減少了對指針的回溯過程。
首先慢指針就不用回溯了!!!然後快指針也是回溯到半路。
你看像這樣匹配失敗了,慢指針就留在‘c’那裏等着,然後快指針來回溯:
快指針也只是回溯到了‘b’。
爲什麼?上面那個可能不是很直觀,還是要打個比方:
比方說對子串“ababc”,已經匹配到’c’失敗了,不用回溯到開頭,只需要回溯到第二個‘a’,因爲在匹配失敗前的兩個字符肯定是‘ab’。
這樣講可能不是很嚴肅,但是應該比較好理解吧。那我總結了對這個點的話術,畢竟別人問你的時候你不能這麼通俗的告訴人家。
我是這麼想的:如果子串沒有重疊部分,哪怕是一個字符的重疊,也沒有,那麼直接將快指針回溯到開頭,然後將快指針對準慢指針。
如果子串有重疊的部分,就將快指針回溯到重疊部分後面那個位置,然後將快慢指針對準。
接着繼續比對。
那麼,你又要怎麼知道,快指針應該回溯到哪裏,說是重疊部分後面那個位置,那你每次匹配失敗都要去找一下有哪裏重疊?
那要是重疊部分很多,你要回到哪個?
這些準備工作,應該在進行KMP匹配之前做好。那就是next數組。
next數組
其實前面那些都是很基礎的東西,不過這個能點進這篇的應該都是衝着next數組來的,而我的目的,也是要把這個數組的生成講清楚。
相信代碼大家都有,不過我依然要放在這裏:
void getnext(string p, vector<int> &next) //next在傳入時應該進行擴容
{
int len = p.size();
int k = -1;
int j = 0;
next[0]=-1;
while (j < len - 1)
{
if (k == -1 || p[k] == p[j])
{
k++;
j++;
next[j] = k;
}
else {
k = next[k];
}
}
}
首先爲了後面運算方便,將next[0]設置爲-1,不得不說這個設置爲-1非常之巧妙。
先不說巧妙在哪裏,自己去寫的話就知道了。
也先不說那個令人絞盡腦汁的 k = next[k]
,我們先把基礎弄明白。
先看next[j] = k
,這一句。
來我們來個簡單的栗子:“ababcba”.
要對這個子串求它的next數組,是這樣的。
1、a
2、ab
3、aba
4、abab
5、ababc
6、ababcb
7、ababcba
將這個字符串這樣分一下,然後對號入座,看到我標的號了沒,對應的是next數組中的號,最後那個可以去掉,因爲如果整個串都對上了還回溯什麼。
首先我們來看一下“前後子集“的概念,我自己起的名字,還不錯吧。
拿4來說把,它的前子集有:
{
a,
ab,
aba
}
後子集有:
{
b,
ab,
bab
}
規律不難找啊。
那,他倆子集裏面有一個同類,“ab”,將ab的長度填入next[4]裏面。
接下來難度要稍微升級了。
這個next數組,也有半自動推導,碧如說4,它的對稱度爲2,那麼如果在4的基礎上,加上一個字符,這個字符剛好跟對稱度+1的位置的字符對上,即如果加上的字符是a,那麼便可以知道 5 的對稱度爲3,因爲前面兩個已經有 4 做了鋪墊。
這就是:
if (k == -1 || p[k] == p[j])
{
k++;
j++;
next[j] = k;
}
這一個部分的原理。next[++j] = ++k;
,是這樣來的。
雖然我語文不好,但是講到這個份上了,還不能心領神會那就不是我的問題了。
可惜,上面那個例子加上去的是 ‘c’。那就·是另外一部分代碼的事情了:
else {
k = next[k];
}
稍事休息。
k = next[k]
要理解這行代碼,我們用另外一個字符串會比較直觀一些。
“a b a b a b c b”
一步一步來啊,
1、
next[0] = -1;
k = -1,j=0; //a
k = 0,j=1;
next[1] = 0;
//這兩個簡直是鐵索連環,就寫一起吧
2、
j = 1,k = 0; //a,b
k = -1;
k = 0,j = 2;
next[2] = 0;
3、a,b,a
k = 1,j = 3;
next[3] = 1;
//看到了啊,出現了,進入if
4、a,b,a,b
k = 2,j = 4;
next[4] = 2;
//看啊,用到上面講的了。
//其實還有一條鐵律忘記說了,如果有耐心看到這裏那我就說。後一位的對稱度,頂多比前一位,多1!!!
5、a,b,a,b,a
k = 3,j = 5;
next[5] = 3;
//你去找,隨便找,像我這麼有耐心的真不多了。
6、a,b,a,b,a,b
k = 4,j = 6;
next[6] = 4;
//越來越接近目標了啊,馬上就要斷了香火了
7、a,b,a,b,*a*,b,c
k = 5,j = 7;
next[7] = 5?
這時候你會發現,它新加上來的那個字符,和對稱度後面一位字符不匹配,‘c’!=‘a’!,那裏我打了星標。
這時候怎麼辦?重頭找?不可能的事,重頭找的話,怎麼說,那個代碼該怎麼寫?一個一個在比對?
這時候還有另外一種想法,你看:
在插入‘c’之前,前面已經是對稱的了有,好幾組‘a,b’的存在。那麼,爲什麼不推到當前失敗‘a,b’的前面一個‘a,b’
去看看,這樣既保證了對稱度不會一下子跌到谷底,又能保證了對稱性。因爲第三個字符的前面也是‘a,b’,‘c’的前面也是‘a,b’,
那爲什麼不把這個對稱輪迴一輪一輪往前提並匹配呢?
如果最後真的輪迴到了0點,那也總比直接回到原點有不知道後面會不會有驚喜要來的強一些。
那麼,要怎麼將快指針(k)回溯到前一個輪迴的後一個字符呢?
其實上面跟大家開了個玩笑,哈哈,不知道有沒有人發現。
k = 5?
這個k,是7還是6?看看清楚,不記得的話把上面代碼翻出來看看。
那麼你看看 ,這時候的next[k]存的是什麼東西,是不是上一輪的對稱度,要是不記得,我給你找:`next[4] = 2`,那這個對稱度是什麼東西?
是不是等於字符串中上一輪輪迴對應的後一個位置!注意,數組是以0爲下標開始的!
是不是繞暈了?
我把一些概念再捋一下:
代碼:
if (k == -1 || p[k] == p[j])
{
k++;
j++;
next[j] = k;
}
else {
k = next[k];
}
對稱度:最高有幾個字符的相同子集。
輪迴:比方說對稱度爲2的時候,‘a,b’爲一個輪迴,第一個‘a,b’爲第一個輪迴。
鐵律:後一位的對稱度,頂多比前一位,多1!!!
示例字符串:
“a b a b a b c b”
KMP匹配、
這個匹配就比較好理解了,該註釋的地方我註釋了
int kmp(string s, string p)
{
int i = 0;
int j = 0;
int sLen = s.size();
int pLen = p.size();
if (pLen == 0 )
return 0;
vector<int> vec(pLen, 0);
getnext(p,vec); //獲取next數組
while (i < sLen && j < pLen)
{
if (j == -1 || s[i] == p[j])
{
i++;
j++;
}
else
{
//②如果j != -1,且當前字符匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]
//next[j]即爲j所對應的next值
j = vec[j];
}
}
if (j >= pLen)
return(i - j);
return -1;
}
KMP算法整體實現(LeetCode測試通過)
#include<iostream>
#include<string>
#include<vector>
using namespace std;
void getnext(string p, vector<int> &next) //next在傳入時應該進行擴容
{
int len = p.size();
int k = -1;
int j = 0;
next[0]=-1;
while (j < len - 1)
{
if (k == -1 || p[k] == p[j])
{
k++;
j++;
next[j] = k;
}
else {
k = next[k];
}
}
}
int kmp(string s, string p)
{
int i = 0;
int j = 0;
int sLen = s.size();
int pLen = p.size();
if (pLen == 0 )
return 0;
vector<int> vec(pLen, 0);
getnext(p,vec); //獲取next數組
while (i < sLen && j < pLen)
{
if (j == -1 || s[i] == p[j])
{
i++;
j++;
}
else
{
//②如果j != -1,且當前字符匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]
//next[j]即爲j所對應的next值
j = vec[j];
}
}
if (j >= pLen)
return(i - j);
return -1;
}
int main()
{
vector<int> vec1(10,0);
//for (int i = 0; i < vec1.size(); i++)
// cout << vec1[i] << " ";
//cout << endl;
string str = "";
string str2 = "";
int a = kmp(str,str2);
cout << a << endl;
/*getnext(str2,vec1);
for(int i = 0;i<vec1.size();i++)
cout << vec1[i]<<" ";
cout << endl;*/
}