串
说白了就是处理char数组的问题。
有一个子串,就是一个串的一部分。
涉及串的操作也不多,插入删除,查找子串。
存储结构的话,其实用链表也行,但是会很繁琐(链表可以考虑一个数据域存储多个字母,即使用char数组,记得用一个‘\0’即可)
模式匹配
什么叫模式匹配呢,就是在一个串中匹配给定子串的起点下标,匹配不成功返回-1。
回溯法
看名字就能猜到含义,就是匹配的过程中如果和子串出现不同,就要在将子串回溯到起点,主串回溯到第一个匹配成功的点的下一个。
举个例子
abcdefg
bce
第二个匹配成功了,直到主串的d和子串的e匹配失败了,此时主串要回溯到第一个匹配成功点的下一个,就是c,子串应该从头开始继续匹配。
int Brute_Force(char *a,char *b,int length_a,int length_b)//a主串,b子串
{
int i=j=0;
while(i<length_a && j<length_b)
{
if(a[i]==b[j])
{
i++;
j++;
}
else
{
i=i-j+1;//回溯
j=0;
}
}
if(j>length_b)//说明子串完全被匹配了
return j-length_b+1;
else
return -1;
}
简单粗暴,考虑一下平均时间复杂度
最好情况
每一次要么是第一位匹配失败,要么是直接成功,能成功的位置为0~length_a - length_b,假设失败的次数为i,则 i 的范围也应为0到length_a - length_b。
所以最好情况的时间复杂度:(ab分别为ab的长度简写)
o(a + b);
最坏情况
每一个点都是匹配到最后一个元素,发现自己被耍了,然后回溯,重新匹配,直到匹配成功。
所以匹配次数应该为(i+1) * length_b,i为匹配的趟数。
时间复杂度:
o(a * b);
可以看出来,在情况不如意的时候,该算法还是不太合理,所以一种改进的算法出现了。
kmp算法
之前的暴力匹配算法之所以能达到乘积的时间复杂度,主要原因就在回溯上,所有的情况都一概而论,直接回溯到最开始,但如果有一些情况并不需要回溯这么多,时间上就会省不少。
那应该回溯多少呢?接下来提出next函数(该函数对应的是j回溯的量,此时i不回溯)
- 子串的第一位是-1;//注意了,next函数是针对子串的
- 和子串第一个字母相同,下面两个条件满足一个,为-1;
取k∈[1.j),如果a[0]到a[k-1]和a[j-k]到a[j-1]没有一个k能使他们相同
或者有k保证其相同的,但是a[k]和a[j]相同 - 取k∈[1.j),如果a[0]到a[k-1]和a[j-k]到a[j-1]有k保证其相同且a[k] != a[j],为k
- 剩余情况为0
这么讲正常人都懵,那就来一些例子
先自己想一下,然后下拉看答案吧
这里面感觉bc属于第四条的可能是有一点不太好弄(反正我是翻车了)
下一个:
答案:
注意一下6,虽然a和a[0]一样,但是应该是用3来判断的。因为3不限制当前元素是否和a[0]一样。
下一个:
答案:
这里小心一下最后一个吧,因为a[3]既在前面的[0,k-1]又在[k-j,k-1]
next的应用
当你千辛万苦看到这里,并熟练的掌握了next函数,恭喜你,kmp算法小课堂开始了(狗头保命)
next函数在这样用的:(当前为a[i])
- next=-1,说明下一次a[i+1]和b[0]//本意是说明间接比较过a[i]和b[0]了
- next=0 ,说明下一次a[i]和b[0]
- next=k ,说明下一次a[i]和b[k]
这个没啥,毕竟求next的目的就是在于找出到底回溯多少是合适的。
代码:
int KMP(char* s,char* t,int next[])
{
int index=1,i=0,j=0;
if(!s||!t)
return -1;
else if(s[0]=='\0'||t[0]=='\0')
return -1;
while(s[i]!='\0'&&t[j]!='\0')
{
if(s[i]==t[j])//匹配成功
{
i++;
j++;
}
else
{
index+=j-next[j];//失配,回溯返回值,并按情况讨论
if(next[j]!=-1)//情况二、三在一起
j=next[j];
else//情况一
{
j=0;
i++;
}
}
}
if(t[j]=='\0')
return index;
else
return -1;
}
void Next(char* t,int next[])
{
int j=0,k=-1;
next[0]=-1;//第一条
while(t[j]!='\0')
{
if(k==-1||t[j]==t[k])
{
k++;
j++;
if(t[j]!=t[k])
next[j]=k;//第四条的直接失配,或者是第三条
else
next[j]=next[k];//第二条,和第四条中最后一位相同导致的失配
}
else
k=next[k];//倒退k
}
}
next函数代码和上述的思路还是有一点出入的,但是功能相同。
首先第一个为-1,此时k为-1,j为0,进入循环;
满足第一个if,k=0,j=1,且两个下标对应值不等,第四种情况为0;
满足第一个else,k回退为-1;
进入第一个if,同上一个,next[2]=0;
k再次回退为-1;
t[3]和首字母相等,next[3]=next[0]=-1;第二条
此时不需要回退,j、k加一后不等,符合第三条
next[0] = -1;
next[1&2]和上面相同,都是回退k为-1后进入内层的if语句,为0;
next[3]同上,在回退k为-1进入内层的else语句,取next[0]=-1;
next[4]因为是在最后一个位置相同了,也就是t[k]=t[j],符合第四条,进入内层的else语句。
next[5]同上;(但此时k没有发生回退而是一直加一)
next[6]也为内层的else语句,此时k为3;
next[7]时,因为最后一个元素不等,所以next函数取k值,为4,内层if语句。
通过这两个例子我们对这个函数的体会能更加深刻一些。
第二条的实现是在内部的else语句实现的,此时的k为0,next[0]=-1;
第三条的匹配成功且最后一个不同,是在内层的if语句实现的;
至于第四条,大致分两部分,一个是直接匹配失败(如next[1&2])和匹配成功但是最后一个元素相同,第一种是在内层if实现的,此时的k为0;第二种则是在内层else语句中实现的。
至于外层的else语句,则是实现了k的倒退功能,一般为-1(当然有不同的情况,多数是k为0,也就是第四条的直接失配)
kmp总结
kmp的算法中有一个求next步骤是之前没有的,是一个o(m),因为一般来说m<<n,所以可以不用心疼这么一点时间,而且还是将乘积的时间复杂度降为线性阶。
但是呢,因为我们之前得到的乘积阶时间复杂度是在最不理想的情况下,正常情况kmp是简单一些,但是不一定是绝对需要的,(毕竟还占了一个数组),而且kmp算法很麻烦(其实是麻烦的一批),所以在实际中也不是一定要用的,但是还是要会的。