如何在目標字符串 S 中,查找是否存在子串 P?
int sub_index(const char *s, const char* p) { int ret = -1; int sl = strlen(s); int pl = strlen(p); int len = sl - pl; for (int i = 0; (ret < 0) && (i <= len); ++i) { bool equal = true; for (int j = 0; equal && (j<pl); ++j) equal = equal && (s[i + j] == p[j]); ret = (equal ? i : -1); } return ret; }
由於,pa
!= pb
且 pb
== sb
;
因此,Pa
!= sb
;
結論,字串 p 右移 1 位比較,沒有意義且低效。ios
KMP算法是一種改進的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,所以人們稱它爲克努特—莫里斯—普拉特操做(簡稱KMP算法)。KMP算法的核心是利用匹配失敗後的信息,儘可能減小模式串與主串的匹配次數以達到快速匹配的目的算法
- 匹配失敗時的右移位數與子串自己相關,與目標串無關
- 移動位數 = 已匹配的字符數 - 對應的部分匹配值
- 任意字串都存在一個惟一的部分匹配表
用法:
==> 第 7 位匹配失敗
==> 前 6 位匹配成功
==> 查表PMT[6]
==> 右移位數 6 - PMT[6] = 6 - 2 = 4編程
前綴優化
- 除了最後一個字符之外,一個字符串的所有頭部組合
後綴spa
- 除了第一個字符之外,一個字符串的所有尾部組合
部分匹配值code
- 前綴和後綴最長共有元素的長度
字符 | 前綴 | 後綴 | 交集 | 匹配值 | |
1 | A | 空 | 空 | 空 | 0 |
2 | AB | A | B | 空 | 0 |
3 | ABC | A,AB | BC,C | 空 | 0 |
4 | ABCD | A,AB,ABC | BCD,CD,D | 空 | 0 |
5 | ABCDA | A,AB,ABC,ABCD | BCDA,CDA,DA,A | A | 1 |
6 | ABCDAB | A,AB,ABC,ABCD,ABCDA | BCDAB,CDAB,DAB,AB,B | AB | 2 |
7 | ABCDABD | A,AB,ABC,ABCD,ABCDA,ABCDAB | BCDABD,CDABD,DABD,ABD,BD,D | 空 | 0 |
"ABCDABD" 部分匹配值爲 2blog
- (1) PMT[1] = 0 (下標爲 0 的元素匹配值爲 0)
- (2) 從 2 個字符開始遞推 (從下標爲 1 的字符開始遞推)
- (3) 假設 PMT[n] = PMT[n - 1] + 1 (最長共有元素的長度)
- (4) 當假設不成立, PMT[n] 在 PMT[n-1] 的基礎上減小
- LL : longest length (前綴、後綴交集元素的最長長度)
- (a) 當前要求的 LL 值經過歷史 LL 值推導 (+1)
- (b) 當可選的 LL 值爲0,直接比較首首尾元素
- (c) 將已匹配最長交集元素做爲種子,分別向後擴展一個字符,比較擴展字符
分解:
(0) "a" LL = 0
第1個元素的LL值爲0,即 PMT[1] = 0。字符串
(1) "ab" LL = 0
從第1個元素開始推導,假設 PMT[2] = PMT[1] + 1;
判斷假設是否成立:,當可選的LL值爲0,直接比較首尾元素, a != b
假設不成立,因此 PMT[2] = 0;get
(2) "aba" LL = 1
假設 PMT[3] = PMT[2] + 1;
判斷假設是否成立:,當可選的LL值爲0,直接比較首尾元素, a == a
假設成立,因此 PMT[3] = 0 + 1 = 1string
(3) "abab" LL = 2
假設 PMT[4] = PMT[3] + 1;
判斷假設是否成立: 將已匹配最長交集元素做爲種子,分別向後擴展一個字符,比較擴展字符,a-> ab | a->ab
b == b 假設成立,因此 PMT[4] = 1 + 1 = 2
(4) "ababa" LL = 3
假設 PMT[5] = PMT[4] + 1;
判斷假設是否成立: 將已匹配最長交集元素做爲種子,分別向後擴展一個字符,比較擴展字符,ab-> aba | ab->aba
a == a 假設成立,因此 PMT[5] = 2 + 1 = 3
(5) "ababax" LL = 0
假設 PMT[6] = PMT[5] + 1;
判斷假設是否成立: 已匹配最長交集元素做爲種子,分別向後擴展一個字符,比較擴展字符,aba-> abab | aba->abax
b != x 假設不成立;
==> 將已匹配最長交集元素最爲總支再次嘗試匹配
"aba|b" "aba|x" ==> "aba" ==> LL1
= PMT(3) = 1; ==> 交集元素 "a"
將已匹配最長交集元素做爲種子,分別向後擴展一個字符,比較擴展字符:a-> ab | a->ax;
b != x 匹配失敗
==> 將已匹配最長交集元素最爲總支再次嘗試匹配
"a|b" "a|x" ==> "a" ==> LL2
= PMT(1) = 0; 【嘗試匹配結束條件】
當可選的LL值爲0,直接比較首尾元素, a != x, 因此PMT[6] = 0
文件:main.cpp
#include <iostream> #include <cstring> using namespace std; int *make_pmt(const char *s) { size_t len = strlen(s); int *ret = static_cast<int*>(malloc(sizeof(int) * len)); if (ret != nullptr) { int ll = 0; // 第0個元素的ll值爲0 ret[0] = 0; for (size_t i=1; i<len; ++i) { // 假設不成功時,再次嘗試擴展 while ((ll > 0) && (s[i] != s[ll])) { ll = ret[ll - 1]; } // 對首尾字符或者擴展後的字符進行判斷 if (s[i] == s[ll]) { // 假設成功時,ll自加 ++ll; } ret[i] = ll; } } return ret; } int main() { int * pmt = make_pmt("ababax"); for (size_t i=0; i<strlen("ababax"); ++i) { cout << "i" << ":" << pmt[i] << endl; } free(pmt); return 0; }
輸出:
i:0 i:0 i:1 i:2 i:3 i:0
==> 下標 j 處匹配失敗
==> 前 j 位匹配成功
==> 查表 PMT[j-1]
==> 右移位數 j - PMT[j-1]
由於, S[i] != p[j]
因此,查表, LL = PMT[j - 1]
因而,右移,i 的值不變, j 的值改變, j = j - (j - LL) = LL = PMT[j-1]
文件:main.cpp
#include <iostream> #include <cstring> using namespace std; int *make_pmt(const char *p) // O(m) { size_t len = strlen(p); int *ret = static_cast<int*>(malloc(sizeof(int) * len)); if (ret != nullptr) { int ll = 0; // 第0個元素的ll值爲0 ret[0] = 0; for (size_t i=1; i<len; ++i) { // 假設不成功時,再次嘗試擴展 while ((ll > 0) && (p[i] != p[ll])) { ll = ret[ll - 1]; } // 對首尾字符或者擴展後的字符進行判斷 if (p[i] == p[ll]) { // 假設成功時,ll自加 ++ll; } ret[i] = ll; } } return ret; } int kmp(const char *s, const char *p) // O(m + n) { int ret = -1; int sl = strlen(s); int pl = strlen(p); int *pmt = make_pmt(p); // O(m) if ((pmt != nullptr) && (0 < pl) &&(pl <= sl)) { for (int i=0, j = 0; i < sl; ++i) // O(n) { while ((j > 0) && (s[i] != p[j])) { j = pmt[j - 1]; // j = j - (j - LL) = LL = PMT[j-1]; } if (s[i] == p[j]) { ++j; } if (j == pl) { ret = i + 1 - pl; break; } } } free(pmt); return ret; } int main() { cout << kmp("abcde", "cde") << endl; cout << kmp("abcde", "") << endl; cout << kmp("abcde", "a") << endl; cout << kmp("abcde", "bc") << endl; return 0; }
輸出:
2 -1 0 1
- 部分匹配表是提升字串查找效率的關鍵
- 部分匹配值定義爲前綴和後綴最長共有元素的長度
- 能夠用遞推的方法產生部分匹配表
- KMP 利用部分匹配值與字串移動位數的關係提升查找效率
以上內容整理於狄泰軟件學院系列課程,請你們保護原創!