算法 | 串匹配算法之KMP算法及其優化

主串 s:A B D A B C A B C
子串 t:  A B C A B
問題:在主串 s 中是否存在一段 t 的子串呢?

形如上述問題,就是串匹配類問題。【串匹配——百度百科】

串匹配問題是一項有着非常多應用的重要技術,KMP匹配算法就是其中一種高效的字符串匹配算法。
在KMP算法之前先介紹一下BF算法,BF算法又名暴力匹配算法,該算法在匹配的時候把子串依次從主串的起始位置開始匹配,若匹配失敗再從主串的下一個位置開始,子串重新從頭開始匹配……
BF算法

int BF(char *str, char *sub) 
{//暴力匹配算法
	int i = 0;	//遍歷主串
	int j = 0;	//遍歷子串
	int k = i;	//記錄每次從主串匹配的起始位置
	while (i < strlen(str) && j < strlen(sub)) 
	{
		if (str[i] == sub[j])	//當前下標位置匹配
		{
			++i;	
			++j;
		}
		else						//當前下標位置不匹配
		{
			if(strlen(str) - i <= strlen(sub) ) return -1;//優化,如果主串剩餘的長度沒有子串長,則肯定不匹配
			j = 0;		//子串從頭開始匹配
			i = ++k;	//匹配失敗,i從主串的下一位置開始
		}
	}
	if (j >= strlen(sub))	//子串遍歷完,說明找到了對應的位置
	{
		return k;
	}
	else						//子串沒有遍歷完,說明無該子串
		return -1;
}

可以看到,BF算法是一種非常笨的算法,執行效率不高,那麼有沒有更優化的算法呢?

當然有啦,就是本文所講的 KMP 算法。

KMP算法是一種改進的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人們稱它爲克努特—莫里斯—普拉特操作(簡稱KMP算法)。KMP算法的核心是利用匹配失敗後的信息,儘量減少模式串與主串的匹配次數以達到快速匹配的目的。具體實現就是通過一個next()函數實現,函數本身包含了模式串的局部匹配信息。KMP算法的時間複雜度O(m+n)。
KMP算法的核心在於求Next子串,Next子串也叫模式串的前綴表,也就是模式串的最長公共前後綴
在這裏插入圖片描述
GetNext

next 前綴表中存放着當前字符之前的串最大公共前後綴。這樣做的好處就在於,當模式串與主串進行匹配時,如果出現失配情況可以根據保存在 next 中的信息迅速定位,無需從頭開始匹配。

比如:下列匹配情況
X X X X X A B A X X X
     A B A B C
AC 不匹配,如果按照BF算法就要重頭開始匹配了。但是我們發現,在模式串 A B A B C 中,有 A BA B 相同的部分,那麼是不是可以這樣呢?
X X X X X A B A X X X
     A B A B C
         A B A B C
我們發現此時主串中的 A 與 模式串中的 A 匹配上了,先不管後續匹配情況如何,單就這個過程我們已經可以看出 KMP 算法的一些細節了。那麼怎麼讓計算機實現我們上述的算法呢,這就不得不利用我們的next子串了。在本例中使用的以 -1,0 開頭的next串格式,此格式便於在失配時(求next時失配或串匹配時失配) 快速回退。即 i = next(i) 這種形式。如下爲求 next 的源碼:

void GetNext(const char *sub, int *next)
{
	int len = strlen(sub);
	next[0] = -1;		//此前綴表從-1開始,一些書籍上是從0開始
	next[1] = 0;
	int i = 0;			//前綴
	int j = 1;			//後綴
	while(j < len-1)	//len-1 模式串中最後一個字符無需求前綴表
	{ 
		if (i == -1 || sub[i] == sub[j])	//匹配結束 || 匹配成功 ,寫入next
		{
			next[++j] = ++i;
			//++i;
			//++j;
		}
		else			//匹配失敗,i回退
		{
			i = next[i];
		}
	}
}

KMP匹配過程演示
在這裏插入圖片描述

//從主串pos位置開始查找,默認從頭開始
int KMP(const char* str, const char* sub, int pos = 0 )
{
	int len_str = strlen(str);
	int len_sub = strlen(sub);
	
	if (pos < 0 || pos >= lenstr)	return -1;//位置非法,查找失敗
	char* next = (char*)malloc(sizeof(char)*len_sub);	//構建前綴表
	GetNext(sub, next);

	int i = pos;	//主串	//從pos位置開始查找
	int k = 0;	//模式串/next
	while(i < len_str && k < len_sub)
	{
		if(k == -1 || str[i] == sub[k])	//k==-1匹配結束(失敗)|| 匹配成功,向後移動
		{
			++k;++i;
		}
		else		//匹配失敗,查詢next表,移動模式串重新比較
		{
			k = next[k];
		}

	}

	free(next);

	if(k == len_sub)	//模式串遍歷完,找到位置
	{
		return i-k;
	}
	else				//主串已結束,未找到子串
	{
		return -1;
	}

}

KMP算法的確是非常的智能,但是還有一種情況我們沒有考慮到,比如一個 aaaaaaab 的模式串,在該模式串與主串匹配的時候,KMP算法也不是那麼好用了。真是聰明難得糊塗一回,不過我們也有改進的方法,只需要去除next子串中多餘重複的前綴並對其進行相應的優化即可,如下圖所示:

優化:GetNextVal
在這裏插入圖片描述

void GetNextVal(const char* str, int* next)
{
	int len = strlen(str);
	next[0] = -1;
	next[1] = 0;
	int j = 1;
	int k = 0;

	while (j + 1 < len)
	{
		if (k == -1 || str[j] == str[k])
		{
			next[++j] = ++k;
		}
		else
		{
			k = next[k];
		}
	}

	int* nextval = (int*)malloc(len * sizeof(int));
	nextval[0] = -1;
	for (int i = 1; i < len; i++)
	{
		if (str[i] == str[next[i]])
		{
			nextval[i] = nextval[next[i]];
		}
		else
		{
			nextval[i] = next[i];
		}
	}

	for (int i = 0; i < len; i++)
	{
		next[i] = nextval[i];
	}
	free(nextval);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章