KMP演算法

鹹魚的習慣發表於2021-01-17

字串匹配,給定一個文字串S和一個模式串P,如何找到P在S中的位置?

BF演算法(暴力匹配演算法)

思路:假設文字串S匹配到i位置,P匹配到j位置。如果文字串S的i位置的元素與模式串P的j位置的元素相匹配,則繼續讓文字串的下一個元素和模式串的下一個元素比較是否匹配;如果S的i位置的元素與模式串P的j位置的元素不匹配,i回溯到上一次開始匹配的位置的下一個位置,j回溯為0,如此迴圈,直到找到匹配的字串為止。如果發生匹配,j等於模式串P的長度;如果沒有子字串與之匹配,j將一直小於模式串P的長度。

#include<bits/stdc++.h>
using namespace std;
int main() {
	char s[105], p[105];
	scanf("%s %s", s, p);
	int i = 0, j = 0;
	int len1 = strlen(s);
	int len2 = strlen(p);
	while(i < len1 && j < len2) {
		if(s[i] == p[j]) {	//當文字串和模式串字元相匹配時,讓文字串下一個元素和模式串下一個元素繼續比較
			i++;
			j++;
		}
		else {   //如果發生了不匹配,則模式串返回第一個字元,文字串回到與模式串首元素對應的位置的後一個位置,即c-d+1位置 
			i = i - (j - 1);
			j = 0;
		}
	} 
	if(j == len2)	printf("找到匹配字串,位於文字串第%d個字元位置\n", i - j + 1);
	else	printf("未找到匹配字串\n"); 
}

假設文字串長度為n,模式串長度為m,時間複雜度:O(n * m)

KMP演算法

KMP演算法實現

思想:此演算法與BF演算法最大的區別在於i是否回溯,KMP演算法使得i不回溯,而j回溯到j之前子串的最長公共前字尾位置
以中文字元為例(假設中文字元只佔一個位元組):

(1)如果文字串的第0, 1, 2個字元與模式串的第0, 1, 2個字元匹配上,第3個字元匹配失敗,按照BF演算法的思路應該讓文字串的”江“字與模式串的”望“字進行比較,而在第一次迴圈的比較中我們已經得知”江樓“相匹配,如果再做重複的比較就沒有意義了,所以我們使得i位置不變,令P串的j回溯到第0個字元”望“。此時i=3, j=0。
(2)進行下一步比較,此時文字串i位置的字元與模式串j=0的字元不匹配,此時需要使得文字串i的下一個字元和模式串的這個字元相比較,匹配失敗。此時i=4,j=0。
(3)進行下一步比較,文字串的”望江“和模式串的”望江“相匹配,”樓“字不匹配,使得i不變,j回溯到模式串的第0個字元。此時i=6,j=0。
(4)i=6,j=0處的字元不匹配,使得文字串i的下一個字元和模式串的這個字元相比較,匹配失敗。此時i=7,j=0。
(5)i=7,j=0處的字元不匹配,使得文字串i的下一個字元和模式串的這個字元相比較,匹配失敗。此時i=8,j=0。
(6)文字串的i=8位置的字元到i=13位置的字元與模式串的j=0位置的字元到第j=5位置的字元相匹配,即圖中橙色部分的比較,此時i=14,j=6。
(7)i=14,j=6處的字元不匹配,i不變,j回溯,按照(1)的思路,應使得j回溯到0位置。但是由於模式串的第二個望江與第一個望江是一樣的字元,而第二個望江與文字串的望江又相匹配,因此再次比較第一個望江和文字串中的望江是毫無意義的,因此使j回溯到模式串第一個望江後面的位置,即j=3。此時i=14,j=6。
..............(後面步驟情況與前7次相類似,故而省略)
 
 
將j字元之前的字串的最長公共前字尾用陣列next[]儲存,以便j值的回溯,即next[]陣列中所存元素也是j下一次文字串字元與模式串字元失配時,j值應該回溯到哪個位置。如果沒有前字尾則賦值為0 / -1。next[]陣列值的程式碼實現下文介紹。

