KMP演算法詳解

2021-09-18 05:41:27 字數 3738 閱讀 3112

(原創)詳解kmp演算法

kmp演算法應該是每一本《資料結構》書都會講的,算是知名度最高的演算法之一了,但很可惜,我就沒看懂過~~~

之後也在很多地方也都經常看到講解kmp演算法的文章,看久了好像也知道是怎麼一回事,但總感覺有些地方自己還是沒有完全懂明白。這兩天花了點時間總結一下,有點小體會,我希望可以通過我自己的語言來把這個演算法的一些細節梳理清楚,也算是考驗一下自己有真正理解這個演算法。

什麼是kmp演算法:

kmp是三位大牛:d.e.knuth、j.h.morris和v.r.pratt同時發現的。其中第一位就是《計算機程式設計藝術》的作者!!

kmp演算法要解決的問題就是在字串(也叫主串)中的模式(pattern)定位問題。說簡單點就是我們平時常說的關鍵字搜尋。模式串就是關鍵字(接下來稱它為p),如果它在乙個主串(接下來稱為t)**現,就返回它的具體位置,否則返回-1(常用手段)。

首先,對於這個問題有乙個很單純的想法:從左到右乙個個匹配,如果這個過程中有某個字元不匹配,就跳回去,將模式串向右移動一位。這有什麼難的?

我們可以這樣初始化:

之後我們只需要比較i指標指向的字元和j指標指向的字元是否一致。如果一致就都向後移動,如果不一致,如下圖:

a和e不相等,那就把i指標移回第1位(假設下標從0開始),j移動到模式串的第0位,然後又重新開始這個步驟:

基於這個想法我們可以得到以下的程式:

/**

* 暴力破解法

* @param ts 主串

* @param ps 模式串

* @return 如果找到,返回在主串中第乙個字元出現的下標,否則為-1

*/public static

intbf

(string ts, string ps)

else}if

(j == p.length)

else

}

上面的程式是沒有問題的,但不夠好!(想起我高中時候數字老師的一句話:我不能說你錯,只能說你不對~~~)

如果是人為來尋找的話,肯定不會再把i移動回第1位,因為主串匹配失敗的位置前面除了第乙個a之外再也沒有a了,我們為什麼能知道主串前面只有乙個a?因為我們已經知道前面三個字元都是匹配的!(這很重要)。移動過去肯定也是不匹配的!有乙個想法,i可以不動,我們只需要移動j即可,如下圖:

上面的這種情況還是比較理想的情況,我們最多也就多比較了再次。但假如是在主串「sssssssssssssa」中查詢「ssssb」,比較到最後乙個才知道不匹配,然後i回溯,這個的效率是顯然是最低的。

大牛們是無法忍受「暴力破解」這種低效的手段的,於是他們三個研究出了kmp演算法。其思想就如同我們上邊所看到的一樣:「利用已經部分匹配這個有效資訊,保持i指標不回溯,通過修改j指標,讓模式串盡量地移動到有效的位置。」

所以,整個kmp的重點就在於當某乙個字元與主串不匹配時,我們應該知道j指標要移動到哪?

接下來我們自己來發現j的移動規律:

如圖:c和d不匹配了,我們要把j移動到哪?顯然是第1位。為什麼?因為前面有乙個a相同啊:

如下圖也是一樣的情況:

可以把j指標移動到第2位,因為前面有兩個字母是一樣的:

如果用數學公式來表示是這樣的

p[0 ~ k-1] == p[j-k ~ j-1]

這個相當重要,如果覺得不好記的話,可以通過下圖來理解:

弄明白了這個就應該可能明白為什麼可以直接將j移動到k位置了。

因為:當t[i] != p[j]時

有t[i-j ~ i-1] == p[0 ~ j-1]

由p[0 ~ k-1] == p[j-k ~ j-1]

必然:t[i-k ~ i-1] == p[0 ~ k-1]

公式很無聊,能看明白就行了,不需要記住。

這一段只是為了證明我們為什麼可以直接將j移動到k而無須再比較前面的k個字元。

好,接下來就是重點了,怎麼求這個(這些)k呢?因為在p的每乙個位置都可能發生不匹配,也就是說我們要計算每乙個位置j對應的k,所以用乙個陣列next來儲存,next[j] = k,表示當t[i] != p[j]時,j指標的下乙個位置。

