字符串是一种特殊的线性结构
- 数据对象
- 无特殊限制
- 串的数据对象为字符集
- 基本操作
- 线性表的大多以“单个元素”为操作对象 – 串通常以“串的整体”作为操作对象
- 线性表的存储方法同样适用于字符串
- 应根据不同情况选择合适的存储表示
为了字符串间比较和运算的便利,字符编码表 一般遵循约定俗成的“偏序编码规则”
字符串的存储结构主要是需要考虑到串长问题,现在一般是用一个特殊的末尾标记 ‘\0’ (C/C++)。
字符串类的存储结构
private: // 具体实现的字符串存储结构
char *str; // 字符串的数据表示
int size; // 串的当前长度
字符串的模式匹配-KMP算法
朴素算法之所以较慢的原因是有冗余运算,每次模式串只右滑了一位,实际上大多数时候模式串可以右滑更多位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
-
N[0] = -1,N[1] = 0;(此时N[j] = k)
-
从第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
-
-
优化,如果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.