KMP算法
Posted yangj-blog
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了KMP算法相关的知识,希望对你有一定的参考价值。
首先看看kmp的出现原因:暴力匹配算法
假设现在我们面临这样一个问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置,怎么查找呢?
如果用暴力匹配的思路,并假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置,则有:
- 如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符;
- 如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。
- int ViolentMatch(char* s, char* p)
- {
- int sLen = strlen(s);
- int pLen = strlen(p);
- int i = 0;
- int j = 0;
- while (i < sLen && j < pLen)
- {
- if (s[i] == p[j])
- {
- //①如果当前字符匹配成功(即S[i] == P[j]),则i++,j++
- i++;
- j++;
- }
- else
- {
- //②如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0
- i = i - j + 1;
- j = 0;
- }
- }
- //匹配成功,返回模式串p在文本串s中的位置,否则返回-1
- if (j == pLen)
- return i - j;
- else
- return -1;
- }
举个例子,如果给定文本串S“BBC ABCDAB ABCDABCDABDE”,和模式串P“ABCDABD”,现在要拿模式串P去跟文本串S匹配,整个过程如下所示:
1. S[0]为B,P[0]为A,不匹配,执行第②条指令:“如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0”,S[1]跟P[0]匹配,相当于模式串要往右移动一位(i=1,j=0)
2. S[1]跟P[0]还是不匹配,继续执行第②条指令:“如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0”,S[2]跟P[0]匹配(i=2,j=0),从而模式串不断的向右移动一位(不断的执行“令i = i - (j - 1),j = 0”,i从2变到4,j一直为0)
3. 直到S[4]跟P[0]匹配成功(i=4,j=0),此时按照上面的暴力匹配算法的思路,转而执行第①条指令:“如果当前字符匹配成功(即S[i] == P[j]),则i++,j++”,可得S[i]为S[5],P[j]为P[1],即接下来S[5]跟P[1]匹配(i=5,j=1)
4. S[5]跟P[1]匹配成功,继续执行第①条指令:“如果当前字符匹配成功(即S[i] == P[j]),则i++,j++”,得到S[6]跟P[2]匹配(i=6,j=2),如此进行下去
5. 直到S[10]为空格字符,P[6]为字符D(i=10,j=6),因为不匹配,重新执行第②条指令:“如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0”,相当于S[5]跟P[0]匹配(i=5,j=0)
6. 至此,我们可以看到,如果按照暴力匹配算法的思路,尽管之前文本串和模式串已经分别匹配到了S[9]、P[5],但因为S[10]跟P[6]不匹配,所以文本串回溯到S[5],模式串回溯到P[0],从而让S[5]跟P[0]匹配。
而S[5]肯定跟P[0]失配。为什么呢?因为在之前第4步匹配中,我们已经得知S[5] = P[1] = B,而P[0] = A,即P[1] != P[0],故S[5]必定不等于P[0],所以回溯过去必然会导致失配。那有没有一种算法,让i 不往回退,只需要移动j 即可呢?
答案是肯定的。这种算法就是本文的主旨KMP算法,它利用之前已经部分匹配这个有效信息,保持i 不回溯,通过修改j 的位置,让模式串尽量地移动到有效的位置。
3. KMP算法
3.1 定义
- 假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置
- 如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符;
- 如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了j - next [j] 位。
- 换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值(next 数组的求解会在下文的3.3.3节中详细阐述),即移动的实际位数为:j - next[j],且此值大于等于1。
- int KmpSearch(char* s, char* p)
- {
- int i = 0;
- int j = 0;
- int sLen = strlen(s);
- int pLen = strlen(p);
- while (i < sLen && j < pLen)
- {
- //①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
- if (j == -1 || s[i] == p[j])
- {
- i++;
- j++;
- }
- else
- {
- //②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]
- //next[j]即为j所对应的next值
- j = next[j];
- }
- }
- if (j == pLen)
- return i - j;
- else
- return -1;
- }
向右移动4位后,S[10]跟P[2]继续匹配。为什么要向右移动4位呢,因为移动4位后,模式串中又有个“AB”可以继续跟S[8]S[9]对应着,从而不用让i 回溯。相当于在除去字符D的模式串子串中寻找相同的前缀和后缀,然后根据前缀后缀求出next 数组,最后基于next 数组进行匹配(不关心next 数组是怎么求来的,只想看匹配过程是咋样的,可直接跳到下文3.3.4节)。
即就是寻求最大公共元素,然后用模式串移动到那里,如最大公共在此之前是ab所以移到ab
kmp算法的精髓就在于next数组,从而达到跳跃式匹配的高效模式
如果是暴力的查找方法,当strText[i]和strKey[j]匹配失败的时候,i和j都要回退,然后从i-j的下一个字符开始重新匹配。
而KMP就是保证i永远不回退,只回退j来使得匹配效率有所提升。它用的方法就是利用strKey在失配的j为之前的成功匹配的子串的特征来寻找j应该回退的位置。而这个子串的特征就是前后缀的相同程度。
所以next数组其实就是查找strKey中每一位前面的子串的前后缀有多少位匹配,从而决定j失配时应该回退到哪个位置。
那么综上所述,我把kmp理解为
1.长的不动短的动
2.利用makeNext方法将next数组赋值实现字符串的跳跃到最大公共相同子串,
3.再不停匹配找到对应匹配
以上是关于KMP算法的主要内容,如果未能解决你的问题,请参考以下文章