很多教材或博文在這個地方都是講得比較含糊或是根本就一筆帶過,甚至就是貼一段**上來,為什麼是這樣求?怎麼可以這樣求?根本就沒有說清楚。而這裡恰恰是整個演算法最關鍵的地方。

public static

int[

]getnext

(string ps)

else

}return next;

}

這個版本的求next陣列的演算法應該是流傳最廣泛的,**是很簡潔。可是真的很讓人摸不到頭腦,它這樣計算的依據到底是什麼?

好,先把這個放一邊,我們自己來推導思路,現在要始終記住一點,next[j]的值(也就是k)表示,當p[j] != t[i]時,j指標的下一步移動位置。

先來看第乙個:當j為0時,如果這時候不匹配,怎麼辦?

像上圖這種情況,j已經在最左邊了,不可能再移動了,這時候要應該是i指標後移。所以在**中才會有next[0] = -1;這個初始化。

如果是當j為1的時候呢?

顯然,j指標一定是後移到0位置的。因為它前面也就只有這乙個位置了~~~

下面這個是最重要的,請看如下圖:

請仔細對比這兩個圖。

我們發現乙個規律:

當p[k] == p[j]時,

有next[j+1] == next[j] + 1

其實這個是可以證明的:

因為在p[j]之前已經有p[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k)

這時候現有p[k] == p[j],我們是不是可以得到p[0 ~ k-1] + p[k] == p[j-k ~ j-1] + p[j]。

即:p[0 ~ k] == p[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1。

這裡的公式不是很好懂,還是看圖會容易理解些。

那如果p[k] != p[j]呢?比如下圖所示:

像這種情況,如果你從**上看應該是這一句:k = next[k];為什麼是這樣子?你看下面應該就明白了。

現在你應該知道為什麼要k = next[k]了吧!像上邊的例子,我們已經不可能找到[ a,b,a,b ]這個最長的字尾串了,但我們還是可能找到[ a,b ]、[ b ]這樣的字首串的。所以這個過程像不像在定位[ a,b,a,c ]這個串,當c和主串不一樣了(也就是k位置不一樣了),那當然是把指標移動到next[k]啦。

有了next陣列之後就一切好辦了,我們可以動手寫kmp演算法了:

public static

intkmp

(string ts, string ps)

else}if

(j == p.length)

else

}

和暴力破解相比,就改動了4個地方。其中最主要的一點就是,i不需要回溯了。

最後,來看一下上邊的演算法存在的缺陷。來看第乙個例子:

顯然,當我們上邊的演算法得到的next陣列應該是[ -1,0,0,1 ]

不難發現,這一步是完全沒有意義的。因為後面的b已經不匹配了,那前面的b也一定是不匹配的,同樣的情況其實還發生在第2個元素a上。

顯然,發生問題的原因在於p[j] == p[next[j]]。

所以我們也只需要新增乙個判斷條件即可:

public static int getnext(string ps)  else 

} else

}return next;

}

好了,至此。kmp演算法也結束了。

KMP演算法詳解

模式匹配的kmp演算法詳解 這種由d.e.knuth,j.h.morris和v.r.pratt同時發現的改進的模式匹配演算法簡稱為kmp演算法。大概學過資訊學的都知道,是個比較難理解的演算法,今天特把它搞個徹徹底底明明白白。注意到這是乙個改進的演算法,所以有必要把原來的模式匹配演算法拿出來,其實理解...

KMP演算法詳解

kmp演算法即knuth morris pratt演算法,是模式匹配的一種改進演算法,因為是名字中三人同時發現的,所以稱為kmp演算法。因為偶然接觸到有關kmp的問題,所以上網查了一下next陣列和 nextval陣列的求法,卻沒有找到,只有在csdn的資料檔案裡找到了next陣列的簡單求法 根據書...

KMP演算法詳解

相信很多人 包括自己 初識kmp演算法的時候始終是丈二和尚摸不著頭腦,要麼完全不知所云,要麼看不懂書上的解釋,要麼自己覺得好像心裡了解kmp演算法的意思,卻說不出個究竟,所謂知其然不知其所以然是也。經過七八個小時地仔細研究,終於感覺自己能說出其所以然了,又覺得資料結構書上寫得過於簡潔,不易於初學者接...