最详细KMP算法
目錄
?
1.KMP介紹
1.1什么是KMP
1.2KMP有什么用
2.前綴表的介紹
2.1什么是前綴表
2.2前綴表如何記錄
2.3為什么一定要用前綴表
2.4.如何計算前綴表
3.構造next數組
4.使用next數組做匹配
5.例題(實現strStr(Leetcode第28題))
5.1題目描述
5.2代碼實現
1.KMP介紹
1.1什么是KMP
之所以叫KMP是為了紀念發明者,分別為:Knuth,Morris和Pratt
1.2KMP有什么用
KMP主要是應用在字符串匹配上。
KMP的主要思想是當出現字符串不匹配時,可以知道一部分之前匹配的文本內容,可以利用這些信息避免從頭再去做匹配。
2.前綴表的介紹
2.1什么是前綴表
寫KMP時用到的next數組就是前綴表。
前綴表是用來回溯的,它記錄了模式串與主串(文本串)不匹配的時候,模式串應該從那里重新開始匹配。
2.2前綴表如何記錄
首先要知道前綴表的任務是當前位置匹配失敗,找到之前匹配上的位置,再重新匹配,此也意味著在某個字符失配時,前綴表會告訴一下步匹配中字符串應跳到那個位置。
2.3為什么一定要用前綴表
例如:要在文本串:aabaabaafa中查找是否出現過一個模式串:aabaaf。
在f之前最長相等的前綴和后綴字符串是子字符串aa,因為找到了最長相等的前綴和后綴,匹配失敗的位置是后綴子串的后面,那么我們找到與其相同的前綴的后面從新匹配就可以了。所以前綴表具有告訴我們當前位置匹配失敗跳到之前已經匹配過得地方的能力。
前綴:包含首字母,不包含尾字母的所有子串。例如:a;aa;aab;aaba;aabaa;
后綴:只包含尾字母不包含首字母的子串。例如:f;af;aaf;baaf;abaaf;
2.4.如何計算前綴表
長度為前1個字符的子串a,最長相同前后綴的長度為0。(注意這里計算相同前后綴,不算重復的字符)
長度為前2個字符的子串aa,最長相同前后綴的長度為1。
長度為前3個字符的子串aab,最長相同前后綴的長度為0。
以此類推:
長度為前4個字符的子串aaba,最長相同前后綴的長度為1。
長度為前5個字符的子串aabaa,最長相同前后綴的長度為2。
長度為前6個字符的子串aabaaf,最長相同前后綴的長度為0。
那么把求得的最長相同前后綴的長度就是對應前綴表的元素,如圖:
可以看出,前綴表里的數值代表著就是:當前位置之前的子串有多大長度相同的前后綴。
找到的不匹配的位置,那么此時我們要看它的前一個字符的前綴表數值是多少。
為什么要看前一個字符的前綴表的數值呢,因為要找前面字符串的最長相同的前綴和后綴,所以要看前一位的前綴表的數值。
3.構造next數組
我們定義一個函數getNext來構建next數組,函數參數為指向next數組的指針,和一個字符串。代碼如下:
void getNext(int* next, const string& s)「構造next數組其實就是計算模式串s,前綴表的過程?!?/strong>?主要有如下三步:
初始化
處理前后綴不相同的情況
處理前后綴相同的情況
接下來我們詳解詳解一下。
初始化:
定義兩個指針i和j,j指向前綴終止位置(嚴格來說是終止位置減一的位置),i指向后綴終止位置(與j同理)。然后還要對next數組進行初始化賦值,如下:
int j = -1; next[0] = j;j 為什么要初始化為 -1呢,因為之前說過 前綴表要統一減一的操作,所以j初始化為-1。next[i] 表示 i(包括i)之前最長相等的前后綴長度(其實就是j)
所以初始化next[0] = j 。
? ? ?2.處理前后綴不相同的情況
?因為j初始化為-1,那么i就從1開始,進行s[i] 與 s[j+1]的比較。所以遍歷模式串s的循環下表i 要從 1開始,代碼如下:
for(int i = 1; i < s.size(); i++) {如果 s[i] 與 s[j+1]不相同,也就是遇到 前后綴末尾不相同的情況,就要向前回溯。
怎么回溯呢?
next[j]就是記錄著j(包括j)之前的子串的相同前后綴的長度。
那么 s[i] 與 s[j+1] 不相同,就要找 j+1前一個元素在next數組里的值(就是next[j])。
所以,處理前后綴不相同的情況代碼如下:
while (j >= 0 && s[i] != s[j + 1]) { // 前后綴不相同了j = next[j]; // 向前回溯 }? ? ? 3.處理前后綴相同的情況
如果s[i]?與 s[j + 1] 相同,那么就同時向后移動i 和j 說明找到了相同的前后綴,同時還要將j(前綴的長度)賦給next[i], 因為next[i]要記錄相同前后綴的長度。
代碼如下:
if (s[i] == s[j + 1]) { // 找到相同的前后綴j++; } next[i] = j;最后整體構建next數組的函數代碼如下:
void getNext(int* next, const string& s){int j = -1;next[0] = j;for(int i = 1; i < s.size(); i++) { // 注意i從1開始while (j >= 0 && s[i] != s[j + 1]) { // 前后綴不相同了j = next[j]; // 向前回溯}if (s[i] == s[j + 1]) { // 找到相同的前后綴j++;}next[i] = j; // 將j(前綴的長度)賦給next[i]} }4.使用next數組做匹配
在文本串s里 找是否出現過模式串t。
定義兩個下標,j 指向模式串起始位置,i指向文本串其實位置。
那么j初始值依然為-1,為什么呢?「依然因為next數組里記錄的起始位置為-1?!?/strong>
i就從0開始,遍歷文本串,代碼如下:
for (int i = 0; i < s.size(); i++)接下來就是 s[i] 與 t[j + 1] (因為j從-1開始的) 進行比較。
如果 s[i] 與 t[j + 1] 不相同,j就要從next數組里尋找下一個匹配的位置。
代碼如下:
while(j >= 0 && s[i] != t[j + 1]) {j = next[j]; }如果 s[i] 與 t[j + 1] 相同,那么i 和 j 同時向后移動, 代碼如下:
if (s[i] == t[j + 1]) {j++; // i的增加在for循環里 }如何判斷在文本串s里出現了模式串t呢,如果j指向了模式串t的末尾,那么就說明模式串t完全匹配文本串s里的某個子串了。
5.例題(實現strStr(Leetcode第28題))
5.1題目描述
看題很容易知道是典型的KMP算法題。
KMP的經典思想就是:「當出現字符串不匹配時,可以記錄一部分之前已經匹配的文本內容,利用這些信息避免從頭再去做匹配?!?/strong>
具體過程就不再講述了,直接上代碼。
5.2代碼實現
class Solution { public:void getNext(int* next, const string& s) {int j = -1;next[0] = j;for(int i = 1; i < s.size(); i++) { // 注意i從1開始while (j >= 0 && s[i] != s[j + 1]) { // 前后綴不相同了j = next[j]; // 向前回溯}if (s[i] == s[j + 1]) { // 找到相同的前后綴j++;}next[i] = j; // 將j(前綴的長度)賦給next[i]}}int strStr(string haystack, string needle) {if (needle.size() == 0) {return 0;}int next[needle.size()];getNext(next, needle);int j = -1; // // 因為next數組里記錄的起始位置為-1for (int i = 0; i < haystack.size(); i++) { // 注意i就從0開始while(j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配j = next[j]; // j 尋找之前匹配的位置}if (haystack[i] == needle[j + 1]) { // 匹配,j和i同時向后移動 j++; }if (j == (needle.size() - 1) ) { // 文本串s里出現了模式串treturn (i - needle.size() + 1); }}return -1;} };如果有還沒有完全明白的,可以關注微信公眾號:代碼隨想錄
bilibili視頻:https://www.bilibili.com/video/BV1M5411j7Xx/?spm_id_from=trigger_reload
此文章是根著代碼隨想錄學習之后編寫的,內容上大致一樣,但從中受益匪淺。
總結
- 上一篇: c语言 统计数量用count_请问c语言
- 下一篇: 程序员面试系列——插入排序