文章出处:极客时间《数据结构和算法之美》-作者:王争。该系列文章是本人的学习笔记。
1文本编辑器中的查找功能怎么实现
在word中有一个功能:查找某个字符串,将其替换为另一个字符串,就会用到这个功能。
需要新算法的原因是:word可能特别长,BF算法可能性能退化很严重;RK算法需要设计一个能够处理所有字符串的哈希函数,这并不容易。
新的算法BM,根据实验统计,其性能是KMP的3~4倍。
2算法的核心思想
我们把模式串和主串匹配的过程看做是模式串在主串中不断向后匹配的过程。当遇到不匹配的字符的时候BF和RK都是向后移动一位。那可不可以多移动几位呢?
在例子中主串中的c在模式串中不存在,可以直接将模式串移动到c后面。
由现象找规律。当遇到不匹配的字符的时候,有什么规律可以让模式串一次移动好几位呢?
BM算法就是在找这种规律。规律的核心分为坏字符和好后缀两个规则。
2.1 坏字符规则
我们从模式串的末尾往前倒着匹配,当我们发现某个字符没法匹配的时候,把这个没有匹配的字符叫做坏字符(主串中的字符串)。
当发生不匹配的时候,我们把坏字符在模式串中出现的位置记为si,如果坏字符在模式串中存在,则找到在模式串中最右边的位置记为xi,不存在xi=-1。模式串向后移动的位数:si-xi。
我们用数组bc[i]表示字符i在模式串中出现的位置。当模式串中有多个字符i的时候,bc[i]保存的是i的最大位置。这是为了防止过度偏移。
在上图中,当再次发生不匹配的时候bc[a]=0。这时候偏移2-0=2。
坏字符规则代码部分。
// 数据包含的字符范围private static final int SIZE = 256;private void generateBC(char[] b, int m, int[] bc) {Arrays.fill(bc, -1);for (int i = 0; i < m; i++) {bc[(int) b[i]] = i;}}/*** 使用BM算法,查找字符串b在字符串a中出现的首位置。如果没有出现,返回-1.* * @param a* @param b* @return*/public int bm(char[] a, char[] b) {int n = a.length, m = b.length;int[] bc = new int[SIZE];generateBC(b, m, bc);int i = 0;while (i <= n - m) {int j = m - 1;while (j >= 0 && a[i + j] == b[j]) {j--;}if (j == -1)return i;int x = j - bc[(int) a[i + j]]; i = i + x;}return -1;}
特殊情况下比如主串是 aaaaaaaaaaaaaaaa,模式串是 baaaa。模式串不但不会向后移动,还会向前走。这是因为,假设i=0,j从m-1开始匹配,匹配到j=0的时候发生不匹配,坏字符为a,suffix[a]=4,j-suffix[‘a’]=-4,i=i-4。i的值 不断减小。
这个时候就需要第二个规则了。
2.2 好后缀规则
模式串和主串已经匹配好的部分,叫做好后缀。记为{u}。
模式串中可能只有一个好后缀,也有可能包含至少2个好后缀。分两种情况讨论。
2.2.1 模式串中至少包含2个u
我们查找{u},在模式串中是否还有出现。从右向左查找第一个出现{u}的匹配串记为{u*}。将{u*}滑动到主串与{u}匹配的位置。也就是不同于好后缀,但是是最后一次出现u的起始位置。
2.2.2 模式串中只有一个u
如果不能在模式串中找到另外一个u,怎么办?直接滑动到主串{u}的后面,会有过渡滑动的可能性。
介绍几个概念。
字符串s的后缀子串是指 与最后一个字符跟 字符串s 对齐的子串,比如 abc 的后缀子串包括 c, bc。前缀子串是指起始字符与字符串s对齐的子串。abc的前缀子串包括a,ab。
如果好后缀在模式串中不存在可匹配的子串,那在我们一步一步往后滑动模式串的过程中,只要主串中的{u}与模式串有重合,那肯定就无法完全匹配。但是当模式串滑动到前缀与主串中{u}的后缀有部分重合的时候,就有可能会存在完全匹配的情况。
所以,针对这种情况,需要考察好后缀的后缀子串,是否存在跟模式串的前缀子串匹配的。
例如图中第一次发生不匹配的时候,模式串位置j=4,对应主串位置x=7,好后缀是字符串bc。bc在模式串中只出现了一次。如果我们整体向右移动模式串长度 m(=7),那就可能错过一次匹配。因为在这里好后缀bc,有匹配的前缀子串c。将前缀字符串c和好后缀子串c对应起来,再次匹配即可。
2.2.3 好后缀规则编码
首先我们总结好后缀规则。令{u}=好后缀。
如果模式串中包含另外一个子字符串{u*}={u},则找到这个这个{u*}的起始位置,移动。
如果模式串中不包含另外一个子字符串{u*},则找到{u}的最长前缀匹配字符串{v}的起始位置,移动。
先考虑怎么表示一个字符串的后缀。对于所有后缀字符串的结束字符都是一样的。我们可以用不同的长度表示后缀字符串 。用数组suffix[i]表示长度为i的后缀字符串 ,在模式串中出现的起始位置。
用prefix[i]表示长度为i的后缀字符串是否有匹配的前缀子串。
怎么得到这两个数组有编码上的技巧。具体看代码。
2.3 怎么选择坏字符和好后缀规则
我们可以分别计算好后缀和坏字符往后滑动的位数,然后取两个数中最大的,作为模式串往后滑动的位数。
3 BM代码实现
public class BM {// 数据包含的字符范围private static final int SIZE = 256;private void generateBC(char[] b, int m, int[] bc) {Arrays.fill(bc, -1);for (int i = 0; i < m; i++) {bc[(int) b[i]] = i;}}/*** 使用BM算法,查找字符串b在字符串a中出现的首位置。如果没有出现,返回-1.* * @param a* @param b* @return*/public int bm(char[] a, char[] b) {int n = a.length, m = b.length;int[] bc = new int[SIZE];int[] suffix = new int[m];boolean[] prefix = new boolean[m];generateBC(b, m, bc);generatorGS(b, m, suffix, prefix);int i = 0;while (i <= n - m) {int j = m - 1;while (j >= 0 && a[i + j] == b[j]) {j--;}if (j == -1)return i;int x = j - bc[(int) a[i + j]];int y = 0;if(j < m - 1) {y = moveByGS(b, m, j, suffix, prefix);} i = i + Math.max(x, y);}return -1;}/*** 生成后缀子串完整匹配的位置 后缀子串可匹配的最长前缀子串* * @param b* @param m* @param suffix* @param prefix*/private void generatorGS(char[] b, int m, int[] suffix, boolean[] prefix) {Arrays.fill(suffix, -1);for (int i = 0; i < m - 1; i++) {int k = 0;int j = i;while (j >= 0 && b[j] == b[m - 1 - k]) {j--;k++;suffix[k] = j + 1;}if (j == -1) {prefix[k] = true;}}}/*** 按照好后缀规则,当j位置发生比匹配的时候应该移动几步* * @param b* @param j* @param suffix* @param prefix* @return*/private int moveByGS(char[] b, int m, int j, int[] suffix, boolean[] prefix) {int k = m - 1 - j;//好后缀长度if(suffix[k] != -1) return j - suffix[k] + 1;for(int r = j+2; r < m; r++) {if(prefix[m - r] == true) {return r;}}return 0;}public static void main(String[] args) {String a = "abcacabdc";String b = "abd";int postion = new BM().bm(a.toCharArray(), b.toCharArray());System.out.println(postion);}}