字符串匹配问题
首先简单介绍一下字符串匹配问题,字符串匹配问题里面包含一个文本串和一个模式串。我们的目标是找到文本串中与模式串相同的子字符串,该问题就称之为字符串匹配问题。
朴素字符串匹配算法
朴素字符串匹配算法其实就是暴力对比的原理,因为模式字符串所有可能的开头只有文本串中每一个字符的位置,所以我们只需要判断以文本串中每一个字符打头时,模式串是否可以匹配文本串。
code
//朴素算法
bool ncmp(string str,string p){
int n=str.length();
int m=p.length();
for(int i=0;i<n-m+1;i++){
bool tag=true;
for(int j=0;j<m;j++){
if(str[i+j]!=p[j]){
tag=false;
break;
}
}
if(tag)return true;
}
return false;
}
复杂度分析
朴素算法的时间复杂度是(n-m-1)m的,其中n是文本串长度,m是模式串长度,当m长度接近n时,算法的时间复杂度就是O(n^2)的,所以针对大规模的字符串来说,该算法的性能并不高,之所以造成这种情况,是因为他没有有效的利用文本串每一位与模式串比较时的结果,也即假如每一位从开头就比较失败,那么算法的时间复杂度是O(n)的,此时我们可以认为是利用了每一步文本串S与模式串P比较时的信息,而当P与S比较成功时,此时已经比较过的匹配成功字符的信息我们并没有在后续比较过程中加以利用,例如在S=abcabcd和P=abcd这种情况下,P首先从S的第0位尝试比较,比较失败;接下来朴素算法会选择S的第二位重复操作,而没有利用第一步时P[1]=S[1],P[2]=S[2]的信息,如果我们有效利用该信息,并知晓P[0]!=P[1] ,P[0]!=p[2],那么下一步时我们可以直接从S[3]开始匹配,而跳过S[1],与S[2]的匹配过程,这显然会很大程度地降低时间复杂度。
Rabin-Karp算法
考虑到朴素算法的缺陷,后面三种算法都是对模式串进行预处理,来保存之前比较时可利用的信息。
RK算法的预处理时间复杂度是O(n),而最坏的情况下,时间复杂度是O((n-m+1)*m)的,但是基于一些假设,该算法的平均时间复杂度是较好的。
该算法运用了初等数论的概念,假设字符集只包含{0,1,2…,9},那么字符串"12345",对应数字就是12345,对于一个字符集只包含0~9的文本串而言,对于模式串P来说,我们可以计算出P对应的值,然后我们用这个唯一的数字代替该字符串,现在我们的比较过程就是用P对应的数,和文本串S中每一个P将要匹配的子串所对应的整数进行比较,如果两个相等就证明原始字符串相等。
可以看出该方法实际上就是将字符串映射到了一个数,而将字符串比较与数挂钩,实际上的思想与哈希函数的思想是类似的。
现在的问题是如何快速计算出模式串对应的数,以及文本串S中每一个与P长度相同的子串对应的值,我们以S=“1231321”,p="313"为例,我们可以在O(m)的时间复杂度内求得P对应的值,而如何在O(n)的时间复杂度内求出S所有长度与P相等的子串对应的值呢?首先我们可以求得S[0]打头的字符串对应的数num[0]是123,我们如何由num[0]求解出num[1]呢?这实际上就是一个简单的差分数组的问题,我们可以由num[1]=(num[0]-S[0] * 10^m)+S[1+p];快速求出S中每一位打头的长度等于P的子串对应的nums[i],此时我们只需要比较对应的P的值和nums[i]的值是否相等即可;
另一个问题是当m较大时,由于数也会变得很大,这样数的比较就不是O(1)时间复杂度了,所以这里我们利用模相等的思想来判断是否匹配,具体来说就是(P=nums[i])mod q,时P可能与S中i打头的子串匹配,之所以是可能只因为,对于大数来说模相等,不意味原值相等,但是模不相等,原值一定不相等。
这样我们就可以利用模等式来排除不可能相等的,而只比较模相等的情况,这也是我们为什么说该算法最坏条件下的时间复杂度是O((n-m+1)*m)的原因。
code
#include<bits/stdc++.h>