diff --git a/thu_dsa/chp11/bm.md b/thu_dsa/chp11/bm.md index 2df47c0..c7e2e4f 100644 --- a/thu_dsa/chp11/bm.md +++ b/thu_dsa/chp11/bm.md @@ -11,7 +11,7 @@ ![compare_backwards](compare_backwards.png) -如我们前面指出的那样,这种情况在头几次比对中出现的概率是极高的,因此相对于自左向右的比对,将比对的方向变成向右向左,的确可以极大地提高串匹配的效率。 +如我们前面指出的那样,这种情况在头几次比对中出现的概率是极高的,因此相对于自左向右的比对,将比对的方向变成自右向左,的确可以极大地提高串匹配的效率。 ## bm-bc策略 @@ -21,11 +21,11 @@ 因此,坏字符策略实现的关键,就在于找到模式串中,位于坏字符`Y`左侧,并且与字符`X`能够匹配的下一个字符。应该指出的是,这样的字符能够有多个,也就对应了多个移动距离,所有这些移动距离都是值得对齐的。因此,为了不错过其中的任意一个字符,应该取移动距离最小的那一个,使之与文本串中的`X`对齐。 -为了快速地确定下一个对齐位置,可以仿照`kmp`算法的思路,将模式串中出现过的所有字符,保存每个字符出现的最后位置,构成`bc`表,以便在字符匹配时迅速更新对齐位置。因此,`bc`表就有$\left|\Sigma\right| + 1$项,其中$\Sigma$为模式串的字符集大小,并且将额外的一项用来代表所有没有在模式串中出现过的字符,此时直接将模式串整体移过该字符。 +为了快速地确定下一个对齐位置,可以仿照`kmp`算法的思路,对于模式串中出现过的所有字符,保存每个字符出现的最后位置,构成`bc`表,以便在字符匹配时迅速更新对齐位置。因此,`bc`表就有$\left|\Sigma\right| + 1$项,其中$\Sigma$为模式串的字符集大小,并且将额外的一项用来代表所有没有在模式串中出现过的字符,此时直接将模式串整体移过该字符。 这样,`bm-bc`策略就可以利用`bc`数组来快捷地实现了。在一次匹配失败后,比如失败位置`j`处文本串字符为`X`,查询`bc`表会有三种情况: -一是`bc[X]`的确存在,并且`bc[X] < j`,此时直接将`bc[X]`处的字符平移到与文本串中的`X`对齐,即可开始新一轮的比对,此时模式串移动的距离应该是`j - bc[X]`;另一种情况是`X`没有出现在模式串的字符集中,此时应该将模式串整体移动字符`X`,即移动的距离的`j + 1`,为了与上面的情况统一,可以令`bc[*] = -1`,其中`*`表示所有没有出现在模式串中的字符,另一种理解方法是认为在模式串的左侧`P[-1]`存在一个通配符可以与`X`匹配。这两种情况如下图所示: +一是`bc[X]`的确存在,并且`bc[X] < j`,此时直接将`bc[X]`处的字符平移到与文本串中的`X`对齐,即可开始新一轮的比对,此时模式串移动的距离应该是`j - bc[X]`;另一种情况是`X`没有出现在模式串的字符集中,此时应该将模式串整体移动字符过`X`,即移动的距离的`j + 1`,为了与上面的情况统一,可以令`bc[*] = -1`,其中`*`表示所有没有出现在模式串中的字符,另一种理解方法是认为在模式串的左侧`P[-1]`存在一个通配符可以与`X`匹配。这两种情况如下图所示: ![bc_case12](bc_case12.png) @@ -45,9 +45,9 @@ int match(char* text, char* pattern){ ### `bc`表的构建 -实际上,在前面的讨论中,已经涉及到了如何构造`bc`表的问题,这里做一个统一的总结。为了构造`bc`表,需要遍历模式串中的每一个字符,并且把每个字符`X`最后出现的位置保存在`bc[X]`中。为了简单起见,这里的`bc`表包含全部字符集,比如整个ASCII码字符集,这样便于判断某个字符是否出现在模式串当中,否则为了判断某个字符是否在模式串中出现过,还需要额外建立一个散列表或者位图,还是需要消耗同样的空间,因此这里的`bc`表兼具了给出移动位置以及散列表的作用。 +实际上,在前面的讨论中,已经涉及到了如何构造`bc`表的问题,这里做一个统一的总结。为了构造`bc`表,需要遍历模式串中的每一个字符,并且把每个字符`X`最后出现的位置保存在`bc[X]`中。为了简单起见,这里的`bc`表包含全部字符集,比如整个ASCII码字符集,这样便于判断某个字符是否出现在模式串当中,否则还需要额外建立一个散列表或者位图,还是需要消耗同样的空间,因此这里的`bc`表兼具了给出移动位置以及散列表的作用。 -在`bc`表的构建中采用`画家算法`(painter's algorithm),即从左至右遍历模式串,对于其中每一个出现的字符,都将其位置(或者秩)更新到其在`bc`表中对应的项中,这样,遍历结束时`bc`表保存的就是所有字符最后出现过的位置了。`bc`构造的算法如下: +在`bc`表的构建中采用`画家算法`(painter's algorithm),即从左至右遍历模式串,对于其中每一个出现的字符,都将其位置(或者秩)更新到其在`bc`表中对应的项中,这样,遍历结束时`bc`表保存的就是所有字符最后出现过的位置了,因为`bc`表中各项的值,只取决于该字符最后一次出现的位置,类似于画家作画时,画布上的某处最终的颜色,仅取决于画家在该处的最后一笔,因此称之为画家算法。`bc`构造的算法如下: ```c void makeBC(char* const pattern, int* bc){ @@ -61,10 +61,10 @@ void makeBC(char* const pattern, int* bc){ 使用`bc`策略时,最好可以达到`O(n/m)`的时间复杂度,对应了每次都在最右一个字符匹配失败,然后整体右移`m`个单位的情况,如下图所示: -![bc_bestcase](bc_bestcase/png) +![bc_bestcase](bc_bestcase.png) 这固然是一个非常好的结论,但是`bc`策略在最坏情况下却会达到`O(mn)`的时间复杂度,与蛮力策略相当,该情况如下图所示: ![bc_worstcase](bc_worstcase.png) -可以看到,在这种情况下,每次都需要进行`m - 1`次比对,才能在最左侧一次比对中失败,而该次失败只能让模式串右移一个单位。这种情况正与蛮力策略的最好情况相一致。一般地,单次匹配成功的概率越大,即字符集越小,就越接近于这种最坏的情况;单次匹配成功的概率越小,即字符集越大,就越接近与最好的情况。 +可以看到,在这种情况下,每次都需要进行`m - 1`次比对,才能在最左侧一次比对中失败,而该次失败只能让模式串右移一个单位。这种情况正与蛮力策略的最好情况相一致。一般地,单次匹配成功的概率越大,即字符集越小,就越接近于这种最坏的情况;单次匹配成功的概率越小,即字符集越大,就越接近于最好的情况。