引言
KMP算法指的是字符串模式匹配算法,问题是:在主串T中找到第一次出现完整子串P时的起始位置。该算法是三位大牛:D.E.Knuth、J.H.Morris和V.R.Pratt同时发现的,以其名字首字母命名。在网上看了不少对KMP算法的解析,大多写的不甚明了。直到我看到一篇博客的介绍,看完基本了解脉络,本文主要是在其基础上,在自己较难理解的地方进行补充修改而成。该博客地址为:https://www.cnblogs.com/yjiyjige/p/3263858.html,对作者的明晰的解析表示感谢。
1. 一般的解法
KMP算法要解决的问题就是在字符串(也叫主串)中的模式(pattern)定位问题。说简单点就是我们平时常说的关键字搜索。模式串就是关键字(接下来称它为P),如果它在一个主串(接下来称为T)中出现,就返回它的具体位置,否则返回-1(常用手段)。
首先,对于这个问题有一个很直接的想法:从左到右一个个匹配,如果这个过程中有某个字符不匹配,就跳回去,将模式串向右移动一位。这有什么难的?
我们可以这样初始化:
之后我们只需要比较i指针指向的字符和j指针指向的字符是否一致。如果一致就都向后移动,如果不一致,如下图:
A和E不相等,那就把i指针移回第1位(假设下标从0开始)(即i回溯),j移动到模式串的第0位,然后又重新开始这个步骤:
基于这个想法我们可以得到以下的程序:
1 /**
2
3 * 暴力破解法
4
5 * @param ts 主串
6
7 * @param ps 模式串
8
9 * @return 如果找到,返回在主串中第一个字符出现的下标,否则为-1
10
11 */
12
13 public static int bf(String ts, String ps) {
14
15 char[] t = ts.toCharArray();
16
17 char[] p = ps.toCharArray();
18
19 int i = 0; // 主串的位置
20
21 int j = 0; // 模式串的位置
22
23 while (i < t.length && j < p.length) {
24
25 if (t[i] == p[j]) { // 当两个字符相同,就比较下一个
26
27 i++;
28
29 j++;
30
31 } else {
32
33 i = i+1 - (j-0); // 一旦不匹配,i后退,j-0(因为下标从0开始,j-0是相等时匹配的长度,i+1说明主串指针i向后挪1位)
34
35 j = 0; // j归0
36
37 }
38
39 }
40
41 if (j == p.length) {
42
43 return i - j;
44
45 } else {
46
47 return -1;
48
49 }
50
51 }
int strindex(char s[],int slen,char t[],int tlen){
for(int i=0;i<slen;i++){
for(int j=i,k=0;s[j]==t[k]&&t[k]!='\0';j++,k++);
if(k==tlen)return i;
}
return -1;
}//徐宝文c语言书,第4章59页,字符存储从0号单元开始
引申下,修改徐宝文的程序,来统计总共比较多少次,代码如下:
int strindex002(char s[],int slen,char t[],int tlen){//right====
int count1=0;int count2=0;
for(int i=0;i<slen;i++)
{
for(int j=i,k=0;k<tlen;j++,k++)
{
if(s[j]==t[k]&&t[k]!='\0')
{
count1++;//相等时
continue;
}
count2++;//不等时比较次数
break;//结束内层循环
}
if(t[k]=='\0')break;//首次匹配成功,结束外层循环
}
return count1+count2;
// return -1;
}
//该函数用于统计主串和模式串的比较次数
//主串S:A STRING SEARCHING EXAMPLE CONSISTING OF SIMPLE TEXT
//模式串T:STING 比较41次
//用了两个计数器,统计相等时比较次数和不等时比较次数
// 字符串匹配bf算法之字符比较次数统计.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include<string.h>
#include<stdio.h>
int strindex001(char s[],int slen,char t[],int tlen){
int count=0;
int flag=0;//设立标志位
for(int i=0;i<slen;i++)
{
int k=0;//k表示第i趟比较的次数
for(int j=i;k<tlen;j++,k++)//这个地方小心
{
if(s[j]==t[k]&&t[k]!='\0')//if(s[j]==t[k]&&t[k]!='\0'),其实t[k]!='\0'可以不要因为 for(int j=i;k<tlen;j++,k++)中k<tlen
{
continue;//相等继续往后比较
}
else
break;//不等结束循环
/* printf("i=%d k=%d \n",i,k);//test
if((++k)=tlen)
{
count+=k;
}else
{
count+=(k+1);//为第i趟字符比较的次数
}
break;*///猪脑子吗?第i趟字符比较的次数,i控制外层循环,应该放在外层循环中
}
if(t[k]=='\0')//匹配成功
{
count+=tlen;
break;
}else{
count+=k;
}
}
return count;
}
int strindex002(char s[],int slen,char t[],int tlen){//right====
int count=0;int pos=0;int count1=0;int count2=0;
for(int i=0;i<slen;i++)
{
for(int j=i,k=0;k<tlen;j++,k++)
{
if(s[j]==t[k]&&t[k]!='\0')
{
count1++;
continue;
}
count2++;//跟踪下,没有执行count2++,特例:主串和模式串想同
break;
}
if(t[k]=='\0')break;
}
printf("count1=%d count2=%d\n",count1,count2);//test
return count1+count2;
}
int main(int argc, char* argv[])
{
char s[]="a string searching example consisting of";
// char s[]="sting";
int slen=strlen(s);
// char t[]="yyds";//test
char t[]="sting";
int tlen=strlen(t);
printf("%d %d\n",slen,tlen);
int index=strindex002(s,slen,t,tlen);
printf("index=%d\n",index);
return 0;
}
2. 严蔚敏《数据结构》的解法
数据结构的选择:
//串长的定长顺序存储表示
#define MAXSTRLEN 255//用户可以在255以内定义最大串长
typedef unsigned char SString[MAXSTRLEN+1];//0号单元存放串的长度
0号单元存放字符串的长度,存储数据从1号单元开始(这是与上面解法,不一样的地方)
//返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数值返回0.因为字符串0号单元存储字符串的
//长度,而没有存放有意义的数据,其中T非空,1<=pos<=主串S的长度
int index(SString S,SString T,int pos)
{
int i=pos,j=1;
while(i<=S[0]&&j<=T[0])
{
if(S[i]==T[j]){++i,++j;}//相等继续比较后续字符
else
{
i=i+1-(j-1);j=1;
//指针后退重新开始匹配,tips:T字符串0号单元存储长度,所以比较从1号单元开始,所以j=1
//j-1为主串和子串匹配的长度,可以与上面的代码进行比较,i+1说明往后挪动1位
}
}
if(j>T[0])return i-T[0];//匹配成功
else return 0;//匹配失败
}
上述算法分别利用计数指针i和j指示主串s和模式串T中当前正待比较的字符位置。
算法的基本思想:
这里数组内容从1号单元存储
上面图片中举了几个例子:
(1)主串:A STRING SEARCHING EXAMPLE CONSISTING OF SIMPLE TEXT
模式串:STING
关于比较次数如何计算在第一大部分给出结尾给出了统计比较次数的函数
(2)主串:0000 0001
模式串:0001
第一次比较主串:0000 0001
0001
比较到最后1个字符不等,i=4,j=4
第二次比较主串:0000 0001
0001
i指针回溯到2,j变成1
分析:0001部分匹配000这3个0,i又往后挪动1位,所以:i+1-部分匹配的3位,回溯到i-2
这和咱们图片的例子相似,只不过举得例子简单,,,更容易理解
引申:遇到复杂的例子,咱们举简单的例子充分分析后,更容易弄懂复杂的问题
主串: 0000 0001
模式串:0001
第1次指针i不需要回溯
最后1次:
主串: 0000 0001
模式串: 0001
回溯4次(8-4)
但是循环从第一次比较就开始(第1次+回溯的次数),循环(4+1)*m,m是模式串的长度
这时候是最坏的情况时间复杂度为O(n*m),n为主串长度,m为模式串长度
通过这个例子,主串中可能存在多个和模式串“部分匹配”的子串,导致指针i多次回溯
那么有没有什么方法,i不用回溯
于是,就有了KMP算法
(3)今天的反思
依旧对循环理解不透彻,
在循环条件上,下面贴出错误代码
int strindex001(char s[],int slen,char t[],int tlen){
int count=0;int pos=0;
for(int i=0;i<slen;i++)
{
for(int j=i,k=0;k<tlen;j++,k++)
{
if(s[j]==t[k]&&t[k]!='\0')
{
continue;
}
printf("i=%d k=%d \n",i,k);
count+=(k+1);
break;
}
if(t[k]=='\0')break;
}
return count;
}
//当s和t一样时,count=0,思考为什么?s[i]==t[k]完全相等,k++,当字符串比较到最后,不再进入内层循环,所以count+=(k+1)没有被执行
//上面,第一部分末尾有正确的写法
int strindex002(char s[],int slen,char t[],int tlen){//right====
int count=0;int pos=0;int count1=0;int count2=0;
for(int i=0;i<slen;i++)
{
for(int j=i,k=0;k<tlen;j++,k++)
{
if(s[j]==t[k]&&t[k]!='\0')
{
count1++;//相同则count1++
continue;
}
count2++;//跟踪下,,特例:主串和模式串相同,没有执行count2++
break;
}
if(t[k]=='\0')break;
}
printf("count1=%d count2=%d\n",count1,count2);//test
return count1+count2;
}
//这样如果相同,count1会记录比较相等的次数,count2记录比较不等的次数
//计数在循化中经常运用
********************************
深入体会break和continue
********************************
特别小心:循环的控制条件
循环易错点:
计数在循化中经常运用
********************************
深入体会break和continue
********************************
特别小心:循环的控制条件
下面是正确的代码:
int strindex001(char s[],int slen,char t[],int tlen){
int count=0;
int flag=0;//设立标志位
for(int i=0;i<slen;i++)
{
int k=0;//k表示第i趟比较的次数
for(int j=i;k<tlen;j++,k++)//这个地方小心
{
if(s[j]==t[k]&&t[k]!='\0')//if(s[j]==t[k]&&t[k]!='\0'),其实t[k]!='\0'可以不要因为 for(int j=i;k<tlen;j++,k++)中k<tlen
{
continue;//相等继续往后比较
}
else
break;//不等结束循环
/* printf("i=%d k=%d \n",i,k);//test
if((++k)=tlen)
{
count+=k;
}else
{
count+=(k+1);//为第i趟字符比较的次数
}
break;*///猪脑子吗?第i趟字符比较的次数,i控制外层循环,应该放在外层循环中
}
if(t[k]=='\0')//匹配成功
{
count+=tlen;
break;
}else{
count+=k;
}
}
return count;
}
错误部分(被注释)如下
/* printf("i=%d k=%d \n",i,k);//test
if((++k)=tlen)
{
count+=k;
}else
{
count+=(k+1);//为第i趟字符比较的次数
}
break;*/
//猪脑子吗?k记录第i趟字符比较的次数,i控制外层循环,应该放在外层循环中