如何在目标字符串S中,查找是否存在子串P?
朴素解法:
//字符串S中查找子串P的位置
int sub_str_index(const char* s, const char* p)
{
int ret = -1;
int s1 = strlen(s);
int p1 = strlen(p);
int len = s1 - p1;
for(int i = 0; (ret < 0) && (i <= len); i++)
{
bool equal = true;
for(int j = 0; equal && (j < p1); j++)
{
equal = equal && (s[i + j] == p[j]);
}
ret = equal ? i : -1;
}
return ret;
}
朴素解法的一个优化线索
因为,Pa != Pb != Pc 且 Pc== Sc; 所以,Pa != Sb Pa != Sc
结论,子串p右移1位比较,没有意义。完全可以无脑右移3位再比较
示例
伟大的发现
-匹配失败时的右移位数与子串本身相关,与目标串无关
-移动位数=已匹配的字符数-对应的部分匹配值 (当部分匹配值为0时,即可无脑右移已匹配的字符数)
-任意子串都存在一个唯一的部分匹配表
部分匹配表示例
前缀:除了最后—个字符以外,一个字符串的全部头部组合
后缀:除了第—个字符以外,—个字符串的全部尾部组合
部分匹配值:前缀和后缀最长共有元素的长度
#include <iostream>
#include <string.h>
using namespace std;
int* make_pmt(const char* p) //O(n)
{
int len = strlen(p);
int* ret = static_cast<int*>(malloc(sizeof(int) * len));
if(ret != NULL)
{
int ll = 0; // 前后缀交集元素最大长度 large length
ret[0] = 0; //长度为1的字符串ll值为0
for(int i = 1; i < len; i++)
{
// 第一次循环不会执行,先跳过这里,理解后面的
while((ll > 0) && (p[i] != p[ll]))
{
cout << "p[" << i << "] != p[" << ll << "]: " << ll << " -> " << ret[ll-1] << "(ret[" << ll-1 << "])" << endl
// 说明当前ll之前的部分匹配成功,p[ll]匹配失败,那么ll的索引需要调整
// 现在需要考虑的是p[i]需要和谁比较,已经知道p[ll-1]是匹配成功,ll-1位置的是否有公共部分呢,有如果没有,那么ret[ll-1]就是0,p[i]直接可以从头开始比较
// 如果有公共部分,那么p[i]就只需与公共部分的后一个比较即可,看结果分析图
ll = ret[ll-1];
}
// 相当于子串的自身比较,例如:
// ABCDABD i = 1, 2, 3, 4
// ABCDABD ll : 两个含义:当前正在匹配的下标 或 部分匹配值
if(p[i] == p[ll])
{
cout << "p[" << i << "] == p[" << ll << "]: " << ll << " -> " << ll+1 << endl;
ll++; // i位置匹配到了
}
cout << "ret[" << i << "] : " << ll << endl << endl;
ret[i] = ll; //部分匹配表存放部分匹配值
}
}
return ret;
}
int main()
{
int* pmt = make_pmt("ABCDABCABCDABCD");
for(int i = 0; i < strlen("ABCDABCABCDABCD"); i++)
{
cout << i << " : " << pmt[i] << endl;
}
return 0;
}
部分匹配表的使用(KMP算法)
KMP子串查找算法的实现
#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;
int* make_pmt(const char* p) //O(n)
{
int len = strlen(p);
int* ret = static_cast<int*>(malloc(sizeof(int) * len));
if(ret != NULL)
{
int ll = 0;
ret[0] = 0; //长度为1的字符串ll值为0
for(int i = 1; i < len; i++)
{
while((ll > 0) && (p[ll] != p[i]))
{
ll = ret[ll-1];
}
if(p[ll] == p[i])
{
ll++;
}
ret[i] = ll; //部分匹配表存放部分匹配值
}
}
return ret;
}
int kmp(const char* s, const char* p) //O(m)+O(n)=O(m+n)
{
int ret = -1;
int s1 = strlen(s);
int p1 = strlen(p);
int* pmt = make_pmt(p);
if((pmt != NULL) && (0 < p1) && (p1 <= s1))
{
for(int i=0, j=0; i < s1; i++)
{
while((j > 0) && (s[i] != p[j]) )
{
j = pmt[j-1];
}
if(s[i] == p[j])
{
j++;
}
if(j == p1) // 成功完全匹配
{
ret = i + 1 - p1;
break;
}
}
}
free(pmt);
return ret;
}
int main()
{
cout << kmp("ABCDABD", "ABD") << endl; // 4
cout << kmp("ABCDABD", "ABDC") << endl; // -1
return 0;
}
发现从求PMT,到KMP两者的步骤非常相似...