算法學習-KMP算法

KMP算法是解決字符串查找問題的,給定文本串text和模式串pattern,從文本串text中找出模式串pattern第一次出現的位置。

最基本的字符串匹配算法是暴力求解,時間複雜度爲O(m*n)

KMP算法是一種線性時間複雜度的字符串匹配算法,他是對暴力算法的改進。

記:文本串長度爲N,模式串長度爲M,那麼暴力算法的時間複雜度爲O(M*N),空間複雜度爲O(1),KMP算法的時間複雜度爲O(M+N),空間複雜度爲O(M)

先給出暴力求解算法,比較簡單,如下

// 查找s中首次出現p的位置
int BruteForceSearch(const char* s, const char* p)
{
	int i = 0;// 當前匹配到的原始串收尾
	int j = 0;// 模式串的匹配位置
	int size = (int)strlen(p);
	int nLast = (int)strlen(s) - size;
	while((i <= nLast) && (j < size))
	{
		if (s[i + j] == p[j])// 若匹配,則模式串匹配位置後移
		{
			j++;
		}
		else // 不匹配,則對比下一個位置,模式串回溯到位首
		{
			i++;
			j = 0;
		}
	}
	if (j >= size)
	{
		return i;
	}
	return -1;
}


KMP分析

先看一個圖,改圖表示了當匹配到黃色和綠色失配的情況下KMP的處理過程我們可以不用將j不用移到串首,而是移動到串的某個位置


我們爲什麼可以這樣做呢,可以看下圖,下圖是上面移動的過程的放大圖


如果我們要這樣做,必定有個前提,A和B肯定是相同的。那麼我們只需要找到d以前的相等的最長前綴串和最長後綴串,下面看一下怎麼得到這個前綴串

有如下例子


如:j=5時,考察字符串“abaab”的最大相等k前綴和k後綴如下圖


顯然最大且相等的是ab,所以如果匹配到c的位置發現不匹配,這個時候i就不需要動了,j就可以回溯到next[j]也就是2,然後模式串從2,文本串從i繼續匹配,最好的情況恰好是next[j]=0的時候,因爲他們沒有相同的前綴和後綴,所以我們可以知道在i和j中間怎麼移動都不會有匹配了,所以直接將向後移動j位,j回溯到0就可以了,這樣滑動是最快的,相反next[j]越大滑動越慢,注意理解i的移動和暴力算法下的區別,解釋完畢。



那麼現在比較重要的應該是怎麼去獲得這個next數組了,P爲模式串,如下圖

對於模式串的位置j,有next[j]=k,即:P0P1...Pk-1=Pj-kPj-k+1...Pj-2Pj-1則,對於模式串的位置j+1,考察Pj:

若P[k]==P[j]

則next[j+1]=next[j]+1

若P[k]!=P[j],那麼我們知道A和B是相等的,我們需要在B中找所以

記h=next[k](next[k]肯定是已經求出來了),如果P[h]==P[j],則next[j+1]=h+1,爲什麼呢,因爲A=B,1=3,所以1=2,所以可以的到上面結論,否則重複此過程。




分析完畢,給出求next的代碼

void GetNext(char* p, int next[])
{
	int nLen = (int)strlen(p);
	next[0] = -1;
	int k = -1;
	int j = 0;
	while (j < nLen - 1)
	{
		// 此刻,k即next[j-1],且p[k]表示前綴,p[j]表示後綴
		// 住:k==-1表示未找到k前綴與k後綴相等,首次分析可先忽略
		if (k == -1 || p[j] == p[k])
		{
			++j;
			++k;
			next[j] = k;
		}
		else      // p[j]與p[k]失配,則繼續遞歸計算前綴p[next[k]]
		{
			k = next[k];
		}
	}
}

接下來給出KMP的代碼

char* g_s = "dsaqeabaabcabaffd";
char* g_pattern = "abaabcaba";

int KMP()
{
	int ans = -1;
	int i = 0;
	int j = 0;
	int pattern_len = strlen(g_pattern);
	int g_next[100] = {0};
	GetNext(g_pattern, g_next);
	while (i < strlen(g_s))
	{
		if (j == -1 || g_s[i] == g_pattern[j])
		{
			++i;
			++j;
		}
		else
		{
			j = g_next[j];
		}
		if (j == pattern_len)
		{
			ans = i - pattern_len;
			break;
		}
	}
	return ans;
}