void kmp() {
	char s[105], p[105];
	scanf("%s %s", s, p);
	int i = 0, j = 0;
	int len1 = strlen(s);
	int len2 = strlen(p);
	
	while(i < len1 && j < len2) {
//		if(j == 0 && s[i] != p[j]) {	//如果模式串的第0個字元與對應的文字串字元不匹配,則下次使得文字串的下一個字元與模式串第0個字元比較 
//			i++;
//		}
		if(j == -1 || s[i] == p[j]) {
			i++;
			j++;
		} 
		else {
			j = next[j];	//如失配j回溯。如果0 = next[0]會進入死迴圈, 因此加入一個判斷條件使得文字串陣列下標加一
		}
	}
	
	if(j == len2)	printf("找到匹配字串,位於文字串第%d個字元位置\n", i - j + 1);
	else	printf("未找到匹配字串\n"); 
}

next陣列

    從上文得,next[]陣列儲存的是模式串j字元之前的子串的最長公共前字尾值,可知next[]陣列與文字串沒有關係,只需根據模式串就可以求得各個位置的next[]值。以abcabe模式串為例,可得:

注意:如果將next[0]賦值為0,則在kmp程式碼實現中加上第一個if條件;如果賦值為-1,則在第二個if中加上if(j==-1),這裡可以假想成模式串的第一個元素為萬能萬用字元,讓本次迴圈進入第二個if中,也能像第一個if一樣實現本串的下一個字元與模式串第0個字元比較的結果。

思想:假設c表示字首字元對應位置,d表示字尾字元對應位置。可將尋求next[]值的過程與文字串和模式串的匹配過程類比,尋求next[]值的過程相當於自己和自己匹配,匹配成功則+1,失配則回溯,尋找更短的公共前字尾,即下面三個過程:
(1)使得next[0] = -1;
(2)如果P[c]與P[d]匹配,使得next[d]的值相較於next[d-1]加一,即next[d] = next[d-1] + 1;
(3)如果P[c]與P[d]不匹配,使得d不斷回溯尋找更短的最長公共前字尾
轉化為程式碼:

void get_next(int len2) {
	int next[len2];
	next[0] = -1;
	int c = 0, d = -1;
	while(c < n - 1) {
		if(d == -1 || p[c] == p[d]) {
			d++;
			c++;
			next[c] = d;
		}
		else
			d = next[d];//如果不p[c]與p'[d]不匹配,則使d不斷回溯,直到p[ next[... next[k] ] ] = p[c]為止,以找到長度更短的公共前字尾,如果找到最後一個字元還未找到,則令next[c+1] = 起始位置 
	}
}

KMP演算法的優化:

    以S="aaaabcde", p="aaaaax"為例進行說明,得其next陣列為-1, 0, 1, 2, 3, 4;如下圖,當第一步匹配到d時,b與a不等,此時按照上文思路,d應該回溯到next[d]位置,即倒數第二個a;倒數第二個a與文字串中的b依舊不匹配,繼續回溯到next[next[d]]的位置,即倒數第三個a,如此迴圈,便顯得多餘。既然d位置的字元與對應位置文字串的字元不匹配而d位置的字元又與下次回溯的位置對應的字元相等,便沒有比較的必要了,令next[c] = next[next[c]]。

程式碼實現:

void get_nextval(int len2) {
	int next[len2];
	next[0] = -1;
	int c = 0, d = -1;
	while(c < n - 1) {
		if(d == -1 || p[c] == p[d]) {
			d++;
			c++;
			if(p[d] != p[c]) 
				next[c] = d;
			else 
				next[c]	= next[d];	
		}
		else
			d = next[d]; 
	}
}

 

時間複雜度:假設文字串長度為n,模式串長度為m,kmp時間複雜度為O(n), 求next陣列的時間複雜度為O(m),總的時間複雜度為O(n + m)

相關文章