KMP算法是什么?
作为一个学习计算机或者从事计算机工作的人来说,数据结构与算法几乎是我们必须要了解甚至精通的学科,而学数据结构与算法时总有几个难点,KMP算法就是其中一个,这里我按照我的思路给大家详细讲一下。
KMP算法是一个性能优秀的字符串模式匹配算法,这里就有一点铺垫的概念要解释一下了:
在一个字符串中搜索一个子串的位置,最简单的算法就是暴力匹配算法(Brute-Force,BF算法),暴力匹配算法的原理比较简单,就是循环把字符串和模式串进行逐一比较,不过暴力匹配算法的效率十分低下,所以就出现了效率更高的KMP算法,这里先给大家解释一下暴力匹配算法。
附注:暴力匹配算法有很多种实现方式,不过原理都是一样的,许多书本和网上的文章对于暴力匹配算法的解释是在同一个循环中用i和j分别代表长短两个字符串的匹配的位置,当字符失配时,i回退,j归零,这里讲的可能有点抽象,总之你们要知道暴力匹配算法有时候实现方式不同但是原理是一样的就好了,这里给出的是我认为的比较好理解的实现方式,不过这个如果你们懂了的话应该看别的实现方式也是看得懂的。
因为暴力匹配算法比较简单,这里我就直接上代码了,后面再解释:
#include
//获取字符串长度函数
int length(char *s){
int i=0;
if(s!=NULL) while(s[i]) i++;
return i;
}
//暴力匹配算法 Brute-Force简称BF算法
int indexOf(char *lstr,char *str){
int i,j,m,n;
//1.获取两个字符串的长度
m = length(lstr);
n = length(str);
if(lstr==NULL||str==NULL||m==0||n==0) return -1;//两个字符串都不能为NULL
//2.如果字符串短于模式串则不用进行搜索,直接return -1;
if(m>=n){
for(i=0;i<=m-n;i++){//外循环从0到m-n
for(j=0;j
暴力匹配算法的执行过程是这样的:
假设在字符串”abdabc”中搜索子串”abc”,用m和n分别代表字符串的长度和子串的长度,这里的m=6,n=3;我们把外循环临时变量i的值从0循环到m-n,每一轮循环都把长字符串中编号为i到i+n-1的n个字符与短字符串中的n个字符进行比较。(内循环的逻辑比较简单就不解释了)。
下面模拟一下循环过程:
暴力匹配算法之所以效率低下,主要是因为循环过程中进行了很多不必要的匹配,例如:
下面的一个例子,在一个字符串中搜索子串ABCDABD。当有一处外循环i的值能够与子串的前六个都匹配,但唯独在子串的最后一个位D不匹配,那么我们在外循环的下一次移位时,就没有必要再一位一位的移,而是可以利用已经匹配的字符串信息计算出移位数,例如下面这样,在最后一位D位失配之后我们可以直接把比较位置挪成这样:
(下面这张图片来自另外一篇博客kornberg_fresnel KMP算法到底在干什么)
上面的解释看不懂就算了,随便看看就好了,总是你们要知道,KMP算法就是在字符串的某一位失配时,根据失配的字符前的已匹配信息来计算出下一轮比较的开始位置,从而避免不必要的匹配,提高算法效率。(例如在短字符串的最后一位D失配时,那么说明前面的”ABCDAB”都是完全匹配了的,根据这个信息,计算出下一轮比较的位置),具体怎么做呢,下面我们就来看看。
之前我们说过,KMP算法就在在字符串的某一位失配时,根据失配的字符前的已匹配信息计算出下一轮比较的位置。所以KMP算法的核心就在于对已匹配信息的分析利用和失配之后的移位操作。想要计算移位数首先我们得学习几个新概念。
部分匹配表即一个字符串中每一个位对应的部分匹配值的集合,例如下面是一个字符串对应的PMT表,大家先大概看一下:
字符(char) | a | b | c | d | a | b | c |
---|---|---|---|---|---|---|---|
下标(index) | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
部分匹配值(value) | 0 | 0 | 0 | 0 | 1 | 2 | 3 |
那么这些部分匹配值是怎么来的呢?
概念:部分匹配值是一个字符串中前缀集合和后缀集合交集元素中最长的元素的长度。
铺垫知识:字符串的前缀和后缀
定义:把一个字符串分割成非空的两个部分,前面的就叫前缀,后面的就叫后缀。如:字符串”abc”,划分为非空的两个部分可以是前缀”a”和后缀”bc”,也可以是前缀”ab”和后缀”c”。那么前缀集合就是{“a”,”ab”},后缀集合就是{”bc”,”c”}。
上面这个例子中,字符串”abc”的前缀集合和后缀集合没有交集,所以字符串”abc”的部分匹配值就是0。
所以,部分匹配表就是把字符串中的每一位和它前面的字符当成一个新的字符串然后计算出部分匹配值。
例如上面的字符串”abcdabc”的第三个字符c的部分匹配值就是子串”abc”的部分匹配值,也就是0,所以得到字符串中下标为2的部分匹配值为0。
同理,”abcdabc”中第5位a的部分匹配值就是子串”abcda”的部分匹配值,从而下标为4的部分匹配值就是1。
KMP算法核心就是对部分匹配表的使用,之前我们说过,KMP算法就是在字符失配时根据已匹配的信息计算出下一轮比较的位置。那么这个已匹配的信息和下一轮比较的位置具体都是啥呢?我们就来看看。
例如一个长字符串(这里先不管它是啥)中搜索一个短字符串”ABCDABD”,当匹配过程在短字符串的第6位B处失配时,那么已匹配的信息就是前面的5个字符,也就是说”ABCDA”这五个字符是已经被匹配成功的。如图:
KMP算法中,移位数的计算是和部分匹配表是相关的,字符失配是移位操作具体可以分为:
KMP算法的代码实现主要分为两个部分,即:
我们已经知道了,移位数组next的值就是字符位前的所有字符组成的子串的对应的部分匹配值,而部分匹配值就是前缀集合和后缀集合交集中最长元素的长度,那么具体要怎么算呢?有一个笨办法就是循环遍历这个字符串,每一轮都去计算字符位前所有字符的子串的前缀集合和后缀集合交集的最长元素,代码实现如下:
//这是一个笨办法,只是为了简单的实现这个算法逻辑,算法效率非常低
#include
#include
//铺垫函数 求字符串长度
int length(char *s){
int i=0;
if(s!=NULL) while(s[i]) i++;
return i;
}
//铺垫函数 求一个字符串的前缀和后缀交集元素的长度
int nextNum(char *str,int start,int l){
int len = length(str),i,j;
if(l<=0||start<0||start>=len) return -1;//不存在时返回-1
for(i=start+l-2;i>start;i--){
for(j=start;j
上面这个的算法逻辑是比较简单的,我把上面代码中的nextNum函数稍微讲解一下:
nextNum函数是求解一个字符串前后缀最长交集元素的长度(部分匹配值)的函数。nextNum函数原型是int nextNum(char *str,int start,int l);
即传入一个字符串str,把start到start+l(不含start+l)位上的字符当成一个新字符串来求解部分匹配值。算法的步骤是:
前面我们已经了解了next数组求解的一个笨办法,但是写出来的代码效率非常低的,我们进行了很多重复的比较。例如在求一个字符串”abab”的next数组时,上面getNext函数的操作是:
#include
#include
//铺垫函数 求字符串长度
int length(char *s){
int i=0;
if(s!=NULL) while(s[i]) i++;
return i;
}
int *getNext(char * str){
int len,*next;
len = length(str);
if(len<=0) return NULL;
next = (int *)malloc(sizeof(int)*len);
next[0] = -1;
int i = 0, j = -1;
while (i < len)
{
if (j == -1 || str[i] == str[j])
{
++i;
++j;
next[i] = j;
}
else
j = next[j];
}
return next;
}
int main(){
int *a,i;
a = getNext("ababada");
for(i=0;i<7;i++) printf("%d ",a[i]);
return 0;
}
上面这个东西逻辑还是比较复杂的,但是算法效率很高,大家尽量看一看。
算法解析,以字符串str=”ababca”为例:
这里我就直接上代码啦,如果你们前面的都看懂了的话那么看这个应该也是没问题的。
#include
#include
//铺垫函数 求字符串长度
int length(char *s){
int i=0;
if(s!=NULL) while(s[i]) i++;
return i;
}
int *getNext(char * str){
int len,*next;
len = length(str);
if(len<=0) return NULL;
next = (int *)malloc(sizeof(int)*len);
next[0] = -1;
int i = 0, j = -1;
while (i < len)
{
if (j == -1 || str[i] == str[j])
{
++i;
++j;
next[i] = j;
}
else
j = next[j];
}
return next;
}
//KMP算法
int indexOf(char *lstr,char *str){
int i,j,m,n,*next;
//1.获取两个字符串的长度
m = length(lstr);
n = length(str);
if(lstr==NULL||str==NULL||m==0||n==0) return -1;//两个字符串都不能为NULL
//2.求next数组
next = getNext(str);
//3.如果字符串短于模式串则不用进行搜索,直接return -1;
if(m>=n){
i=0,j=0;
while(i
暴力匹配算法求解思路:只用一个循环来处理比较的过程,用变量i存储长字符串的下标,用变量j存储短字符串的下标,然后进行比较,当字符位匹配时则进行下一个位的匹配直到短字符串尾,当发生失配情况时,变量j归0,变量i回退j-1个位置。代码如下:
#include
//获取字符串长度函数
int length(char *s){
int i=0;
if(s!=NULL) while(s[i]) i++;
return i;
}
//暴力匹配算法 Brute-Force简称BF算法
int indexOf(char *lstr,char *str){
int i,j,m,n;
//1.获取两个字符串的长度
m = length(lstr);
n = length(str);
if(lstr==NULL||str==NULL||m==0||n==0) return -1;//两个字符串都不能为NULL
//2.如果字符串短于模式串则不用进行搜索,直接return -1;
if(m>=n){
i=0,j=0;
while(i
KMP算法实现思路:KMP算法是基于暴力匹配算法的改良,所以这一版的kmp算法就是基于上面的暴力匹配算法做了一些改动,即当发生失配情况时,保持变量i不变,而j回退到next[j]的位置,代码如下:
#include
#include
//铺垫函数 求字符串长度
int length(char *s){
int i=0;
if(s!=NULL) while(s[i]) i++;
return i;
}
int *getNext(char * str){
int len,*next;
len = length(str);
if(len<=0) return NULL;
next = (int *)malloc(sizeof(int)*len);
next[0] = -1;
int i = 0, j = -1;
while (i < len)
{
if (j == -1 || str[i] == str[j])
{
++i;
++j;
next[i] = j;
}
else
j = next[j];
}
return next;
}
//KMP算法
int indexOf(char *lstr,char *str){
int i,j,m,n,*next;
//1.获取两个字符串的长度
m = length(lstr);
n = length(str);
if(lstr==NULL||str==NULL||m==0||n==0) return -1;//两个字符串都不能为NULL
//2.求next数组
next = getNext(str);
//3.如果字符串短于模式串则不用进行搜索,直接return -1;
if(m>=n){
for(i=0,j=0;i
至此,关于KMP算法,我的所学所思已经讲解的差不多啦,今天就写到这里吧,本文作者郑伟斌,写于2019/4/11,转载注明出处。