來我們再聊聊 KMP 算法 -- 我懂,你也得懂

搞了一整天的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;*/
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章