前两个星期在数据结构上学习了KMP,一听说要学KMP就感觉好难,因为上学期在没学数据结构之前实验室有个学姐讲过KMP算法,听的真是云里雾里,不知道next数组到底是干啥的,下去自己也没有学习,所以在前两个星期学到KMP时,很认真的学,终于在老师快讲完这一章的时候听明白了,但感觉不是太熟练,当时也没有时间写博客整理一下思路,所以这就导致了这周一分享KMP时,思想一点就通,但是具体步骤还是理解的不是很好,这两天课多事多,下去仍旧没有细看,但是这周的专题有KMP,趁星期五,刚才花了两个小时彻底弄懂了它,原来只要静下心来学习,还是能理解的嘛,哈哈哈。借用了很多大神的讲解,添加一些自己的想法,好好的整理一下KMP算法的思想和实现过程。
设有主串s和子串t,子串t的定位就是要在主串s中找到一个与子串t相等的子串。通常把主串s称为目标串,把子串t称为模式串,因此定位也称作模式匹配。
模式匹配成功是指在目标串s中找到一个模式串t;不成功则指目标串s中不存在模式串t。
模式匹配的应用非常广泛。例如,在文本编辑程序中,我们经常要查找某一特定词
在文本中出现的位置。显然,解此问题的有效算法能极大地提高文本编辑程序的响应
性能。模式匹配是一个较为复杂的串操作过程。迄今为止,人们对串的模式匹配提出了
许多思想和效率各不相同的计算算法。在分析KMP算法之前,我们先来了解一下BF算
法。(这样更能衬托出KMP的高效,哈哈)
一:Brute-Force算法
Brute-Force简称为BF算法,亦称简单匹配算法,其基本思路是:
从目标串s=“s0s1…sn-1”的第一个字符开始和模式串t=“t0t1…tm-1”中的第一个字符比较,若相等,则继续逐个比较后续字符;否则从目标串s的第二个字符开始重新与模式串t的第一个字符进行比较。依次类推,若从模式串s的第i个字符开始,每个字符依次和目标串t中的对应字符相等,则匹配成功,该算法返回i(i是目标串中与模式串第一个字符匹配成功的位置);否则,匹配失败,函数返回-1。
例如,设目标串s=“cddcdc”,模式串t=“cdc”。s的长度为n(n=6),t的长度为m(m=3)。用指针i指示目标串s的当前比较字符位置,用指针j指示模式串t的当前比较字符位置。BF模式匹配过程如下所示。
第一次匹配:s=c d d c d c i=2 失败
t =c d cj=2
第二次匹配:s=c d d c d c i=1 失败
t = c d cj=0
第三次匹配: s= c d d c d c i=2 失败t = c d c j=0
第四次匹配: s= c d d c d c i=5 成功t = c d c j=2
int index(SqString s,SqString t)
{ int i=0,j=0,k;
while (i=t.length)
k=i-t.length; //返回匹配的第一个字符的下标
else
k=-1; //模式匹配不成功
}
这个算法简单,易于理解,但效率不高,主要原因是:主串指针i在若干个字符序列比较相等后,若有一个字符比较不相等,仍需回溯(即i=i-j+1)。
该算法在最好情况下的时间复杂度为O(m),即主串的前m个字符正好等于模式串的m个字符。在最坏情况下的时间复杂度为O(n*m)。
理解该算法的关键点是:
当第一次si≠tj时:主串要退回(即回溯)到i-j+1的位置,而模式串也要退回到第一个字符(即j=0的位置)。
但是,我们知道当比较出现si≠tj时:应该有si-1=tj-1,…,si-j+1=t1, si-j=t0 ,即已经出现有“部分匹配”的结果。
我们能不能利用这些部分匹配信息,消除主串的回溯以提高算法的效率呢?
例:设有串s=“abacabab” ,t=“abab” 。则第一次匹配过程如下例所示。
s=“a b a c a b a b” i=3
|| || || ≠ 匹配失败
t=“a b a b” j=3
在i=3和j=3时,匹配失败。但重新开始第二次匹配时,不必从i=1 ,j=0开始。因为s1=t1,t0≠t1,必有s1≠t0,又因为s2=t2, t0=t2,所以必有s2=t0。由此可知,第二次匹配可以直接从i = 3 、j = 1开始。下面我们就开始介绍KMP算法啦,注意啦,注意啦!!!
二:KMP算法
KMP算法是D.E.Knuth、J.H.Morris和V.R.Pratt共同提出的,简称KMP算法。该算法较BF算法有较大改进,主要是消除了主串指针的回溯,使算法效率有了某种程度的提高。
每当一趟匹配过程出现字符不相等时,主串指示器不用回溯,而是利用已经得到的“部分匹配”结果,将模式串的指示器向右“滑动”尽可能远的一段距离后,继续进行比较。
例如,设目标串s=“BBC ABCDAB ABCDABCDABDE”,模式串t=“ABCDABD”。我们想知道目标串里是否包含有模式串。
1.
首先,字符串"BBC ABCDAB ABCDABCDABDE"的
第一个字符与搜索词"ABCDABD"的第一个字符,进
行比较。因为B与A不匹配,所以搜索词后移一位
2.
因为B与A不匹配,搜索词再往后移。
3.
就这样,直到字符串有一个字符,与搜索词的第一个
字符相同为止。
4.
接着比较字符串和搜索词的下一个字符,还是相同。
5.
直到字符串有一个字符,与搜索词对应的字符不相同
为止。
6.
这时,最自然的反应是,将搜索词整个后移一位,再
从头逐个比较。这样做虽然可行,但是效率很差,因
为你要把"搜索位置"移到已经比较过的位置,重比一
遍。
7.
一个基本事实是,当空格与D不匹配时,你其实知道
前面六个字符是"ABCDAB"。KMP算法的想法是,设
法利用这个已知信息,不要把"搜索位置"移回已经比
较过的位置,继续把它向后移,这样就提高了效率。
8.
9.
已知空格与D不匹配时,前面六个字符"ABCDAB"是
匹配的。查表可知,最后一个匹配字符B对应的"部分
匹配值"为2,因此按照下面的公式算出向后移动的位
数:
移动位数 = 已匹配的字符数 - 对应的部分匹配值
因为 6 - 2 等于4,所以将搜索词向后移动4位。
10.
因为空格与C不匹配,搜索词还要继续往后移。这
时,已匹配的字符数为2("AB"),对应的"部分匹配
值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将
搜索词向后移2位。
11.
因为空格与A不匹配,继续后移一位。
12.
逐位比较,直到发现C与D不匹配。于是,移动位数
= 6 - 2,继续将搜索词向后移动4位。
13.
逐位比较,直到搜索词的最后一位,发现完全匹配,
于是搜索完成。如果还要继续搜索(即找出全部匹
配),移动位数 = 7 - 0,再将搜索词向后移动7位,
这里就不再重复了。
14.
"部分匹配值"就是"前缀"和"后缀"的最长的共有元素
的长度。以"ABCDABD"为例,
- "A"的前缀和后缀都为空集,共有元素的长度为0;
- "AB"的前缀为[A],后缀为[B],共有元素的长度为0;
- "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
- "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
- "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;
- "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;
- "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。
15.
"部分匹配"的实质是,有时候,字符串头部和尾部会
有重复。比如,"ABCDAB"之中有两个"AB",那么它
的"部分匹配值"就是2("AB"的长度)。搜索词移动
的时候,第一个"AB"向后移动4位(字符串长度-部分
匹配值),就可以来到第二个"AB"的位置。
经过上面的讲解,相信大家都应该很明白了吧,这是我借鉴大牛写的,链接:点击打开链接,感觉他写的很容易明白,那么我们来看代码实现部分吧!
/**这里先要说清楚几点,第一点就是next[i]是用来存前i个字符的相同的最大前缀和最大后缀的长
第二点就是我是以-1开头的,当next数组为-1的时候,说明没有匹配的,为0的时候有一个匹配的等等,
因为我每次的判断就是b[k+1]来判断的,第三点就是这个变量k,它是用来回溯用的。**/
void get_next(char b[],int blen,char next[])/// 求出模式串的next数组
///传模式串和模式串的长度,以及next数组
{
next[0] = -1;///第一个因为没有比较,我们初始他为-1
int k = -1;
///初始一个变量为-1,当字符串匹配到某一个不相等的时候,该变量用来记录next数组值,往前回溯
for (int i = 1; i < blen; i++)
{
while (k > -1 && b[k + 1] != b[i])///如果匹配到某一个不相等的时候,开始回溯
{
k = next[k];///回到next数组中记录的位置重新匹配
}
if (b[k + 1] == b[i])///如果字符一样,继续向前匹配
{
k = k + 1;
}
next[i] = k;///将最大匹配的值赋给next数组
}
}
int KMP(char a[],char b[],int alen,int blen)///KMP算法
///传入的参数是主串a,模式串b,主串a的长度,模式串b的长度
{
get_next();///先的到模式串的next数组,上面已经实现过
int k = -1;///k依然从-1开始
for (int i = 0; i < n; i++)///将主串每个字符遍历一遍
{
while (k >-1&& b[k + 1] != a[i])///匹配到某一字符,突然不匹配了
{
k = next[k];///回溯
}
if (b[k + 1] == a[i])///如果匹配,继续向前
k = k + 1;
if (k == m-1)///如果匹配完模式串,则返回模式串在主串中匹配的初始位置
{
return (i-m+1);
}
}
return -1;///如果没有在主串中找到与模式串匹配的,则返回-1
}