C++字符串与KMP算法和BM算法时间复杂度

3,331 阅读7分钟

字符串是一种特殊的线性结构

  • 数据对象
    • 无特殊限制
    • 串的数据对象为字符集
  • 基本操作
    • 线性表的大多以“单个元素”为操作对象 – 串通常以“串的整体”作为操作对象
  • 线性表的存储方法同样适用于字符串
    • 应根据不同情况选择合适的存储表示

为了字符串间比较和运算的便利,字符编码表 一般遵循约定俗成的“偏序编码规则”

字符串的存储结构主要是需要考虑到串长问题,现在一般是用一个特殊的末尾标记 ‘\0’ (C/C++)。

字符串类的存储结构

private: // 具体实现的字符串存储结构 
char *str; // 字符串的数据表示
int size; // 串的当前长度

字符串的模式匹配-KMP算法

朴素算法之所以较慢的原因是有冗余运算,每次模式串只右滑了一位,实际上大多数时候模式串可以右滑更多位K,并且这个完全由模式串决定。

可以看出只有存在k使得 P0 P1 ...Pk ≠ Pj-k-1 Pj-k ...Pj-1 P0 且 P1 ...Pk-1 = Pj-k Pj-k+1 ...Pj-1, 那么模式串就可以右滑j-k位。

int KMPStrMatching(string s,string p,int *N,int start){
	int m = s.length();  //目标长度
	int n = p.length(); //模式长度
	int i=start; //目标的下标
	if(n -start>m) return -1;
	int j=0; //模式的下标
	while(i<m && j<n){
		if(j==-1 || s[i]==p[j])
			i++,j++;
		else j = N[j];   //注意始终是模式串去滑动,目标串每次只右移一位
	}
	if(j>=n)
		return i-n;
	return -1;
}

字符串的特征向量N:构造方法

其实这也是一个KMP自匹配的过程,我们可以把P0...Pj看作目标串字串的,而把P0...Pk看作模式串的字串,正好匹配到j的位置时需要判断匹配不匹配,如果匹配的话那么模式串和目标串的比较位置都向后移动一位接着KMP,如果不匹配那么就需要递归地去找上一个KMP的位置,到最后都找不到的话,那也只能右移。 我们递归的去实现: 设特征串位s,特征向量为N

  1. N[0] = -1,N[1] = 0;(此时N[j] = k)

  2. 从第0位开始循环计算第j+1位的N[j+1],

    假设我们已经知道 N[j] =k(因为N(0)已经知道,所以后面的肯定可以算出来,只需要保证N(0)满足即可推出所有的),那么如果

    • if

      • s[j]==s[k],又匹配到了一位
      • k=-1,表示需要跳到首元素

      因此这两种情况都是N[j+1] = k+1,也就是说KMP自己向右滑动接着匹配 *else 那么说明之前的都不匹配,匹配串需要向右滑动,直到k达到上述两个条件之1

  3. 优化,如果s[j+1]==s[k+1],那么j+1肯定不匹配,所以说可以优化为N[j+1]=N[k+1],增加模式串KMP向右滑动的距离 难点在于假设N[j] = k这个递归上的假设上。

//j=next[j],对应上面的KMPStrMatching中的j
//				-1, j=0
//    next[j] = max{k:0<k<j. && p[0...k-1] = p[j-k...j-1]},如果k存在
//				0  其他情况
int *findNext1(string s){
	int size = s.length();
	int *next = new int[size];
	int j,k;
	next[0] = -1;
	j = 0;k = -1;
	while(j<size-1){
		while(k>=0 &&s[j]!=s[k])
			k = next[k];
		j++;
		k++;
		if(s[j]==s[k])
			next[j] = next[k];
		else next[j] = k;
	}
	return next;
}

实际上在很多流行软件里BM算法更稳流行。

BM算法

BM算法的核心思想就是模式串从右向左匹配,如果不匹配那么模式串从左向右滑动,滑动距离根据好后缀数组和坏字符数组来取最大的滑动距离。 整体来说算法思路比较清晰,相对于KMP好后缀的构造也比较的易于理解。 坏数组需要额外的字符字典空间,对于中文来说会占用较多空间。 好后缀数组的计算氛围了两步骤:

1. 计算后缀数组
    这里有个小技巧就是,利用满足条件的已算过的好后缀计算出未算过的
2. 有且仅有三种情况计算最终结果:(这三种移动距离逐渐增加,我们实现的时候是逆序实现,用小的移动距离覆盖大的)
    - 模式串包含好后缀,且不是前缀,移动距离与当前位置和后缀数组位置有关
    - 模式串包含好后缀,且是前缀,移动距离只跟后缀数组的位置有关,与当前位置无关
    - 不包含,移动距离与当前位置无关

