本文将详细的介绍串的匹配算法,着重点在串的kmp算法,同时以我自己的理解给出了尽可能详细尽可能完整的介绍,同时也对KMP算法的部分操作给出了我个人理解下的数学证明。
1.基本概念
串的匹配是为了找到一个子串在主串中的位置
2.匹配算法
1.暴力匹配
若不匹配,主串指针每次移动一位,将子串与主串按顺序进行比较,如果比较失败,继续移动。子串指针重置
//暴力匹配算法(不断的通过遍历来完成匹配)(寻找第一次出现的位置)
int Index(sqstring L,sqstring T)
{
int count=0;
for (int i = 0; i < L->len; i++)
{
count = i;
int check = 0; //作用在于判断之后的串是否匹配
if (T->a[0] == L->a[i]&&i<L->len-T->len) //确定第一位相同
{
check += 1;
for (int n = 1; n < T->len; n++) //依次匹配后面的每一位
{
if (T->a[n] == L->a[i + n])
{
check += 1;
}
else
{
break;
}
}
if (check == T->len)
{
break;
}
}
}
return count;
}
2.KMP算法
在一些特殊的串中使用暴力匹配算法将会使得计算量明显增加
如下 主串 0000000000000000001
子串 0001
这样如果直接使用暴力匹配算法将会导致运算量变得很大
KMP算法可以很好的简化计算量
注意KMP算法在时间上是占优的在空间上是劣势的相当于用空间换时间
KMP算法
算法目的与基本想法
首先看下方匹配可以发现实际上在暴力匹配过程中会出现大量的无效匹配造成时间的浪费
(即在下方的暴力匹配操作中实际上第二趟匹配第三趟匹配是完全没有必要的,所以需要尽可能规避这些操作)
(图片来源:知乎博主justcoder)
具体的操作思想如下
将模式串首先于主串比较
当出现不同时,我们考虑比较部分,在已比较正确部分(注意这一点很重要)中找到后缀与前缀相同,将前缀移动到相应的后缀部分,再重新比较非相同部分的值(注意前缀不包含第一个值,后缀不包含最后一个值)
重复以上操作指导找出匹配的区域
关于后缀移动的合理性的证明(为什么一定是移动到公共前后缀)
前置要求:
移动的合理性要求:移动后在已经比较的主串范围内模式串不能出现与主串匹配失败的元素
证明:
设p1p2p3.......pn是一个任意字符串,
那么p1p2...pm是前缀(m为任意小于n的整数)
则p(n-m+1).......pn是对应的后缀
设当m=t时取得这个字符串的最大公共前后缀(前后缀相等)
反设
在字符串中存在
pk,p(k+1).......p(k+m) 其中1<k,k+m<n
满足和最大前缀(后缀)相等
当将前缀移动到和这个字符串重合时
要满足匹配的合理性条件(在这个区域内的模式串的值要与主串相同)则需要满足
p(k+m+1)=p(m+1)
......
p(n)=p(n-k)
则
p1p2......p(n-k) [由k+m<n,所以n-k>m]与p(k)p(k+1)......p(n)相等
则与最大前后缀的条件矛盾
因此只能是从最大前缀移动到最大公共后缀才能满足合理性。
NEXT数组的创建
值的含义:
next【0】目前国内教材上有两种定义方法,一种定义为0(定义为0的书上通常用串的首位存储长度),一种定义为-1(个人喜好这个,个人喜好独立存储串长度)
next【i】:从第起始位(有些从1起始有些从0起始)到第i-1位的最大公共前后缀长度。
基本信息
我们的目光先回到最上面的图片,实际上我们可以发现:
跳跃步数=已匹配长度-公共前后缀长度
已匹配长度:指针直接指出
公共前后缀长度:计算并且存储在定义的next数组中(next数组的来源)
NEXT数组的创建代码(求解公共前后缀)
KMP算法最核心之处
(图片转载自csdn博主yyzsir)
基本算法与理解
1.next的首位设为0,第一位设为1(这是进行递推的基础)
2.p1p2p3.......p(n-1)p(n)是一列字符串
那么不妨设 p1p2p3p4 || p(n-3)p(n-2)p(n-1)p(n)为公共前缀
那么当插入新的元素
后缀变为p(n-3)p(n-2)p(n-1)p(n)p(n+1),那么显然只需要证明p(n+1)=p5即可说明两者最大公共前后缀长度等于原来的基础上加一
3.当不等的时候
继续上图开整
错了错了,再来(皮一下就很开心)
首先我们是要比较这两位数(箭头连接的)是否相同
那如果不同的时候就可以理解为从后缀找到一个更短的与前缀相等
现在来回忆一下NEXT数组中存储的数据的含义,即在当前长度下的公共前后缀的长度
情况一如下
那么我们将前缀单独取出来考虑,一共有k+1个数,那么next(k)代表这k个数中的公共前后缀长度
我们知道公共前后缀长度相等数据相同,那么我们就可以发现下方绿色和红色区域完全相同
回到上面的数据(格子)绿色区域和红色也是完全相同,那么等价代换,最后两块的数据和开头的两块完全相同,这就是不断递归回退的理论基础。
数学表示与证明:
字符串表示为
p0p1p2p3p4.....p(n) n为任意大于1的数
其中 p0p1p2p3p4p5...p(k-1)与p(n-k+1)p(n-k+2).....p(n)是最大公共前后缀,长度为k
现在使得字符串多增加一位s
字符串变为
p0p2........pnS
那么只需要确认S是否等于pk
若等于
最长公共前后缀变为
p0p1p2p3...p(k-1)与p(n-k+1)p(n-k+2)....p(n)S即长度加一
若不等则
需要寻找一个更小的公共前后缀
即
p0....pm=p(n-m-1)....p(n)S
这个前后缀一定会满足如下要求
p0..p(m-1)与p(n-m)....p(n)相同
上述要求的存在性
现在我们考虑NEXT的定义
其中数据代表最大公共前后缀的长度
NEXT[k]即代表前k-1位最大公共前后缀的长度
对于p0...pk
公共前后缀长度即为NEXT[k]
即p0...p(NEXT[k]-1) 与p(k-NEXT[k]).....p(k-1)相同
又由于
p0p1p2p3p4p5...p(k-1)与p(n-k+1)p(n-k+2).....p(n)是最大公共前后缀
则
p(k-NEXT[k]).....p(k-1)与p(n-NEXT[k]+1)......p[n]相同
则p0p1....p(NEXT[k]-1)与p(n-NEXT[k]+1)......p[n]相同
关于这样是最大的证明
不妨设这样不是最大的
即存在一个L,L>NEXT[k]有
p0....p(L-1)=p(n-L+1)......p(n)
同时
p0....p(L-1)=p(k-L)....p(k-1)
那么前k位最大公共前后缀>=L
L>NEXT[k]
即NEXT[k]不是最大公共前后缀长度
与NEXT的定义矛盾
代码实现
void next(sqstring L,int nextf[])
{
nextf[1] = 0;
int i = 0;
int k = -1;
while (i < L->len)
{
if (k == -1 || L->a[i-1] == L->a[k-1])
{
k++;
i++;
nextf[i] = k;
}
else
{
k = nextf[k];
}
}
}
有了上面那么多介绍终于可以写出KMP算法
KMP算法实现
//KMP匹配算法本体
int KMP(sqstring T,sqstring L,int nextf[])
{
//T为主串,L为模式串
int i = 0;
int j = 0;
int check=0;
while (i < L->len&&j<T->len)
{
if (i==-1||L->a[i]==T->a[j])
{
i++;
j++;
}
else
{
i = nextf[i] ;
}
if (i >= L->len)
{
return j - L->len;
check = 1;
break;
}
}
if (check == 0)
{
return 0;
}
}
完整代码
其中辅助函数的定义还有串的定义可参考上一篇文章
#include<iostream>
#include<stdio.h>
using namespace std;
const int MAXSIZE = 255;
typedef struct sqString
{
char a[MAXSIZE];
int len;
}*sqstring, sqstr;
sqstring SQcrease()
{
sqstring L;
L = (sqstring)malloc(sizeof(sqstr)); //在分配空间的时候实际上已经分配了a[MAXSIZE]的空间
L->len = 0;
return L;
}
void StrAssign(sqstring T, char s[]) //转载自CSDN博主「自由不死」,从字符常量构建串(important)
{
T->len = 0;
for (; '\0' != s[T->len]; (T->len)++);
for (int i = 0; '\0' != s[i]; i++) //这里通过判断传入的参数的最后是否是一个空来判断是否结束(因为C/C++)使用'\0'作为数组结尾。
{
T->a[i] = s[i];
}
}
void Print(sqstring L) //打印串
{
cout << "串长:" << L->len << endl;
cout << "串: " << "'";
for (int i = 0; i < L->len; i++)
{
cout << L->a[i];
}
cout << "'";
cout << endl;
}
//基础匹配算法(不断的通过遍历来完成匹配)(寻找第一次出现的位置)
int Index(sqstring L,sqstring T)
{
int count=0;
for (int i = 0; i < L->len; i++)
{
count = i;
int check = 0; //作用在于判断之后的串是否匹配
if (T->a[0] == L->a[i]&&i<L->len-T->len) //确定第一位相同
{
check += 1;
for (int n = 1; n < T->len; n++) //依次匹配后面的每一位
{
if (T->a[n] == L->a[i + n])
{
check += 1;
}
else
{
break;
}
}
if (check == T->len)
{
break;
}
}
}
return count;
}
//KMP算法一种用空间换时间的算法
//NEXT数组的计算
void next(sqstring L,int nextf[])
{
int k=-1;
int i=0;
nextf[0] = -1;
while (i < L->len-1)
{
if (k == -1 || L->a[k] == L->a[i])
{
k++;
i++;
nextf[i] = k;
}
else k = nextf[k];
}
}
//KMP匹配算法本体
int KMP(sqstring T,sqstring L,int nextf[])
{
//T为主串,L为模式串
int i = 0;
int j = 0;
int check=0;
while (i < L->len&&j<T->len)
{
if (i==-1||L->a[i]==T->a[j])
{
i++;
j++;
}
else
{
i = nextf[i] ;
}
if (i >= L->len)
{
return j - L->len;
check = 1;
break;
}
}
if (check == 0)
{
return 0;
}
}
void prinn(int next[], int n)
{
for (int i = 0; i < n; i++)
{
cout << next[i] << ' ';
}
cout << endl;
}
int main()
{
/*
int index;
char ch1[] = "sdafdzgzgzdzgzdadzdz";
char ch2[] = "dzgz";
sqstring L = SQcrease();
sqstring T = SQcrease();
StrAssign(L, ch1);
StrAssign(T, ch2);
index = Index(L, T);
cout << index << endl;
*/
int nexte[9] = {};
char chr3[] = "abaabc";
sqstring M;
M = SQcrease();
M->len = 6;
StrAssign(M, chr3);
next(M, nexte);
prinn(nexte, 8);
char chr4[] = "acabaabaabcacaabc";
sqstring N;
N = SQcrease();
StrAssign(N, chr4);
N->len = 17;
int k;
k=KMP(N, M, nexte);
cout << k;
}