1. 程式人生 > >談談自己對KMP演算法的見解

談談自己對KMP演算法的見解

開門見山,KMP演算法是用來解決一個字串在另外一個字串中是否存在,如果存在,則返回它在該串的位置。

對於初次學習KMP演算法的困惑難點如下:

(1)next陣列是什麼鬼?為什麼會想到字首集合和字尾集合中的相同元素?

(2)如何給next陣列中元素賦值?

(3)next陣列程式碼中回溯那塊為什麼要那樣回溯?

(4)為什麼要改進KMP演算法的next陣列?

當然在學習KMP演算法之前需要知道樸素的模式匹配演算法,沒錯就是暴力求解,死磕。主串:"huanhuanhuan",模式字串:"huanhabc",當遍歷到主串'u'和模式串'a'時發現不匹配,然後主串回退到起點,模式串回退到第二個字元,繼續.........時間複雜度太高了O(m*n)。

接下來KMP閃亮登場,它可以大大避免重複遍歷的情況。能把時間複雜度優化到O(m+n)。來,我們開始解答上面提到的四點困惑。下面就是小學生開始解答日常家庭作業了:

首先我看了看題目,發現要想解釋next陣列必須得用到PMT表(Partial Match Table)。它長這樣的:

preview

表中前兩行應該不用解釋,最後一行value是字串"abababca"截止當前index的子串取其字首集合和字尾集合中最大的相同元素長度。例:index = 3,子串為"abab" ,字首集合為{"a","ab","aba"},字尾集合為{"b","ab","bab"}。按規則value取"ab"的長度為2。這裡注意字首集合不包括最後一個元素,字尾集合不包括第一個元素

解(1):

next陣列的值就是將上面的value值往右挪一位,為了程式設計方便把next[0]賦值為-1。看看新增next後的表格吧:

preview

舉栗子:index=5,它的next值就是index=4時的pmt值(value值)。所以:next陣列中的值儲存的就是截止它----前面字串的前後綴集合中---最長的相同字串長度值。

為什麼會想到字首集合和字尾集合中的相同元素?

preview

看圖,KMP演算法在匹配過程中匹配失敗時,圖中j要回退的位置就是next[6] = 4,這就是字首字尾的在用在這裡的好處,因為已經比較過了,所以有些沒必要的比較就不需要再進行了。

解(2):

next值中的元素求解過程:

求next陣列的過程完全可以看成字串匹配的過程,即以模式字串為主字串,以模式字串的字首為目標字串,一旦字串匹配成功,那麼當前位置向後一位的next值就是匹配成功的字串的長度。(計算next值下標從0開始,匹配從1開始匹配,相差一位)

preview

preview

preview preview preview

 來上程式碼:

void getNext(const char *ptr,int len,int *next)
{
	if(NULL == ptr || NULL == next)
	{
		return ;
	}
	int i = 0;
	int j = -1;
	next[0] = -1;
	while(i<len-1) //注意越界
	{
		if(j == -1 || ptr[i] == ptr[j])
		{
			++i;
			++j;
			next[i] = j;
		}
		else
		{
			j = next[j];   //回溯 
		}
	}

}

解(3):

回溯,next陣列中儲存的是就是匹配到的最長前後綴,求next陣列本身也是一種字串匹配的過程。當匹配不成功的時候回退,找最長前後綴,但是這樣也會匹配不成功,原因如下,先上圖:

解(4):

從圖中可以看出來現在的next演算法還是存在重複的回溯過程,它在每次回溯的過程中都會進行沒必要的比較,何不直接讓其進行有效的比較,改進的演算法如下: 

void getNext(const char *ptr,int len,int *next)
{
	if(NULL == ptr || NULL == next)
	{
		return ;
	}
	int i = 0;
	int j = -1;
	next[0] = -1;
	while(i<len-1) //注意越界
	{
		if(j == -1 || ptr[i] == ptr[j])
		{
			++i;
			++j;
			if(ptr[i] == ptr[j])
			{
				next[i] = next[j];
			}
			else
				next[i] = j;
		}
		else
		{
			j = next[j];   //回溯 
		}
	}
}

其實就是在給next陣列賦值的時候多加了個判斷,如果i位字元與它的next值指向的j位字元相等,則使i位的next值和j位的next值相等。如果不等就是它自己i為的next值。

至此4個困惑都解答完畢,最後附上完整的KMP演算法程式碼:

int KMP(const char*str,const char*ptr,int*next)
{
	if(NULL==str || NULL == ptr)
	{
		return -1;
	}
	int i=0,j=0;
	int len1 = strlen(str);
	int len2 = strlen(ptr);
	while(i< len1 && j< len2)
	{
		if(str[i] == ptr[j])
		{
			i++;
			j++;
		}
		else
		{
			j = next[j];
		}
	}
	if(j==len2) return i-j;
	else return -1;
}
int main()
{
    char *str = "ababababca";
    char *ptr = "abababca";
    int *next = new int[strlen(ptr)];
    getNext(ptr,strlen(ptr),next);
    int index = KMP(str,ptr,next);
    cout<<index<<endl;

}