文章内容转载 王争的《数据结构与算法之美》
1.BF算法的原理与实现
为了方便之后的讲解,我们先来看两个概念:主串和模式串。这两个概念在所有的字符串匹配算法中都会用到。如果在字符串a中查找字符串b,那么字符串a就是主串,字符串b就是模式串。我们把主串的长度记作n,模式串的长度记作m。在一般情况下,n大于或等于m。虽然这不是必须得,但如果n小于m,那么在主串肯定不存在模式串。
BF(Brute Force, 暴力匹配) 算法也称为朴素匹配算法。从名字可以看出,这种匹配方式很“暴力”,简单直接,性能不高。
作为简单和“暴力”的字符串匹配算法,BF算法的思想可以用一句话来概括:如果模式串的长度为m,主串的长度为n,那么在主串中就会有一个n-m+1个长度为m的子串,我们只需要“暴力”地对比这n-m+1个子串与模式串,就可以找出主串与模式串匹配的子串。当然,在具体处理的时候,我们并非把n-m+1个子串都事先罗列出来,而是通过下标操作,让起始下标分别为0,1,2,...,n-m的子串与模式串尝试匹配。BF算法示例如图所示。
基于上述原理,BF算法的代码实现如下所示。
//返回第一个匹配的起始下标位置
int bf(char a[], int n, char b[], int m) {
for(int i = 0; i < n-m; i++){
int j = 0;
while (j < m){
if(a[i+j] != b[j]){
break;
}
j++;
}
if( j== m){
return i;
}
}
return -1;
}
2.BF算法的性能分析
在大部分情况下,时间复杂度表示为数据规模n这一个变量的表达式,但是,在有些情况下,时间复杂度会表示为两个变量的表达式,如O(nm)、O(n+m)。对于字符串匹配算法,其时间复杂度的表示,需要主串数据规模n和模式串数据规模m的共同参与。
从BF算法的原理和代码实现,我们可以看出,在极端情况下,如主串是“aaaa...aaaa”(省略号表示有很多重复的字符a),模式串是“aaaaab”。我们用主串中的n-m+1个子串与模式串匹配,每个子串与模式串都需要对比m个字符,这样才能发现无法匹配,因此,最坏时间复杂度是O(nm)。
从理论上来讲,尽管BF算法的时间复杂度很高,但在实际的开发中,它却是一个比较常用的字符串匹配算法,原因有下面3点。
第一,在实际的软件开发中,大部分情况下,模式串与主串的长度都不会太长。对于小规模数据的处理,时间复杂度的高低并不能代表代码真正的执行时间,有些情况下,时间复杂度高的算法可能比时间复杂度低的算法的运行速度更快。
第二,当每次模式串与主串中的子串匹配的时候,如果中途遇到不能匹配的字符,就可以提前终止,不需要把m个字符都对比一遍。因此,尽管理论上最坏时间复杂度是O(nm),但是,从概率统计上来看,大部分情况下,算法的执行效率要比最坏情况下的执行效率高很多。
第三,BF算法的思想简单,代码实现也非常简单。简单意味着不容易出错,即使存在bug也容易暴露和修复。在工程中,在满足性能要求的前提下,简单是我们的首选。这也符合我们常说的KISS(Keep It Simple and Stupid)设计原则。
因此,在实际的软件开发中,绝大部分情况下,朴素的字符串匹配算法就够用了。
3.内容小结
BF算法的实现思路非常简单,用模式串与主串中所有长度为m的子串进行匹配,其性能与更加高效的KMP、BM算法可能相差无几,甚至因为逻辑简单而更加高效,所以,在实际的软件开发中,BF算法较为常用,另外,大部分的编程语言中提供了相对应的函数实现。
4.思考
字符串匹配算法返回的结果是第一个匹配子串的首地址,如果要返回说有匹配子串的首地址,该如何实现呢?如何实现替换函数replace()?
int matchedPos[] = new int[n];//申请大一点的空间
int bf(char mainStr[], int n,char subStr[], int m,int matchedPos[]){
int matchedNum = 0;
for (int i = 0; i < n - m; i++) {
int j = 0;
while (j < m){
if (mainStr[i+j] != subStr[j]){
break;
}
j++;
}
if (j == m){
matchedPos[matchedNum] = i;
matchedNum++;
}
}
return matchedNum;
}
char* replace(char mStr[], int n, char pStr[], int m, char[] rStr, int k){
int[] matchedPos = new int[n];
int matchedNum = bf(mStr, n, pStr, m, matchedPos);
char[] newStr = new char[n + (k-m)*matchedNum];
int p = 0;//mStr上的游标
int q = 0;//newStr上的游标
for (int i = 0; i < matchedNum; i++) {
while (p < matchedPos[i]){
newStr[q++] = mStr[p++];
}
for (int j = 0; j < k; j++) {
newStr[q++] = rStr[j];]
}
p+=m;
}
while (p<n){
newStr[q++] = mStr[p++];
}
return newStr;
}