一、BM_BC算法
- 观察
- 从局部来看串匹配算法:多次成功,一次失败
- 从整体来看串匹配算法:一次成功,多次失败
- 更多的关注教训,使之更早地出现,更大者更早出现
- 模式串中越靠后的字符对算法性能的优化作用更大,可以排除掉更多的不匹配位置,故可以把计算的次序颠倒过来,以终为始。
- 即每一趟比对都应该从末字符开始,自后向前,自右向左。
- 在蛮力算法版本2的基础上实现,i是文本串中对齐模式串头位置的秩,i+j是与模式串中匹配失败的位置的秩,j是模式串中匹配失败的位置的秩,bc数组存储的是在位置j处匹配失败后下次匹配j的初始值
- 坏字符策略Bad - Character
- 某趟扫描中,一旦发现T[ i + j ] = X ≠ Y = P[j]
- 则P相应的右移,并启动新一轮扫描比对
- 位移量取决于失配位置j,以及X在P中的秩,而与T和i无关
- 令bc[X] = rank[X] = j - shift
- 在有多个候选时,选择秩最靠后者,即相对于X的后缀中必需不包含任何X
- 在模式串中,可能没有任何这样的X可供选择,则将模式串完整的移过这个位置。为每一个模式串在最左侧设置一个假象哨兵,于是,所谓的将整个模式串移过这个位置,也就等效于用这个通配殴打哨兵与刚才失配的字符。
- 模式串中确实存在一个候选的字符X,然而其中最靠右的那个字符,位置过于靠右,即它的秩太大,超过了j,以至于通过二者之差所计算出来的位移量为负。处理方法为,将整个模式串向后移动一个字符,并进而考察下一个对齐位置。
- bc表
- 附加空间 = |bc[ ] | = O( |∑| ) = O(S)
- 时间 = O(| ∑ | + m)= O(s + m)
- 性能分析
- 最好情况:O(n / m)
- 例如:T = xxxx1 xxxx1 xxxx1 … xxx1 P = 00000
- 一般情况:只要P不含T[i + j], 即可直接移动m个字符。仅需单次比较,即可排除m个对齐位置
- 如果说KMP是善于利用经验的高手,BM则乐于借鉴教训,即BM更加欢迎失败的情况
- 单次匹配概率越小,性能优势越明显,适合大字母表的情况如ASCII或UniCode
- 最坏情况
- 最坏 = O(n * m)——退化为蛮力算法
- 例如:T = 00000 00000 … 00000 P = 10000
- 最好情况:O(n / m)
int * buildBC(char* P){
int *bc = new int[256];//bc[]表,与字母表等长
for(size_t j = 0; j < 256; j++) bc[j] = -1;//将所有表项的初始统一指向那个哨兵
for(size_t m = strlen(P), j = 0; j < m; j++)//自左向右扫描
bc[ P[j] ] = j;//刷新P[j]的出现位置记录(画家算法:后来覆盖以往)
return bc;
}//第二个循环,通过引入临时变量m,避免反复调用strlen()
二、BM_GS算法
- 经验 = 匹配的后缀
- 扫描比对中断于T[i + j] = X ≠ Y = P[j]时,U = P(j, m) 必为好后缀
- 也就是说,如果当前的这趟扫描失败于X不等于Y处,那么也同时意味着模式串中相应的后缀必然是完全匹配的,也就是好后缀。因此接下来如果希望通过右移,重新在某个位置上对齐,就应该至少使得文本串中刚才匹配的部分能够得以继续匹配。
- 已经发现Y不能与X匹配,所以在经过这次移动后,对应的新字符,也至少不能依然是Y
- 在模式串中,足以与刚才文本串中匹配的部分继续匹配的子串,有可能会有多个,此时选择最靠后者。
- 在模式串中不存在任何一个这样的子串足以与刚才匹配的部分继续匹配。这也意味着此后的对齐位置至少应该越过当前失配的这个字符,也就是说,移动的距离将会更大。
- 在后移中需要保证,所有值得对齐的位置,都不至被遗漏掉。在这种情况下,要尽可能的从模式串中找到这样一个子串,更准确地说是一个前缀,使得它至少能够与刚才匹配部分的相应后缀继续匹配。
- 观察得到,位移量仅取决于j和P本身——亦可预先计算,并制表待查
- 构造GS表
- MS[ ]-> ss[ ]
- MS[j]: P[0, j]的所有后缀中,与P的某一后缀匹配的最长者。即终止于P[j],且与P的后缀匹配的最长子串,也有可能为空
- ss[j] = |MS[j]| ,即为MS[j]子串的长度
- ss[ ]表中蕴含了gs[ ]表的所有信息
- ss[ ]->gs[ ]
- 若ss[ j ] = j + 1, 则对于任一字符P[ i ](i < m - j -1), m - j - 1必是gs[ i ]的一个候选。即ss[j]可能达到最大的极限,也就是j+1,此时MS[j]子串足够长,它就是整个模式串的一个前缀。于是对于i范围内的任何一个字符P[ i ],一旦在扫描匹配中失配,m - j -1必然是一个值得考虑的位移距离。
- 若ss[ j ] <= j, 则对于任一字符P[m - ss[j] - 1], m - j - 1必是gs[m - ss[j] - 1]的一个候选。即一般的情况,MS子串的长度充其量不过j,如果将来m - ss[j] - 1处失配,接下来一种值得考虑的位移距离也就是m - j - 1.
- 构造ss[ ]
- 采用蛮力策略,每个字符都需一趟扫描,累计O(m^2)时间
- 子后向前逆向扫描,只需O(m)时间
- MS[ ]-> ss[ ]
- 性能
- 空间:| bc[ ] | + | gs[ ] | = O( |∑| + m )
- 预处理:O( |∑| + m )
- 查找
最好 = O( n / m)
最差 = O( n + m)
三、串匹配给典型算法的性能比较
- 横轴表示单次比对的成功概率,大致与字母表表示的规模成反比
- 纵轴表示运行时间,自下而上分别为n / m 、n + m以及n * m
四、Karp - Rabin算法,串即是数
All things are numbers.
Pythagoras(570 - 495BC)
- 将每一个串视作一个自然数,给字母表编号,将串转换为字母表规模进制的数字,可进一步转换为十进制。
- 数位溢出
- 将每一个串所对应的自然数称作它的指纹
- 如果|∑|很大,模式串P较长,其对应的指纹将很大
- 解决上述问题:散列压缩
- 基本构思:通过对比压缩后的指纹,确定匹配位置
- 关键技巧:借助散列,将指纹压缩至存储器支持的范围。比如采取模余函数
- 从首个位置开始,逐次尝试,在每个位置都将局部的子串与模式串进行比对,在两个串的指纹之间进行比对。为此要先用模余法求出模式串的指纹,再在文本串中计算子串的指纹,比对二者。指纹相等后,再逐个比对该数字,以确定是否完全相等。
- 快速指纹计算
- 计算每一个子串指纹的时间正比于子串的长度即O(m),一共需要O(n*m),如何加速?
- 将每一指纹的计算时间优化到常数
- 利用文本串中任何两个相邻子串之间,存在紧密的相关系。相邻的两次计算之间,存在着某种相关系;相邻的两个指纹之间,也存在着某种相关系。
- 相邻的子串几乎一样,二者唯一的区别仅在于前者的首字符与后者的末字符。
- 利用该性质从前一个指纹计算出后一个指纹。