文章目录
前言
KMP算法是我们数据结构串中最难也是最重要的算法。 难是因为KMP算法的代码很优美简洁干练,但里面包含着非常深的思维。真正理解代码的人可以说对KMP算法的了解已经相当深入了。而且这个算法的不少东西的确不容易讲懂,很多正规的书本把概念一摆出直接劝退无数人。开始我先对KMP算法的三位创始人Knuth,Morris,Pratt致敬,懂得这个算法的流程后你真的不得不佩服他们的聪明才智。
解决的问题类型
KMP算法的作用是在一个已知字符串中查找子串的位置,也叫做串的模式匹配 。比如主串s="university",子串t="sit"。现在我们要找到子串t 在主串s 中的位置。大家肯定觉得这还不简单,不就在第七个嘛,一眼就看出来了。
当然,在字符串非常少时,"肉眼观察法"不失为一个好方法。但如果要你在一千行文本里找一个单词,我想一般人都会数得崩溃吧。这就让我想起来考试的时候,如果一两道选择题不会。这时候,"肉眼观察法"可能效果不错,但是如果好几道大题不会呢?"肉眼观察法"就丝毫不起效了。所以打铁还需自身硬,我们把这种枯燥的事以一定的算法交给计算机处理。
第一种我们容易想到的就是暴力求解法。
这种方法也叫朴素的模式匹配:
简单来说就是:从主串s 和子串t 的第一个字符开始,将两字符串的字符一一比对,如果出现某个字符不匹配,主串回溯到第二个字符,子串回溯到第一个字符再进行一一比对。如果出现某个字符不匹配,主串回溯到第三个字符,子串回溯到第一个字符再进行一一比对...一直到子串字符全部匹配成功。
大家可能会想:这个方法也太慢了吧!求一个子串位置需要太多的步骤。而且很多步骤根本不必要进行。
这个想法非常好!!很多伟大的思想都是在一步步完善更正已有方法中诞生的。这种算法在最好情况下时间复杂度为O(n)。即子串的n个字符正好等于主串的前n个字符,而最坏的情况下时间复杂度为O(m*n)。相比而言这种算法空间复杂度为O(1),即不消耗空间而消耗时间。
下面就开始进入我们的正题:KMP算法是怎样优化这些步骤的。其实KMP的主要思想是:"空间换时间"。
大家打起精神,认真看下面的内容。
首先,为什么朴素的模式匹配这么慢呢?
你再回头看一遍就会发现,哦,原来是回溯的步骤太多了。所以我们应该尽量减少回溯的次数。
怎样做呢?比如上面第一个图:当字符'd'与'g'不匹配,我们保持主串的指向不变,
主串依然指向'd',而把子串进行回溯,让'd'与子串中'g'之前的字符再进行比对。
如果字符匹配,则主串和子串字符同时右移。
至于子串回溯到哪个字符,这个问题我们先放一放。
我先提出一个概念:一个字符串最长相等前缀和后缀。
教科书常用的手段是:在此处摆出一堆数学公式让大家自行理解。
这也是为什么看计算机学科的书没有较好的数学基础会很痛苦。
大家先不要强行理解数学公式,且听我慢慢道来:
我给大家个例子:
字符串 abcdab
前缀的集合:{a,ab,abc,abcd,abcda}
后缀的集合:{b,ab,dab,cdab,bcdab}
那么最长相等前后缀不就是ab嘛.
做个小练习吧:
字符串:abcabfabcab中最长相等前后缀是什么呢:
对就是abcab
好了我们现在会求一个字符串的前缀,后缀以及最长相等前后缀了。
这个概念很重要。到这里如果都看懂了,可以鼓励一下自己,然后回想一遍,再往下看。
之前留了一个问题,子串回溯到哪个字符,现在可以着手解决了。
KMP算法对朴素匹配算法进行了改进,利用匹配失败时失败之前的已知部分时匹配的 这个有效信息,保持主串的 i 指针不回溯 ,通过修改模式串(子串)的 j 指针,使模式串尽量地移动到有效的匹配位置 。该算法的时间复杂度为 O(n+m)
视频讲解
总体详解:
总代码
c
#include <stdio.h>
#include <stdlib.h>
#define size 300
typedef struct String
{
char *data;
int len;
}String;
String* initString()
{
String *s=(String *)malloc(sizeof(String));
s->data = NULL;
s->len = 0;
return s;
}
void stringAssign(String* stu)
{
char *temp=NULL;
int len=0;
temp =(char*)malloc(sizeof(char) * (size + 1));
stu->data =(char*)malloc(sizeof(char) * (size + 1));
gets(temp);
while(*temp)
{
stu->data[len] = *temp;
temp++;
len++;
}
if(!len)
{
free(temp);
temp = NULL;
free(stu->data);
stu->data=NULL;
}
stu->len=len;
}
void printString(String* s)
{
int i=0;
for(;i<s->len;i++)
printf(i==s->len-1?"%c":"%c->",s->data[i]);
printf("\r\n");
}
/*
*获取next指针
*/
int* getNext(String* s)
{
int *next=NULL; //防止野指针的出现
int i=1; //右移一位
int prefix_len=0; //next中当前公共前后缀的值
char *data=NULL; //防止野指针的出现
next = (int*)malloc(sizeof(int) * s->len); //为next开辟空间
next[0] = 0; //前缀的公共前后缀默认为0
data = s->data; //将指针指向传递值的数据
while( i < s->len)
{
if(data[prefix_len] == data[i]) //有前后缀的相同的情况
{
prefix_len++; //当前公共前后缀的值增加
next[i] = prefix_len; //赋值到next数组中
i++;
}
else
{
if(prefix_len == 0) //如果当前公共前后缀的值为0,则直接赋值
{
next[i] = prefix_len;
i++;
}
else
prefix_len = next[prefix_len-1]; //否则,则查表,可以类比左值等于右值,找最小公共端(这块建议看视频去理解)
}
}
return next;
}
void printNext(int* next, String* s)
{
int i=0;
for(;i<s->len;i++)
printf(i==s->len-1?"%d":"%d->",next[i]);
printf("\r\n");
}
void kmpMatch(String* master, String* sub)
{
int* next=NULL;
int i=0;
int j=0;
int num=0;
next=getNext(sub);
while(i<master->len && j<sub->len)
{
if(master->data[i] == sub->data[j])
{
j++;
i++;
}
else if(j)
j = next[j-1];
else
i++;
num++;
}
printNext(next,sub);
if(j==sub->len)
printf("对比成功,在%d行对齐,共对比了%d次",i-j,num);
else
printf("funtion fail");
}
int main(int argc, char* argv[])
{
String* s1 = initString();
String* s2 = initString();
stringAssign(s1);
stringAssign(s2);
printString(s1);
printString(s2);
kmpMatch(s1,s2);
return 0;
}
往期回顾
1.【第一章】《线性表与顺序表》
2.【第一章】《单链表》
3.【第一章】《单链表的介绍》
4.【第一章】《单链表的基本操作》
5.【第一章】《单链表循环》
6.【第一章】《双链表》
7.【第一章】《双链表循环》
8.【第二章】《栈》
9.【第二章】《队》
10.【第二章】《字符串暴力匹配》