想理一理KMP算法的学习思路,但是不会制作动图表达,暂时就用文字描述,尽量表达清晰吧。
注:看多了各种解说会发现,有些算法讲解和代码实现有出入,主要是因为针对的字符串存储方式不一样,要先明确串的值从0还是1单元开始存放。
模式匹配
给定主串s和子串t,则在s中找到t的过程称为模式匹配,其中t称为模式。如果在s中找到等于t的子串,则匹配成功,返回t在s中首次出现的存储位置,否则匹配失败,返回-1。
为了运算方便和保持思路清晰,设字符串的长度存放在0号单元,串的值从1号单元开始存放,这样字符序号和存储位置一致。
朴素匹配算法(暴力匹配)
基本思想:首先将s[1]和t[1]进行比较,若不同,则s[2]与t[1]比较(主串向后移动一位)…直到s中的s[i]与t[1]相同,再将它们之后的字符相比较,若下一个也相同,则如此继续往下比较,当s[i]与t[j]不相同时,则主串s回到本趟开始字符的下一个字符,即s[i-j+2],子串则返回到t[1],继续开始下一趟的比较,重复上述的过程。若t中的字符全部比完,则说明此趟匹配完成,起始位置是i-j+1,或者i-t[0],否则匹配失败。
例如s=“5abcba”, t=“2bc”,则返回2,含义是主串从第2位字符开始匹配出模式串。
此思想十分自然,根据此法,算法描述为:
int StrIndex_BF(char *s,char *t)
{
int i = 1,j = 1; //从主串s的第一个字符开始比较,s[0]和t[0]存储的是字符串长度
while(i <= s[0] && j <= t[0])
{
if(s[i] == t[j]) {i++;j++} //相等则均后移一位进行下一步比较
else {i = i-j+1; j=1;} //出现不匹配情况,则回溯,主串回到此趟开始位的下一个字符,子串回到第一个字符
}
if(j > t[0]) return(i - t[0]); //匹配完成,返回存储位置
else return -1; //否则返回-1
}
此算法的时间复杂度为(O(n+m) ~ O(n*m),其中n,m分别是两个串长度。
KMP算法
暴力匹配算法简单易理解,但是效率很低,在此基础上,发展生成了KMP算法,其中KMP名称是取自三个发现人的名字首字母。
原始算法的效率低在于非必要的回溯,在出现不匹配字符时,该字符之前的字符元素排序若出现重复(前后缀相同),则可以利用此性质,舍去重复部分的比较,直接滑到重复后一位比较,此时主串的比较位(指针)不回溯,保持不变,将模式串的比较位(指针)移动到重复部分的后一位,与主串的比较位开始进行下一轮比较。
具体描述:若s[i]与t[j]不相同,若此时模式串t前k-1个字符与t[j]之前的k-1个字符相同,则将模式串指针移到t[k],保持主串比较位不变,此时就是从该**第k个字符与s[i]**进行比较,以此类推。
此思想实现的重点就在于这个模式串指针移动到的位置,此时就引出了著名的next[j]数组,在t[j]上出现不匹配情况时,就将next[j]值对应的第next[j]个字符拿来进行下一轮比较。此处next[j]的获取后续再说,假设数组已知,在此基础上,代码实现算法为:
int StrIndex_KMP(char *s,char*t,int pos) //从主串s第pos个字符开始比较,在暴力匹配中pos=1
{
int i = pos,j =1,slen,tlen;
while(i <= s[0]&& j <= t[0])
{
if(s[i] == t[j]) {i++;j++}
else j = next[j]; //出现不匹配字符,则模式串比较位回溯
}
if(j > t[0]) return(i - t[0]); //匹配完模式串元素,则匹配成功,返回存储位置
else return -1; //否则匹配失败
}
next[]求解(关键)
next[]是关于模式串的指针移动位置的数组,只与模式串t的模式(字符元素)有关。
前后缀概念
是的,在此之前先了解一波(真)前后缀知识,一个字符串,前缀是首字符开始的不包含尾字符的子串,相应后缀为不包含首字符以尾字符结束的子串。
例如:字符串s=“abcab”,前缀有a,ab,abc,abca,后缀有bcab,cab,ab,b。
接下来就是提出前后缀重叠概念,对于一个字符串,若存在其某前缀与某后缀相同,则描述为前后缀重叠,重叠位数出现最多的前后缀,就是最大前后缀重叠,求解next[]就是发现每一个子字符串的最大前后缀重叠数。
在上诉例子中,前缀ab和后缀ab就出现了重叠,且唯一,最大重叠是2。
next[]求解:
实例说明,例如字符串t=“ababaaab”,以数组形似存储,T[0] =8, T = {8,a,b,a,b,a,a,a,b},其中T[0]即为上诉所说表示字符串长度,这样一来,下标和字符所在位数一致。
下标 j 0 1 2 3 4 5 6 7 8
T {8, a, b, a, b, a, a, a, b}
next[j] {\ 0 1 1 2 3 4 2 2}
令next[1] = 0, 意味着T[1] = a出现不匹配时,则将T[0]与主串当前位开始下一轮比较(后移一位)
next[2] = 1,意味着T[2] = b 出现不匹配时,则将T[1]与主串当前位开始下一轮比较(后移一位)
j = 3,T[3] = a 出现不匹配时,观察T的前2个元素组成的字符串的最大前后缀重叠,即“ab”的前后缀重叠,此时无重叠,则next[3] = 0+1 = 1;
j = 4,T[j] = b出现不匹配时,观察T的前3个元素组成的字符串的最大前后缀重叠,即“aba”的前后缀重叠,此时重叠前后缀为“a”,则next[4] = 1+1 = 2;
j = 5,T[j] = a出现不匹配时,观察T的前4个元素组成的字符串的最大前后缀重叠,即“abab”的前后缀重叠,此时重叠前后缀为“ab”,则next[5] = 2+1 = 3;
j = 6,T[j] = a出现不匹配时,观察T的前5个元素组成的字符串的最大前后缀重叠,即“ababa”的前后缀重叠,此时重叠前后缀为“aba”,则next[6] = 3+1 = 4;
j = 7,T[j] = a出现不匹配时,观察T的前6个元素组成的字符串的最大前后缀重叠,即“ababaa”的前后缀重叠,此时重叠前后缀为“a”,则next[7] = 1+1 = 2;
j = 8,T[j] = b出现不匹配时,观察T的前7个元素组成的字符串的最大前后缀重叠,即“ababaaa”的前后缀重叠,此时重叠前后缀为“a”,则next[7] = 1+1 = 2;
代码实现
void get_next(char *t, int * next)
{
int j = 0, i = 1; //前缀和后缀比较位
next[1] = 0;
while(i < t[0])
{
if(j == 0 || t[i] == t[j]) {i++;j++; next[i] = j;} //前后缀相同则继续下一位
else j = next[j];
}
}
优化:
在上诉求解过程中,计算前 j-1 个字符构成的字符串的前后缀重叠时,是没有考虑*t[j]**的,但是想想一下,如果最大相等前缀的后一个字符与现在不匹配的 t[j] 是相等的话,那我们按照前述方法移动,与主串当位置比较的元素也是没有变化的,即肯定也是不匹配的,则此次移动匹配非必须。
实例说明:
上诉 j = 5 ,T[j] = a出现不匹配时,观察T的前4个元素组成的字符串的最大前后缀重叠,即“abab”的前后缀重叠,此时重叠前后缀为“ab”,则next[5] = 2+1 = 3,即将模式串右移2位,但是此时相同前缀“ab” 的后一个字符a 等于此时的T[j],也就是说我们把模式串右移两位后,比较位并没有发生任何改变,肯定依旧不匹配,根据此思想,我们完全可以将T[j] 的值考虑进去,避免这种肯定无效的移动。
代码实现:
void get_next(char *t, int * next)
{
int j = 0, i = 1; //前缀和后缀比较位
next[1] = 0;
while(i < t[0])
{
if(j == 0 || t[i] == t[j])
{i++;j++;
if(t[i] == t[j]) next[i] = next[j]; //前缀后一位与当前t[j]相等,则跳转到前缀j对应地next位
else
next[i] = j; //前缀后一位与当前t[j]不相等,则按照原方法标记
}
else j = next[j];
}
}