KMP子字符串匹配算法学习笔记
文章目錄
- 學習資源
- 什么是KMP
- 什么是前綴表
- 為什么一定要用前綴表
- 如何計算前綴表
- 前綴表有什么問題
- 使用next數組來匹配
- 放碼過來
- 構造next數組
- 一、初始化
- 二、處理前后綴不相同的情況
- 三、處理前后綴相同的情況
- 使用next數組來做匹配
- 代碼總覽
- 測試代碼
- 時間復雜度分析
學習資源
什么是KMP
KMP算法是由這三位學者發明的:Knuth,Morris和Pratt,因此,用這三位學者名字的首字母組合成,來命名該算法。
KMP主要應用在字符串匹配上。KMP的主要思想是當出現字符串不匹配時,可以知道一部分之前已經匹配的文本內容,可以利用這些信息避免從頭再去做匹配了。所以如何記錄已經匹配的文本內容,是KMP的重點,也是next數組肩負的重任。
什么是前綴表
next數組就是一個前綴表(prefix table)。
前綴表是用來回溯的,它記錄了模式串與主串(文本串)不匹配的時候,模式串應該從哪里開始重新匹配。
為了清楚的了解前綴表的來歷,舉一個例子:
要在文本串:aabaabaafa中查找是否出現過一個模式串:aabaaf。
如動畫所示:
動畫里,特意把 子串aa 標記上了,這是有原因的,大家先注意一下,后面還會說道。
可以看出,文本串中第六個字符b 和 模式串的第六個字符f,不匹配了。如果暴力匹配,會發現不匹配,此時就要從頭匹配了。
但如果使用前綴表,就不會從頭匹配,而是從上次已經匹配的內容開始匹配,找到了模式串中第三個字符b繼續開始匹配。
此時就要問了前綴表是如何記錄的呢?
首先要知道前綴表的任務是當前位置匹配失敗,找到之前已經匹配上的位置,在重新匹配,此也意味著在某個字符失配時,前綴表會告訴你下一步匹配中,模式串應該跳到哪個位置。(MyNote:文本串不用跳轉)
那么什么是前綴表:下表i之前(包括i)的字符串中,有多大長度的相同前綴后綴。
(MyNote:本文“下表”的通假于“下標”。)
為什么一定要用前綴表
前綴表那為啥就能告訴我們 上次匹配的位置,并跳過去呢?
回顧一下,剛剛匹配的過程在下表5的地方遇到不匹配,模式串是指向f,如圖:
然后就找到了下表2,指向b,繼續匹配,如圖:
以下這句話,對于理解為什么使用前綴表可以告訴我們匹配失敗之后跳到哪里重新匹配 非常重要!
下表5之前這部分的字符串(也就是字符串aabaa)的最長相等的前綴 和 后綴字符串是 子字符串aa ,因為找到了最長相等的前綴和后綴,匹配失敗的位置是后綴子串的后面,那么我們找到與其相同的前綴的后面從新匹配就可以了。
所以前綴表具有告訴我們當前位置匹配失敗,跳到之前已經匹配過的地方的能力。
如何計算前綴表
接下來就要說一說怎么計算前綴表。如圖:
一、長度為前1個字符的子串a,最長相同前后綴的長度為0。(注意這里計算相同前后綴,不算重復的字符)
二、長度為前2個字符的子串aa,最長相同前后綴的長度為1。
三、長度為前3個字符的子串aab,最長相同前后綴的長度為0。
以此類推:
四、長度為前4個字符的子串aaba,最長相同前后綴的長度為1。
五、長度為前5個字符的子串aabaa,最長相同前后綴的長度為2。
六、長度為前6個字符的子串aabaaf,最長相同前后綴的長度為0。
那么把求得的最長相同前后綴的長度就是對應前綴表的元素,如圖:
可以看出前綴表里的數值代表著就是:當前位置之前的子串有多大長度相同的前綴后綴。
再來看一下如何利用 前綴表找到 當字符不匹配的時候應該指針應該移動的位置。如動畫所示:
找到的不匹配的位置, 那么此時我們要看它的前一個字符的前綴表的數值是多少。
為什么要看前一個字符的前綴表的數值呢,因為要找前面字符串的最長相同的前綴和后綴。
所以要看前一位的 前綴表的數值。
前一個字符的前綴表的數值是2, 所有把下表移動到下表2的位置繼續比配。可以再反復看一下上面的動畫。
最后就在文本串中找到了和模式串匹配的子串了。
前綴表有什么問題
來看一下剛剛求的這個前綴表有什么問題呢?
看這個位置紅框的位置,如果要找下表1 所對應 前綴表里的數值的時候,前綴表里的數值依然是1,然后就要跳到下表1的位置,如此就形成了一個死循環。
**如何怎么避免呢,就把前綴表里的數值統一減一, 開始位置設置為-1 **。 這一點對理解后面KMP代碼很重要!!
改為如圖所示:
這樣就避免的死循環,只不過后續取 前綴表里的數值的時候,要記得再+1,才是我們想要的值。
最后得到的新前綴表在KMP算法里通常用一個next數組來表示。
注意這個next數組就根據模式串求取的。
使用next數組來匹配
有了next數組,就可以根據next數組來 匹配文本串s,和模式串t了。
注意next數組是新前綴表(舊前綴表統一減一了)。
匹配過程動畫如下:
放碼過來
下文統稱haystack為文本串, needle為模式串。
haystack, needle出處。
構造next數組
定義一個方法getNext來構建next數組,參數為一個名為next數組,和一個字符串。代碼如下:
private void getNext(int[] next, String s) {}構造next數組其實就是計算模式串s,前綴表的過程。主要有如下三步:
一、初始化
定義兩個指針i和j:
- j指向前綴終止位置(嚴格來說是終止位置減一的位置),
- i指向后綴終止位置(與j同理)。
(通常是先i后j,為什么這里相反,接下來看代碼就清楚了。)
然后還要對next數組進行初始化賦值,如下:
int j = -1; next[0] = j;-
j 初始化為 -1原因是前文說過前綴表要統一減一的操作(避免死循環得情況),所以j初始化為-1。
-
next[] 表示 i(包括i)之前最長相等的前后綴長度(其實就是j),next[0]初始化為j 。
二、處理前后綴不相同的情況
因為j初始化為-1,那么i就從1開始,進行s[i] 與 s[j+1]的比較。(這里可能一開始不適應理解,不用急。)
所以遍歷模式串s的循環下表i 要從 1開始,代碼如下:
for(int i = 1; i < s.length(); i++) { // 注意i從1開始如果 s[i] 與 s[j+1]不相同,也就是遇到 前后綴末尾不相同的情況,就要回退。
如何回退?next[j]就是記錄著j(包括j)之前的子串的相同前后綴的長度。
那么 s[i] 與 s[j+1] 不相同,就要找 j+1前一個元素在next數組里的值(就是next[j])。
所以,處理前后綴不相同的情況代碼如下:
while (j >= 0 && s.charAt(i) != s.charAt(j + 1)) { // 前后綴不相同了j = next[j]; // 回退 }三、處理前后綴相同的情況
如果s[i] 與 s[j + 1] 相同,那么就同時向后移動i 和j 說明找到了相同的前后綴,同時還要將j(前綴的長度)賦給next[i], 因為next[i]要記錄相同前后綴的長度。
代碼如下:
if (s.charAt(i) == s.charAt(j + 1)) { // 找到相同的前后綴j++; } next[i] = j; // 將j(前綴的長度)賦給next[i]最后整體構建next數組的函數代碼如下:
private void getNext(int[] next, String s) {int j = -1;next[0] = j;for(int i = 1; i < s.length(); i++) { // 注意i從1開始while (j >= 0 && s.charAt(i) != s.charAt(j + 1)) { // 前后綴不相同了j = next[j]; // 向前回溯}if (s.charAt(i) == s.charAt(j + 1)) { // 找到相同的前后綴j++;}next[i] = j; // 將j(前綴的長度)賦給next[i]} }代碼構造next數組的邏輯流程動畫如下:
得到了next數組之后,就開始用它做匹配。
使用next數組來做匹配
在文本串haystack里找是否出現過模式串needle。定義兩個下表j 指向模式串起始位置,i指向文本串其實位置。
那么j初始值依然為-1,這是因為next數組里記錄的起始位置為-1。
i就從0開始,遍歷文本串,代碼如下:
for (int i = 0; i < haystack.length(); i++) { // 注意i就從0開始接下來就是 haystack.charAt(i) 與 needle.charAt(j + 1) (因為j從-1開始的) 進行比較。
如果 haystack.charAt(i) 與 needle.charAt(j + 1) 不相同,j就要從next數組里尋找下一個匹配的位置。
代碼如下:
while(j >= 0 && haystack.charAt(i) != needle.charAt(j + 1)) { // 不匹配j = next[j]; // j 尋找之前匹配的位置 }如果 haystack.charAt(i) 與 needle.charAt(j + 1) 相同,那么i 和 j 同時向后移動, 代碼如下:
if (haystack.charAt(i) == needle.charAt(j + 1)) { // 匹配,j和i同時向后移動 j++; }如果j指向了模式串t的末尾,那么就說明模式串t完全匹配文本串s里的某個子串了。
本題要在文本串字符串中找出模式串出現的第一個位置(從0開始),所以返回當前在文本串匹配模式串的位置i 減去 模式串的長度,就是文本串字符串中出現模式串的第一個位置。
代碼如下:
if (j == (needle.length() - 1) ) { // 文本串s里出現了模式串treturn (i - needle.length() + 1); }代碼總覽
public class KMP {private void getNext(int[] next, String s) {int j = -1;next[0] = j;for(int i = 1; i < s.length(); i++) { // 注意i從1開始while (j >= 0 && s.charAt(i) != s.charAt(j + 1)) { // 前后綴不相同了j = next[j]; // 向前回溯}if (s.charAt(i) == s.charAt(j + 1)) { // 找到相同的前后綴j++;}next[i] = j; // 將j(前綴的長度)賦給next[i]}}public int strStr(String haystack, String needle) {if (needle.length() == 0) {return 0;}int[] next = new int[needle.length()];getNext(next, needle);int j = -1; // // 因為next數組里記錄的起始位置為-1for (int i = 0; i < haystack.length(); i++) { // 注意i就從0開始while(j >= 0 && haystack.charAt(i) != needle.charAt(j + 1)) { // 不匹配j = next[j]; // j 尋找之前匹配的位置}if (haystack.charAt(i) == needle.charAt(j + 1)) { // 匹配,j和i同時向后移動 j++; }if (j == (needle.length() - 1) ) { // 文本串s里出現了模式串treturn (i - needle.length() + 1); }}return -1;} }測試代碼
import static org.junit.Assert.*;import org.junit.Test;public class KMPTest {@Testpublic void test() {KMP k = new KMP();assertEquals(2, k.strStr("hello", "ll"));assertEquals(-1, k.strStr("aaaaa", "bba"));assertEquals(3, k.strStr("aabaabaafa", "aabaaf"));}}時間復雜度分析
假設文本串長度為n,模式串長度為m。因為在匹配的過程中,根據前綴表不斷調整匹配的位置,可以看出匹配的過程是O(n),但之前還要單獨生成next數組,時間復雜度是O(m),所以整個KMP算法的時間復雜度是O(n+m)的。
暴力的解法顯而易見是O(n * m),所以KMP在字符串匹配中極大的提高的搜索的效率。
總結
以上是生活随笔為你收集整理的KMP子字符串匹配算法学习笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python(28)-文件,os模块
- 下一篇: 深度模型压缩论文(02)- BlockS