目录
问题提出:
有一串长的字符串,我们要找到其中是否有我们需要的字符串,即关键字检索。
如以下的一段字符串:
我们想要找到的字符串
朴素模式匹配:
朴素法原理
为了解决上述问题,我们首先第一个想法就是一个一个去对比,首先将我们想要查找的字符串(子串)的第一个位置与被查找的字符串(主串)的第一个位置去比较,然后紧接着第二个第三个。。。
简单的说,就是对主串的每一个字符作为子串开头,与要匹配的字符串进行匹配,对主串进行大循环,每个字符开头做T的长度的小循环。去达到我们的目的。
通过这种思想的出的算法当然是有效的,但是考虑一个问题,如果是主串长度为n,子串长度为m,对于朴素模式匹配算法。最坏的情况就是在主串的最后找到了子串,这样的话对于计算机来说已经进行了(n-m+1)*m次计算,它的时间复杂度为O(mn)
下面是朴素匹配的代码,为了方便与KPM算法做比较,我们有意的使主串很长,在代码中我们设定了主串的长度为3145736
朴素法代码
#include<iostream>
#include<ctime>
#include<cstring>
using namespace std;
int Index(string T, string S )
{
int i = 0; //主串T当前下标
int j = 0; //子串S当前下标
while (i<T.size() && j<S.size()) //i小于T长度,j小于S长度,循环继续
{
if (T[i] == S[j])
{
i++;
j++;
}
else //指针退回,重新开始匹配
{
i = i - j + 1; //i退回到上一次匹配首位的下一位
j = 0; //j退回到子串T的首位
}
}
if (j >= S.size())
return i - j + 1 ; //对于计算机0就是1,所以方便我们读,我们在这加个1
else
return 0;
}
int main()
{
string P = "bcg" ;
string F = "abcbcglx";
string S = "bcgl";
for ( int i = 0; i < 20 ; i++)
{
P = P + P ;
}
string T = P + F ;
int sult ;
clock_t t ;
t = clock() ;
sult = Index(T, S) ;
t = clock() - t ;
cout << "T is : "
<< endl
// << T ,如果电脑好的话,可以把这个T打印出来,是一个长度为3145736的字符串
<< endl
<< "S is :"
<< endl
<< S
<< endl
<< "It will matching at "
<< sult
<< " position"
<< endl
<< "time spend : "
<< t
<< endl;
return 0;
}
KMP模式匹配算法
背景介绍:
如果单纯需求来看。我们想要完成对于一个字符串的检索的目的完全可以通过朴素匹配法完成,剩下的事情交给计算机就可以了。然而在很多的科学家眼里,朴素法(也可以称为“暴力求解法”)这种方法是在是太低效了,作为前辈大牛的他们肯定无法忍受这件事情。于是就有三位前辈,D.E.Knuth 、 J.H.Morris 、 V.R.Pratt发表了一个模式匹配算法,该算法可以大大避免重复遍历的情况,我们把它称为Knuth-Morris-Pratt算法,简称KMP算法
原理介绍: ![](https://i-blog.csdnimg.cn/blog_migrate/9b3eaf442f6717a37c0d2adc15740b8e.png)
首先依旧是主串和子串,主串是text 子串是pattern。在最开始还是从两个串的首位进行比较
我们发现,1,2,3 这三个位置都是匹配的,从第4位开始不匹配,那么在第二次的遍历中我们就不从主串的第二位开始了,直接从主串的第4位开始,但是很明显子串的前缀后缀并不相等,所以我们第二次的比较要用子串的第一位与主串的第四位做比较
第二次比较中主串第四位与子串第一位字符不匹配,继续从第五位开始进行第三次比较。
这个时候我们引入一个前缀和后缀的概念:前缀其实简单来说就是子串从前数,后缀自然就是从后数了,我们从下图来具体的直观说明前缀和后缀。通过前缀后缀可以保证我们在搜索的时候没有遗漏的字符串。
我们发现在第三次比较中,在未匹配的位置前,子串的前面是ab ,而最后也是ab ,那我们就称之为前缀等于后缀,所以在第四次比较中我们就要用主串的第11为与子串的第三位比较,直观如下
在第四次比较中,我们发现主串的第11位与子串的第3位并不相同,对于此时的子串来说,第三位之前的字符是 ab 很明显。前缀不等于后缀,故在第五次比较中,应该用主串的第11位与子串的首位进行比较
在第五次比较中发现子串的首位和主串的第11位并不匹配,所以第六次我们将主串的第12位和子串的首位进行比较
第六次比较后我们发现子串的第8位和主串的第19位不匹配,而子串在第八位之前是abcdabc,比较前后发现 abc 是前后缀相同的部分,所以在第七次匹配时,我们就用子串的第4位与主串的第19位进行比较。
第九次完全匹配!这就是KMP算法的原理啦!
编程实现:
原理其实通过我们的图来看很简单,其实KMP算法的关键就在于前后缀的引用,这对于我们聪明的人类来说还是很简单滴,但是在对于只会2以内加减法的计算机来说,如何让计算机去理解前缀与后缀就是个问题啦。
void Getnext(int next[],String t)
{
int j=0,k=-1;
next[0]=-1;
while(j<t.length-1)
{
if(k == -1 || t[j] == t[k])
{
j++;k++;
next[j] = k;
}
else k = next[k];
}
}
我们给定一个设定一个 next[ ] 数组去记录子串中每个字符所对应的前缀数目。比如下图所示
比如b对应的就是b前面的字符串的前缀长度,但是他前面只有一个a,所以前缀的长度为0
同理对于c来说也一样,但是对于t[5]的b来说,他的前面有一个前缀,所以b对应的next[5]就是1,所以我们就可以把每个值对应的next都求出来
这样的话我们就求出了子串中除了最后一个值剩下的每个值的前缀长度(最后一个值的前缀没有求的必要,因为如果最后一个值匹配上了就说明我们的检索完成了,根本用不到他的前缀值)。 在编程中 next()函数是整个KMP算法的精华所在,想要理解的话个人觉得最好的方式就是自己写一串字符按着代码上的流程进行验算一遍。只要跟着推一遍你就会知道他是在干嘛了
至于为什么在这里不做太多说明,主要是在学习这一块的时候我看论坛上的各种讲解和相关视频以及数据结构书中的表达都没有看懂,直到我自己列了字符串按着代码一个一个带数才搞明白了next函数,最经典的就是最后一句 k = next[k],我只能说我领略到了他的意思,但是确实不知道该怎么表达(翻译一下就是:是我真的菜,我也不懂),真的不理解KMP这三位教授是怎么推导出来的。只能说一句“牛逼!”
代码:
#include <iostream>
#include <cstring>
#include <ctime>
using namespace std;
int getNext(string S, int next[]) //next 函数
{
int j=0;
int k=-1;
next[0]=-1;
int Slen = S.size() ;
while( j <Slen-1)
{
if(k == -1 || S[j] == S[k])
{
j++;
k++;
next[j] = k;
}
else k = next[k];
}
return 0 ;
}
int Index_KMP(string T, string S ,int &num)
{
int i = 0;
int j = 0;
int next[4] ;
getNext(S, next);
int Slen = S.size() ;
int Tlen = T.size() ;
while (i < Tlen && j < Slen )
{
if (j==-1 || T[i] == S[j])
{
i++;
j++;
num++ ;
}
else
j = next[j] ;
}
if (j >= Slen)
{
return i - Slen +1 ;
}
else
return 0 ;
}
int main()
{
int sult ;
int num = 0;
string P = "bcg" ;
string F = "abcbcglx";
string S = "bcgl";
for ( int i = 0; i < 20 ; i++)
{
P = P + P ;
}
string T = P + F ;
// string T = "abcbcglx" ;
// string S = "bcgl" ;
clock_t t ; // 计算cpu运行时间
t = clock() ;
sult = Index_KMP(T,S,num) ;
t = clock() - t ;
cout << "T is : "
// << T //字符太长了,我们就不打印了
<< endl
<< endl
<< "S is :"
<< endl
<< S
<< endl
<< "It will matching at "
<< sult
<< " position"
<< endl
<< "time spend : "
<< t
<< endl
<< "the number of calculate is : "
<< num
<< endl ;
return 0;
}
在最开始写代码的时候遇到了一些问题,比如在调试中突然冒出了一个未知变量,也不知道他是从哪里来的,next[]数组的值无法正常记录,while循环直接被跳过,这些都是我最初不能理解的问题,因为从逻辑的角度我找不到任何问题,特别感谢以下的两篇博文解决了我的问题
https://blog.csdn.net/m0_63520117/article/details/123240205?spm=1001.2014.3001.5501https://blog.csdn.net/ke_yi_/article/details/81908835
next 是C++中的关键字,不可以用来做数组名(不过在实际实现的时候我发现用next做数组名也是可以的,会覆盖掉原先的next)
最重要的是下面这一点!!!while()在进行逻辑判断时,是不能将逻辑判断符对size() 或者strlen()进行判断的,虽然程序可以继续运行,但是输出完全混乱,简直是天坑!
结果对比
朴素模式匹配法输出结果
KMP输出结果
可以看出,从时间的角度,KMP确实比朴素模式匹配算法要好