字串匹配問題——KMP演算法
字串匹配問題:給定兩個字串S(主串)和T(模式串),假設n=strlen(S) > strlen(T)=m,判斷主串S中是否包含模式T,且返回T在S中所在的起始位置。這裡為簡單起見,若S包含T,則只返回第一個T所在的位置。
一般的蠻力法如下:
蠻力法在遇到不匹配時,j每次都要回到T的起點,從新開始匹配,這樣來看效率就比較低,蠻力法的時間複雜度是O(n*m)。
1、理論
思想:儘量利用已經部分匹配的結果資訊,儘量讓 i 不回溯,加快模式串T的滑動速度。
先舉個啟發性的例子,然後引出理論。例子:
結論:從例子中可以看出,每次匹配失敗,然後回溯再次從新匹配時(第2、4、5趟),判斷當前的S[i]和T[j]是否相等可通過上一趟S和T的匹配結果和T本身的結構資訊來得出結論。也就是說,在上一趟S和T匹配結果的基礎上,如果知道T本身的結構資訊,那麼第2、4、5趟的步驟其實是可以省略的,且i可以不用回溯(i不回溯待會兒再來直觀理解)。
1.1、模式串T中的資訊
抓住部分匹配時的兩個特徵:
如上圖所示,假設S[i]和T[j]不相等後,從T的第k個位置開始和S中的第i個位置比較,那麼:
(1)從圖的後半部分可得:
(1)
(2)從圖的後前部分可得:
(2)
所以,兩式聯立可得:
(3)
(3)
所以,由上面的推理可知,只要提前知道模式串T本身的結構資訊,就可以由j計算出k,即下一回T的匹配起點。另外,(3)式可能存在多種不同長度的首尾相等的可能,那麼在這樣的情況下,肯定是k越大越好,這樣一方面可以減少再次匹配的字元數量;另一方面,T向右移動的位置相當S而言也是最慢的,這樣也不會錯過各種匹配的情況。比如:
令k = next[ j ],則:
計算next[j]的方法:
由模式T的字首函式定義易知,next[1]=0,假設已經計算出next[1],next[2],…,next[j],如何計算next[j+1]呢?設k=next[j],則已有:
(4)
此時,比較tk和tj,可能出現兩種情況:
(1)tk=tj:說明t1 … tk-1 tk=tj-k+1 … tj-1 tj,由字首函式定義,next[j+1]=k+1;
(2)tk≠tj:此時要找出t1 … tj-1的字尾中第2大真字首,顯然,這個第2大的真字首就是next[next[j]]=next[k]。
再比較tnext[k]和tj,此時仍會出現兩種情況,當tnext[k]=tj時,與情況(1)類似,next[j]=next[k]+1;當tnext[k]≠tj時,與情況(2)類似,再找t1 … tj-1的字尾中第3大真字首。重複執行上述步驟,直到找到t1
… tj-1的字尾中的最大真字首,或確定t1 … tj-1的字尾中不存在真字首,此時,next[j+1]=1。
求next陣列程式碼:
void getNext(char T[], int next[])
{
next[1] = 0; //從第一個位置開始
int j = 1, k = 0; //這裡比較巧妙,先不用管什麼意思,在相關的地方驗證一下,然後就會明白了
while (j < T[0])
{
if ((k == 0) || (T[j] == T[k]))
{
j++;
k++;
next[j] = k;
}
else
k = next[k]; //往前找
}
}
2、KMP完整演算法
KMP中的核心就是求next陣列,之後就沒什麼關鍵的了。KMP的時間複雜度為O(n+m),當n>>m時,時間複雜度是O(n)。完整程式碼如下:
int calLen(char *p) //計算字元陣列長度
{
int count = 0;
while (*(++p) != '\0') //從第一個位置開始
{
count++;
}
return count;
}
void getNext(char T[], int next[])
{
next[1] = 0; //從第一個位置開始
int j = 1, k = 0; //這裡比較巧妙,先不用管什麼意思,在相關的地方驗證一下,然後就會明白了
while (j < T[0])
{
if ((k == 0) || (T[j] == T[k]))
{
j++;
k++;
next[j] = k;
}
else
k = next[k]; //往前找
}
}
#include<iostream>
using namespace std;
#define STR_LEN 52
int main(int argc, char* argv[])
{
int i = 1, j = 1;
char S[STR_LEN], T[STR_LEN];
int next[STR_LEN];
cout << "輸入源字串:" << endl;
cin >> S + 1; //第0個位置用於儲存長度
cout << "輸入目標字串:" << endl;
cin >> T + 1;
T[0] = calLen(T);
S[0] = calLen(S);
if (T[0] > S[0] || T[0] == 0 || S[0] == 0)
{
cout << "字串格式輸入錯誤!" << endl;
system("pause");
return 0;
}
getNext(T, next);
while (S[i] && T[j]) //判斷字串是否結束
{
if (S[i] == T[j])
{
i++;
j++;
}
else //將T向右滑動
{
j = next[j]; //將計算出來的k賦值給j
if (j == 0) //如果j==0,表明剛剛在j==1處(即一開始)兩個字串就不相等,那麼S中的i要往右移動一位
{
i++;
j++;
}
}
}
if (T[j] == '\0')
cout << "找到目標子串,從S中的第" << i - T[0] << "個位置開始!" << endl;
else
cout << "沒有匹配的目標子串!" << endl;
system("pause");
return 0;
}
參考文獻:
1、演算法設計與分析-王紅梅相關文章
- KMP字串匹配演算法KMP字串匹配演算法
- 字串匹配演算法:KMP字串匹配演算法KMP
- 【字串匹配】KMP字串匹配KMP
- 字串匹配之KMP《演算法很美》字串匹配KMP演算法
- 第五章 字串專題 ---------------- 字串匹配(二)----KMP演算法字串匹配KMP演算法
- 字串匹配基礎下——KMP 演算法字串匹配KMP演算法
- kmp字串匹配,A星尋路演算法KMP字串匹配演算法
- 字串匹配演算法(三)-KMP演算法字串匹配演算法KMP
- 字串匹配-BF演算法和KMP演算法字串匹配演算法KMP
- 匹配字串之——KMP演算法深入理解字串KMP演算法
- 快速字串匹配一: 看毛片演算法(KMP)字串匹配演算法KMP
- 神奇的字串匹配:擴充套件KMP演算法字串匹配套件KMP演算法
- KMP字串模式匹配詳解KMP字串模式
- 字串匹配演算法之 BF 和 KMP 講解字串匹配演算法KMP
- KMP字串匹配學習筆記KMP字串匹配筆記
- 圖解KMP字串匹配演算法+程式碼實現圖解KMP字串匹配演算法
- 字串匹配模式問題字串匹配模式
- KMP模式匹配演算法KMP模式演算法
- 【leetcode】28. Implement strStr() 字串匹配KMP BMLeetCode字串匹配KMP
- 模式匹配kmp演算法(c++)模式KMP演算法C++
- 【資料結構與演算法】字串匹配(Rabin-Karp 演算法和KMP 演算法)資料結構演算法字串匹配KMP
- 字串演算法--$\mathcal{KMP,Trie}$樹字串演算法KMP
- Python 細聊從暴力(BF)字串匹配演算法到 KMP 演算法之間的精妙變化Python字串匹配演算法KMP
- 字串匹配演算法(一)字串匹配演算法
- LeetCode_0028. 找出字串第一個匹配項的下標,KMP演算法的實現LeetCode字串KMP演算法
- KMP-字串KMP字串
- 第五章 字串專題 ---------------- 字串匹配(三)----字尾陣列演算法字串匹配陣列演算法
- [譯] Swift 演算法學院 - KMP 字串搜尋演算法Swift演算法KMP字串
- KMP演算法(Leetcode第28題)KMP演算法LeetCode
- 單模式匹配 KMP 演算法 簡易版學習筆記模式KMP演算法筆記
- 字串匹配演算法【未完待續】字串匹配演算法
- 字串匹配字串匹配
- 【資料結構與演算法】字串匹配資料結構演算法字串匹配
- 藍橋杯演算法提高——字串匹配(Java)演算法字串匹配Java
- 演算法之字串——正規表示式匹配演算法字串
- 字串匹配演算法(二)-BM演算法詳解字串匹配演算法
- 程式碼隨想錄演算法訓練營第9天 | 字串(KMP演算法) 28. 找出字串中第一個匹配項的下標 459.重複的子字串演算法字串KMP
- 第五章 字串專題 ---------------- 5.12 字串匹配之PabinKarp字串匹配