引入:寻找子串在源串中的起始位置。
传统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