KMP字符串模式匹配

字符串相關的處理在程序設計中佔有很大的一部分篇幅,對於模式匹配的相關應用場景也多如牛毛,那麼什麼樣的匹配算法纔可以提高程序的執行效率呢

所謂的字符串模式匹配時例如這樣: 有兩個字符串T和S,字符串T稱之爲正文, 字符串S稱之爲模式, 要求找出模式S在正文T中首次出現的位置,這個查找的過程就是匹配的過程, 一旦查找成功了我們就說發生了一次模式匹配並且匹配成功,我們可以利用這個方式找到所有的匹配。


在講解KMP之前,我們先講解常規的樸素模式匹配算法,我稱之爲暴力匹配算法。

假設我們有兩個字符串 A = “abcdefgabcdefx”, B= “abcdefx”;現在我們需要匹配B是否在A中出現,或者說B在A中第一次出現的位置,利用常規的算法我們會這樣:

     

int Match(string A, string B)
{
	int i = 0, j = 0;
	while(i < A.length() && j < B.length())
	{
		if (A[i+j] == B[j])
		{
			j++;
		}
		else
		{
			i++;
			j = 0;
		}
	}

	if (j >= B.length()) 
		return i;
	else 
		return -1;
}

樸素的模式匹配在每次匹配失敗後,都需要回溯i的位置到i+1的位置,j回朔到其實位置重新進行匹配,這樣將帶來的一個問題則是,當模式串各個位置都不相同時,如上的B串,B= “abcdefx”, 當j=6 即x是發生不匹配,此時j=6之前的模式匹配都已經成功,此時再回朔i=i+1, j = 0; 那麼因爲B中每個元素都不相同,所以i=i+1到i+j 的匹配都全部不成功,所以做了很多無用功。爲了解決這一一個問題便有了KMP算法。


KMP算法在不匹配發生時,不會選擇回朔i,從始至終I都不需要回朔, 而是根據子串求出的一個next數組對j進行右移,使得字符串匹配得以加速,KMP的匹配算法如下:


int KMP(string sstr, string pstr, int next[])
{
	int slen = sstr.length();
	int plen = pstr.length();
	int i = 0, j = 0;
	
	while(i < slen && j < plen)
	{
		if (j == -1 || sstr[i] == pstr[j])
		{
			i++;
			j++;
		}
		else
		{
			j = next[j];
		}
	}

	if (j >= plen)
		return i - plen;
	else 
		return -1;
}


從代碼可以看出, 和樸素的模式匹配唯一不同的地方在於j=next[j], 而i的值始終都沒有進行回溯操作, 那麼匹配失敗的時候我們可以看出KMP算法中,如果當前字符匹配成功,即sstr[i]==pstr[j],令i++j++,繼續匹配下一個字符;如果匹配失敗,即sstr[i] != pstr[j],需要保持i不變,並且讓j = next[j],這裏next[j] <=j -1,即模式串pstr相對於原始串sstr向右移動了至少1(移動的實際位數j - next[j]  >=1);  那麼KMP中最重要的部分都來自於next數組, 那麼next數組是如何求得的呢?


void get_next(int next[], string str)
{
	int i = 0, k = -1;
	int str_len = str.length()-1;

	next[0] = -1;

	while(i < str_len)
	{
		if (k == -1 || str[k] == str[i])
		{
			k++;
			i++;
			next[i] = k;
			continue;
		}
		k =  next[k];
	}
}

       那麼next數組代表的是什麼呢,其實就是模式串每一個位置的前後綴的最大公共長度。這個最大公共長度在算法導論裏面被記爲next數組。在這裏要注意一點,next數組表示的是長度,下標從1開始;但是在遍歷原字符串時,下標還是從0開始。假設我們現在已經求得next[1]、next[2]、……next[i],分別表示長度爲1到i的字符串的前綴和後綴最大公共長度,現在要求next[i+1]。如果位置i和位置next[i]處的兩個字符相同(下標從零開始),則next[i+1]等於next[i]加1。如果兩個位置的字符不相同,我們可以將長度爲next[i]的字符串繼續分割,獲得其最大公共長度next[next[i]],然後再和位置i的字符比較。這是因爲長度爲next[i]前綴和後綴都可以分割成上部的構造,如果位置next[next[i]]和位置i的字符相同,則next[i+1]就等於next[next[i]]加1。如果不相等,就可以繼續分割長度爲next[next[i]]的字符串,直到字符串長度爲0爲止。

求得了next數組,我們就可以用正常的使用kmp算法了, 它將時間複雜度從kmp算法的複雜度是O(n*m) 提高到了O(n+m)


對KMP算法的改進:

KMP算法雖然高效,但是人們後來發現KMP算法還是有缺陷存在, 比如當aaaabcde和aaaaax匹配時,next數組爲{-1, 0, 1, 2, 3, 4}  當在第5個a處匹配失敗時, 此時j = 4 變成 next[j] = 3, 也就是說模式串向右移動了4-3 = 1 位, 此時j= 3處還是a和主串i=4處的b仍舊不相等,匹配仍然失敗,導致最終無效的匹配同樣做了多次,降低效率。 我們觀察發現第1位的a和第二三四五位的a都是一樣的,那麼我們很自然的可以用第一個a的next值去取代第二三四位的next值,溯本回原這樣可以減少無效匹配的次數。改進後的next如下:

void get_nextval(int nextval[], string str)
{
	int i = 0;
	int k = -1;
	nextval[0] = -1;
	int str_len = str.length()-1;

	while(i < str_len)
	{
		if (k == -1 || str[k] == str[i])
		{
			i++;
			k++;
			if (str[k] != str[i])
				nextval[i] = k;
			else
				nextval[i] = nextval[k];
		}
		else
		{
			k = nextval[k];
		}
	}
}
改進過的KMP算法在計算出next值的同事, 如果i位的字符與他對應的next指向的字符相同, 則將i的next值指向他們共同的父值next[next[i]], 如果不相同,則i的next即爲next[i]自己的值。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章