分析BF與KMP的區別

  1. 假設當前文本串text匹配到i位置,模式串pattern匹配到j位置。
  2. BF算法中,如果當前字符串匹配成功,即text[i+j]==pattern[j],令j++,繼續匹配下一個字符。若適配,即text[i+j]!=pattern[j],令i++,j=0,即匹配失敗是,模式串pattern相當於文本串向右移動了一位。
  3. KMP算法中,若當前字符串匹配成,即text[i+j]==pattern[j],令j++,繼續匹配下一個字符。若失配,即text[i+j]!=pattern[j],令j=next[j](next[j]<=j-1),即模式串pattern相對於文本串text向右移動至少一位(實際移動位數爲:j-next[j]>=1)


到這裏感覺可以鬆口氣了,但是告訴你,還沒完呢,不要氣餒,繼續前進吧

進一步分析next


  • 文本串匹配到i,模式串匹配到j,此刻,若text[i] != pattern[j],即失配的情況:
  • 若next[j]=k,說明模式串應該從j滑動到k位置
  • 若此時滿足pattern[j]==pattern[k],因爲next[i]!=pattern[j],所以,text[i]!=pattern[k]
    • 即i和k沒有匹配,應該繼續滑動next[k]。
    • 換句話說:在原始的next數組中,若next[j]=k並且pattern[j]==pattern[k],next[j]可以直接等於next[k]。
按照上面說法,我們可以直接改變next得到新的next,如下圖


以最後一個a爲例,它的原始next爲2,但是P[2]是跟他相等的,所以這時候再去匹配P[2]肯定也不行,那麼直接就把原來的next更新爲2下面的next,應該很好理解的,因爲值越小效率月快,所以變種後的比較優,那麼下面給出變種後的next代碼

void GetNext(char* p, int next[])
{
	int nLen = (int)strlen(p);
	next[0] = -1;
	int k = -1;
	int j = 0;
	while (j < nLen - 1)
	{
		// 此刻,k即next[j-1],且p[k]表示前綴,p[j]表示後綴
		// 住:k==-1表示未找到k前綴與k後綴相等,首次分析可先忽略
		if (k == -1 || p[j] == p[k])
		{
			++j;
			++k;
			if (p[j] == p[k])
			{
				next[j] = next[k];
			}
			else
			{
				next[j] = k;
			}
		}
		else      // p[j]與p[k]失配,則繼續遞歸計算前綴p[next[k]]
		{
			k = next[k];
		}
	}
}

理解KMP的時間複雜度

我們考察模式串的“串頭”和主串的對應位置(也就是暴力算法中的i);

不匹配:穿透後移,保證儘快結束算法

匹配:穿透保持不動(僅僅是i++、j++,但穿透和主串對應位置沒變,但一旦發現不匹配,會跳過一賠過的字符(next[j]))。

最壞的情況,當穿透魚尾N-M的位置,算法結束

因此,匹配的時間複雜度爲O(N),算上next的O(M),整體時間複雜度爲O(M+N)。

下面給出一個KMP算法的應用PowerString週期串

給定一個長度爲n的字符串S,如果存在一個字符轉T,重複若干次T能夠得到S,那麼,S叫作週期串,T叫做S的一個週期

如:字符串abababab是週期串,abab、ab都是它的週期,其中,ab是它的最小週期。

設計一個算法,計算S的最小週期。如果S不存在週期,返回空串。



對於下面三個條帶圖我們從第一個開始分析,他是最長前綴和最長後綴,從圖中的得到的信息是中間部分相等,爲什麼他們相等呢,其實這個問題我思考了好長時間,最後恍然大悟,他們是重合的,所以就相等了,呵呵,那我們依次向後同時取t長度,則得出上下對應相等,又因爲前提是len-k可以被t整除,所以得出一共有t個串,所以t是他的週期了。

最後一個題目不是特別理解,就理解到這裏吧,估計應付面試應該沒問題了。

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