算法简介
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。
算法原理
现在先给两个字符串,我们的目的就是要寻找模式串与主串中所有字符都匹配的位置,并返回模式串第一个元素的下标。
第一轮:模式串和主串的第一个等长子串比较,发现第0位字符一致,第1位字符一致,第2位字符不一致
我们先介绍一下最简单的匹配方法
1.BF算法
BF算法就是使用简单粗暴的方式,对主串和模式串进行逐个字符的比较
第二轮:模式串向后挪动一位,和主串的第二个等长子串比较,发现第0位字符不一致
第三轮:模式串继续向后挪动一位,和主串的第三个等长子串比较,发现第0位字符不一致
以此类推,直到第N轮
第N轮:此时我们可以发现模式串的所有字符都得到了匹配
以上就是BF算法的原理,我们可以发现,BF算法效率实在太低了,每一轮只能老老实实地把模式串右移一位,实际上做了很多无谓的比较。
2.KMP算法
KMP算法和BF算法有些是共通的,同样是把主串和模式串的首位对齐,从左到右对逐个字符进行比较。
现在先给两个字符串
第一轮:模式串和主串的第一个等长子串比较,发现前5个字符都是匹配的,第6个字符不匹配,是一个“坏字符”。
我们可以发现,在已经匹配的字符串中,前缀“GTGTG”的后三个字符“GTG”和前三位字符“GTG”是相同的。
在下一轮的比较时,只有把这两个相同的片段对齐,才有可能出现匹配。这两个字符串片段,分别叫做最长可匹配后缀子串和最长可匹配前缀子串
第二轮:我们直接把模式串向后移动两位,让两个“GTG”对齐,继续从刚才主串的坏字符A开始进行比较
显然,主串的字符A仍然是坏字符,这时候的匹配前缀缩短成了GTG:
按照第一轮的思路,我们来重新确定最长可匹配后缀子串和最长可匹配前缀子串:
第三轮:我们再次把模式串向后移动两位,让两个“G”对齐,继续从刚才主串的坏字符A开始进行比较
重复操作,直到匹配结束。
KMP算法的整体思路:在已匹配的前缀当中寻找到最长可匹配后缀子串和最长可匹配前缀子串,在下一轮直接把两者对齐,从而实现模式串的快速移动。
3.移动原理(next数组)
先介绍一下字符串前缀后缀的概念
移动大小
next数组计算方法:
next数组计算方法
实现代码
完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void GetNext(char* p, int next[])
{
int pLen = strlen(p);
next[0] = -1;
int k = -1;
int j = 0;
while (j < pLen - 1)
{
//p[k]表示前缀,p[j]表示后缀
if (k == -1 || p[j] == p[k])
{
++k;
++j;
next[j] = k;
}
else
{
k = next[k];
}
}
}
int KmpSearch(char* s, char* p)
{
int i = 0; //主串中字符的移动标识i
int j = 0; //子串中字符的移动标识j
int sLen = strlen(s);//主串字符个数
int pLen = strlen(p);//子串字符个数
int next[255]; //next数组,记录子串的移动规则
GetNext(p, next);
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;//没找到返回-1
}
int main()
{
//子串或模式串
char p[20] = "ABCDABD";
//主串
char s[30] = "BBC ABCDAB ABCDABCDABDE";//23个字符
printf("%d\n", KmpSearch(s, p));//15
system("pause");
return 0;
}