文字不懂的话参看这里图示

好了,Talk is cheap,上代码:

#include <iostream>
#include <string>
#include <algorithm>
using namespace std;


//这里我们只使用ASCII的128个字符,中文处理不了
#define ASIZE 128
void preBmBc(char *x, int m, int bmBc[]) {
   int i;
 
   for (i = 0; i < ASIZE; ++i)
      bmBc[i] = m;
   for (i = 0; i < m - 1; ++i)
      bmBc[x[i]] = m - i - 1;
}

void suffixes(char *x, int m, int *suff) {
   int f, g, i;
 
   suff[m - 1] = m;
   g = m - 1;
   for (i = m - 2; i >= 0; --i) {
   	//如果某一后缀段包含在其中一个大后缀段,那么大后缀段子集 与与之对应的前面匹配的后缀的子集必然是相等的
      if (i > g && suff[i + m - 1 - f] < i - g)
         suff[i] = suff[i + m - 1 - f];
      else {
         if (i < g)
            g = i;
         f = i;
         while (g >= 0 && x[g] == x[g + m - 1 - f])
            --g;
         suff[i] = f - g;
      }
   }
}
 
void preBmGs(char *x, int m, int bmGs[]) {
   int i, j, suff[m];
 
   suffixes(x, m, suff);
 	//case1:不存在好后缀,直接移动模式串长度,与j所处位置无关
   for (i = 0; i < m; ++i)
      bmGs[i] = m;
   j = 0;
   //case2:存在好后缀,且好后缀是前缀,中间段任何j都移动,m - 1 - i,可以看出是与j无关的
   for (i = m - 1; i >= 0; --i)
      if (suff[i] == i + 1) //长度比i多1,说明是前缀
         for (; j < m - 1 - i; ++j)
            if (bmGs[j] == m) //多个前缀可能重复
               bmGs[j] = m - 1 - i;
    //case3:存在好后缀,且不是前缀,m - 1 - j,可以看出与j所处的位置有关
    //这里的移动距离会覆盖上面的两种,因为这个是移动最小的,并且不会重复
   for (j = 0; j <= m - 2; ++j)
      bmGs[m - 1 - suff[j]] = m - 1 - j;
}

void OUTPUT(int pos){
	cout<<pos<<endl;
}

//x模式串,m模式串长度,y目标串,n目标串长度
void BM(char *x,int m, char *y,int n){
	if(m>n){
		return OUTPUT(-1);
	}
	int i;//模式串位置
	int j=0; //目标串位置
	int total = 0; //找到了几个
	int bmGs[m];//坏字符集
	int bmBc[ASIZE];//好后缀
	/**预处理**/
	preBmGs(x,m,bmGs);
	preBmBc(x,m,bmBc);

	while(j<=n-m){
		for(i=m-1;i>=0&& x[i]== y[i+j];i--);
		if(i<0) //找到了一个结果
		{
			total++;
			OUTPUT(j);
			j+=bmGs[0]; //跨国匹配的字符接着匹配
		} else {
			//移动最多的
			j+=max(bmGs[i],bmBc[y[i+j]]+i-m+1);
		}
	}
	printf("共找到%d\n个", total);
}



int main(){
	string targetStr = "BMBBMB";
	string patternStr = "BMB";
	int m = patternStr.length();
	int n = targetStr.length();
	char *x = (char *)patternStr.c_str();
	char *y = (char *)targetStr.c_str();
	BM(x,m,y,n);
	return 0;
};

KMP与BM算法时间复杂度比较

BM

BM算法的预处理时间是O(m+ASIZE)

匹配时间最差是O(mn),引用论文的例子 (e.g. pattern = "CABABA," and string = "XXXXAABABAXXXXAABABA ..."). 可以看出每次最多只能移动2步,也就是运算(n-m)/2*(m-2)也就是O(mn)

最好的情况是 pattern:am-1b ,string:bn,(n-m)/m,也就是O(n/m)

对于无周期的模式串,时间复杂度不超过3n。

KMP

KMP的预处理时间是O(m)

匹配时间是比较稳定的是O(n),证明如下:

循环体中”j = N[j];” 语句的执行次数不能超过 n 次。否则,由于“j = N[j]; ” 每执行一次必然使得j减少(至少减1)而使得 j 增加的操作只有“j++ ”那么,如果“j = N[j]; ”的执行次数超过n次,最终的结果必然 使得 j 为比-1小很多的负数。这是不可能的(j有时为-1,但是 很快+1回到0)。

实际应用中BM算法往往效率更高,还有一个改进的Turbo-BM 算法,能让BM算法最坏的情况降到2n,参考:Turbo-BM algorithm.