引入:尋找子串在源串中的起始位置。
傳統C++代碼如下:
#include<iostream>
#include<string>
using namespace std;
//KMP---常規操作
int find_substr_location(string str, string pattern)
{
int size1 = str.size();
int size2 = pattern.size();
for (int i = 0; i <= size1-size2; i++)
{
int Flag = true;
for (int j = 0; pattern[j]!= '\0'; j++)
{
if (str[i + j] != pattern[j])
{
Flag = false;
break;
}
else;
}
if (Flag == true)
{
return i;
}
else;
}
return -1;
}
返回值:-1和其他,對應找不到和找到子串的起始位置下標。n = |S| 爲串 S 的長度,m = |P| 爲串 P 的長度,考慮最壞的情況即最後一個字符失配對應的時間複雜度爲:O(n) = |P| *( |S| - |P| + 1) 考慮到主串一般遠遠大於子串,所以O(n) = |P|*|S|
字符串比較只能依次比較沒有辦法減少,所以考慮減少比較趟數。
核心:跳過不可能匹配成功的字符即依次失配後源串索引的移動不再是加一。
針對模式串引入了next數組,定義::next[i] 表示 P[0] ~ P[i] 這一個子串,使得 前k個字符恰等於後k個字符 的最大的k,k<i+1.
改進算法規則:失配之後源串索引移動很多位,跳過不可能匹配的字符位置,怎麼確定移動多少位?
第一次比較,模式串失配位置爲S[3]、P[3]、模式串下標記爲p,源串下表記爲s,此時失配移動後的模式串下標p = next[p-1]=1、s=3。
第二次比較,起始位置S[3]、P[1],模式串失配位置下標爲6,此時失配移動後的模式串下標p = next[p-1]=3,s=8。
第三次比較,起始位置S[8]、P[3]、匹配成功,返回s-p即可。
使用暴力構造next數組實現的KMP代碼如下:
//字符串截取方法(start,end)前閉後閉區間
string str_truncation(string str, int start, int end)
{
string s="";
int size = str.size();
if (start <= end && 0 <= start &&start < size && 0 <= end && end < size) {
if (start == end) {
s += str[start];
}
else {
for (int i = start; i <= end; i++) {
s += str[i];
}
}
}
else;
return s;
}
// 暴力構建next數組
void get_next(string pattern, vector<int> &next)
{
int size = pattern.size();
int i = 0;
next.push_back(0);
for (int x = 1; x < size; x++)
{
int Flag = false;
//下標爲x的索引對應的next數組的值
for (i = x; i >= 1; i--) {
if (str_truncation(pattern, 0, i-1) == str_truncation(pattern, x - i + 1, x)) {
next.push_back(i);
Flag = true;
break;
}
}
if (Flag==false) {
next.push_back(0);
}
}
}
int KMP(string str, string pattern)
{
int size = str.size();
vector<int> next;
get_next(pattern, next);
int tar = 0;
int pos = 0;
while(tar <size) {
if (str[tar] == pattern[pos]) {
tar++;
pos++;
}
//避免第一個字符就不匹配索引爲負值
else if (pos) {
pos = next[pos - 1];
}
else {
tar++;
}
if (pos == pattern.size()) {
return tar - pos;
}
}
return -1;
}
int main()
{
//測試組()
//string str1, pattern1;
//cin >> str1 >> pattern1;
//string str2, pattern2;
//cin >> str2 >> pattern2;
//cout << find_substr_location(str1, pattern1) << endl;
//cout << find_substr_location(str2, pattern2) << endl;
string str3, pattern3;
cin >> str3>> pattern3;
cout << KMP(str3, pattern3) << endl;
//測試字符串截取函數str_truncation
//string s = "sdsdfhhghg";
//cout<<str_truncation(s,8,8) << endl;
//測試 get_next方法
//string s = "abcdefghabcd";
//vector<int> vec;
//get_next(s, vec);
//for (int i = 0; i < vec.size(); i++) {
// cout << vec[i] << " ";
//}
return 0;
}
問題及解決方案:
下文均以tar、pos來依次代替源串索引、模式串索引。
1.0 考慮到字符比較不相等時會取其失配位置前一位對應的next值爲新的pos即 pos= next[pos-1],可能從第一個元素就失配會造成越界訪問,所以在更新pos位置時判斷是否pos爲0,不是則pos= next[pos-1],否則tar+=1.
2.0 字符串截取方法先使用了substr(start,n)方法,浪費大量時間找問題,居然最後是使用的函數有問題,大意了substr(int start,int n) 從start索引位置取n個字符(包括start)。所以後面自己實現了一個str_truncation來截取字符串。
現在比較的趟數已經大大減少,可否快速構建next數組?
上圖欲求X位置在next數組裏的值,將X-1記爲now,假設P[X]=d,P[next[now]=P[X]=d,那麼易得出next[X] = next[now]+1。
如果P[X] != d,那麼需要將now的位置左移,移動多少位?按照上圖需要移動至now=next[now-1]即2,再來比較P[X]是否等於P[now],P[now]不等於P[X]時需要縮減now (now = next[now-1])直至P[X]=P[now],此刻如果now不等於0的條件下next[X] = next[now-1]+1。
next快速構建代碼如下:
// 快速構建next數組
void get_next_fast(string pattern, vector<int>& next)
{
next.push_back(0);
int X = 1;
int now = 0;
while ( X<pattern.size() ) {
if (pattern[now] == pattern[X]) {
X++;
now++;
next.push_back(now);
}
//縮減now
else if (now) {
now = next[now - 1];
}
else {
next.push_back(0);
X++;
}
}
}
時間複雜度O(n)=m,再加上KMP匹配的複雜度爲O(n) = n,總複雜度:O(n) = n+m