字符串匹配就是在主串A中查找模式串B,例如在主串abababc中查找模式串abc是否存在,记主串A的长度为n,模式串B的长度为m,n>=m。
BF算法
BF(Brute Force)算法,又叫暴力匹配算法或者朴素匹配算法,思路很简单:在主串中取前下标为[0,m-1]这m个字符的子串和模式串逐个字符逐个字符比较,如果完全一样就结束并返回下标;如果有不一样的,那么主串中的子串后移一位,主串中[1,m]这个子串和模式串继续比较,… ,主串中[n-m,n-1]这个子串和模式串继续比较。
主串中长度为m的子串有n-m+1个。
主串 | a | b | a | b | a | b | c |
---|---|---|---|---|---|---|---|
模式串 | a | b | c |
主串 | a | b | a | b | a | b | c |
---|---|---|---|---|---|---|---|
模式串 | a | b | c |
主串 | a | b | a | b | a | b | c |
---|---|---|---|---|---|---|---|
模式串 | a | b | c |
主串 | a | b | a | b | a | b | c |
---|---|---|---|---|---|---|---|
模式串 | a | b | c |
主串 | a | b | a | b | a | b | c |
---|---|---|---|---|---|---|---|
模式串 | a | b | c |
int BF(std::string &s,std::string &pattern) {
int n = s.length(), m = pattern.length();
for (int i = 0; i < n-m+1; i++) {
int j = 0;
for (; j < m; j++) {
if (s[i + j] != pattern[j])
break;
}
if(j==m)
return i; //匹配到了,返回主串中的下标
}
return -1; //匹配不到
}
最坏的情况下,在第一个for循环里,i 从0到n-m走满共n-m+1次,第二个for循环里,j 从0到m-1走满共m次,因此最坏的情况下时间复杂度为O(n*m),举个例子,在bbbbbbf中查找bf,所有n-m+1个子串都要走完,并且每次和模式串比较都要比较m次,总共比较n-m+1次。
BF算法最大的优点就是简单,代码不容易出错,在主串和模式串的长度都不大的时候还是比较实用的。
RK算法
RK(Rabin-Karp)算法,是用两个发明者的名字命名的。思路也比较简单:对主串中n-m+1个子串求哈希值,模式串也求哈希值,然后比较子串的哈希值和模式串的哈希值,如果不相等证明不匹配,如果相等就匹配(在没有哈希冲突的情况,冲突的情况后面会讲)。
这个算法对哈希函数的设计要求会高一点,当然最好就不存在哈希冲突,就会比较简单,相等就匹配,不相等就不匹配。
来看看这样一个设计:假设主串和模式串只有a-j这10个字母,我们可以直接将字符串映射成整数(a-j对应十进制0-9),例如bcd我们可以直接映射成bcd=1*10*10+2*10+3=123,这样就不存在哈希冲突了。那如果现在是a-z这26个字母,我们可以用同样的思路,但是使用26进制,a-z对应0-25:例如bcd=1*26*26+2*26+3=731。
这个哈希函数是有规律的,当前子串的哈希值hash[i]是可以根据上一个子串的哈希值hash[i-1]计算得到,来看看下面这个例子:
上面的子串aba的起始坐标为i-1,哈希值为hash[i-1],下面子串bab的起始坐标为i,哈希值为hash[i],我们知道两个子串中都有ba,但是下面的ba映射成的值要比上面的ba要大26倍,因为所在的位置不一样,下面的ba在最高位和第二位,下面的ba在第二位和第三位,那么我们将hash[i-1]乘26,上下子串ba的hash值都相同了,然后再减去上面子串的最高位a(注意此时的a也是乘多了个26的),最后加上下面子串的末尾的b即可。最终的计算结果就同上图中的计算一样,注意蓝色框框的m即可,是m而不是m-1,因为hash[i-1]乘了26。
//假设哈希值不会溢出int的范围
int RK(std::string& s, std::string& pattern) {
int n = s.length(), m = pattern.length();
int p = pow(26, m); //26的m次方
int* hash = new int[n - m + 1];
hash[0]