字串匹配問題——KMP演算法

夕陽下江堤上的男孩發表於2018-03-23

字串匹配問題:給定兩個字串S(主串)和T(模式串),假設n=strlen(S) > strlen(T)=m,判斷主串S中是否包含模式T,且返回T在S中所在的起始位置。這裡為簡單起見,若S包含T,則只返回第一個T所在的位置。

一般的蠻力法如下:


蠻力法在遇到不匹配時,j每次都要回到T的起點,從新開始匹配,這樣來看效率就比較低,蠻力法的時間複雜度是O(n*m)。

1、理論

思想:儘量利用已經部分匹配的結果資訊,儘量讓 i 不回溯,加快模式串T的滑動速度。
先舉個啟發性的例子,然後引出理論。例子:





結論:從例子中可以看出,每次匹配失敗,然後回溯再次從新匹配時(第2、4、5趟),判斷當前的S[i]和T[j]是否相等可通過上一趟S和T的匹配結果和T本身的結構資訊來得出結論。也就是說,在上一趟S和T匹配結果的基礎上,如果知道T本身的結構資訊,那麼第2、4、5趟的步驟其實是可以省略的,且i可以不用回溯(i不回溯待會兒再來直觀理解)。

1.1、模式串T中的資訊

抓住部分匹配時的兩個特徵:

如上圖所示,假設S[i]和T[j]不相等後,從T的第k個位置開始和S中的第i個位置比較,那麼:
(1)從圖的後半部分可得:
                                                                                (1)
(2)從圖的後前部分可得: 
                                                                                                (2)
所以,兩式聯立可得:
                                              (3)
所以,由上面的推理可知,只要提前知道模式串T本身的結構資訊,就可以由j計算出k,即下一回T的匹配起點。另外,(3)式可能存在多種不同長度的首尾相等的可能,那麼在這樣的情況下,肯定是k越大越好,這樣一方面可以減少再次匹配的字元數量;另一方面,T向右移動的位置相當S而言也是最慢的,這樣也不會錯過各種匹配的情況。比如:


令k = next[ j ],則:

計算next[j]的方法:
由模式T的字首函式定義易知,next[1]=0,假設已經計算出next[1],next[2],…,next[j],如何計算next[j+1]呢?設k=next[j],則已有:
                                                         (4)
此時,比較tk和tj,可能出現兩種情況:
(1)tk=tj:說明t1 … tk-1 tk=tj-k+1 … tj-1 tj,由字首函式定義,next[j+1]=k+1;
(2)tk≠tj:此時要找出t1 … tj-1的字尾中第2大真字首,顯然,這個第2大的真字首就是next[next[j]]=next[k]。
再比較tnext[k]和tj,此時仍會出現兩種情況,當tnext[k]=tj時,與情況(1)類似,next[j]=next[k]+1;當tnext[k]≠tj時,與情況(2)類似,再找t1 … tj-1的字尾中第3大真字首。重複執行上述步驟,直到找到t1 … tj-1的字尾中的最大真字首,或確定t1 … tj-1的字尾中不存在真字首,此時,next[j+1]=1。
求next陣列程式碼:
void getNext(char T[], int next[])
{
	next[1] = 0;        //從第一個位置開始
	int j = 1, k = 0;   //這裡比較巧妙,先不用管什麼意思,在相關的地方驗證一下,然後就會明白了
	while (j < T[0])
	{
		if ((k == 0) || (T[j] == T[k]))
		{
			j++;
			k++;
			next[j] = k;
		}
		else
			k = next[k];  //往前找
	}
}

2、KMP完整演算法

KMP中的核心就是求next陣列,之後就沒什麼關鍵的了。KMP的時間複雜度為O(n+m),當n>>m時,時間複雜度是O(n)。完整程式碼如下:
int calLen(char *p)     //計算字元陣列長度
{
	int count = 0;
	while (*(++p) != '\0')  //從第一個位置開始
	{
		count++;
	}
	return count;
}

void getNext(char T[], int next[])
{
	next[1] = 0;        //從第一個位置開始
	int j = 1, k = 0;   //這裡比較巧妙,先不用管什麼意思,在相關的地方驗證一下,然後就會明白了
	while (j < T[0])
	{
		if ((k == 0) || (T[j] == T[k]))
		{
			j++;
			k++;
			next[j] = k;
		}
		else
			k = next[k];  //往前找
	}
}

#include<iostream>
using namespace std;
#define STR_LEN 52
int main(int argc, char* argv[])
{
	int i = 1, j = 1;
	char S[STR_LEN], T[STR_LEN];
	int next[STR_LEN];
	cout << "輸入源字串:" << endl;
	cin >> S + 1;   //第0個位置用於儲存長度
	cout << "輸入目標字串:" << endl;
	cin >> T + 1;
	T[0] = calLen(T);
	S[0] = calLen(S);
	if (T[0] > S[0] || T[0] == 0 || S[0] == 0)
	{
		cout << "字串格式輸入錯誤!" << endl;
		system("pause");
		return 0;
	}

	getNext(T, next);

	while (S[i] && T[j])  //判斷字串是否結束
	{
		if (S[i] == T[j])
		{
			i++;
			j++;
		}
		else   //將T向右滑動
		{
			j = next[j];  //將計算出來的k賦值給j
			if (j == 0)   //如果j==0,表明剛剛在j==1處(即一開始)兩個字串就不相等,那麼S中的i要往右移動一位
			{             
				i++;
				j++;
			}
		}
	}
	if (T[j] == '\0')
		cout << "找到目標子串,從S中的第" << i - T[0] << "個位置開始!" << endl;
	else
		cout << "沒有匹配的目標子串!" << endl;

	system("pause");
	return 0;
}

參考文獻:

1、演算法設計與分析-王紅梅


相關文章