33.讲字符串匹配基础(中):如何实现文本编辑器中的查找功能

问题:对于查找功能是重要功能的软件来说,比如一些文本编辑器,它们的查找功能都是用哪种算法来实现的呢?有没有比BF算法和RK算法更加高效的字符串匹配算法呢?

1. BM算法的核心思想

跳过肯定不会匹配的字符串。如下:
在这里插入图片描述
在这里插入图片描述

2. BM算法原理分析

不同其他的匹配算法,BM是从后往前匹配

2.1坏字符规则 bad character rule

在这里插入图片描述
在这里插入图片描述

滑动规律:

在这里插入图片描述
BM算法在最好情况下的时间复杂度非常低,是O(n/m)。比如,主串是aaabaaabaaabaaab,模式串是aaaa。

注意:仅仅坏字符规则是不够的,因为有可能出现si-xi为负数,比如主串aaaaaab, 模式串caa,si为0,xi为2,0-2为-2。

2.2 好后缀规则(good suffix shift)

在这里插入图片描述
在这里插入图片描述
注意:如果{u}在模式串中匹配不到,应该滑动多少距离,如下说明。
在这里插入图片描述
在这里插入图片描述

2.3 两种规则怎么选

分别计算两种规则得到的滑动位数,取较大的一个作为模式串滑动的位数。还可以避免坏字符滑动位数为负数的情况。

3. BM算法代码实现

3.1如何计算坏字符在模式串的下标

si - xi,xi是坏字符在模式串的下标,如何计算? 可以用散列表模式串的每个字符和下标的关系。
注: 如果存在相同的字符,散列表只记录最靠后的下标。

这里实现最简单的情况, 字符集不大,每个一个字节,用大小为256的数组表示,下标表示对应的ASCII码,存储字符在模式串的索引位置。
在这里插入图片描述

private static final int SIZE = 256; // 全局变量或成员变量
// 变量b是模式串,m是模式串的长度,bc表示刚刚讲的散列表。
private void generateBC(char[] b, int m, int[] bc) {
  for (int i = 0; i < SIZE; ++i) {
    bc[i] = -1; // 初始化bc
  }
  for (int i = 0; i < m; ++i) {
    int ascii = (int) b[i]; // 计算b[i]的ASCII值
    bc[ascii] = i;
  }
}

3.2 BM算法的大框架

不考虑好后缀规则,仅考虑坏字符规则,并且不考虑si - xi 可能为负数。

public int bm(char[] a, int n, char[] b, int m) {
  int[] bc = new int[SIZE]; // 记录模式串中每个字符最后出现的位置
  generateBC(b, m, bc); // 构建坏字符哈希表
  int i = 0; // i表示主串与模式串对齐的第一个字符
  while (i <= n - m) {
    int j;
    for (j = m - 1; j >= 0; --j) { // 模式串从后往前匹配
      if (a[i+j] != b[j]) break; // 坏字符对应模式串中的下标是j
    }
    // 注意上面是--j,所以这里j可能是负数
    if (j < 0) {
      return i; // 匹配成功,返回主串与模式串第一个匹配的字符的位置
    }
    // 这里等同于将模式串往后滑动j-bc[(int)a[i+j]]位
    // bc[(int) a[i+j]] 表示xi位置,j就是si位置
    i = i + (j - bc[(int) a[i+j]]); 
  }
  return -1;
}

在这里插入图片描述

3.3 如何处理“好后缀规则”

两个核心:

  • 模式串中,查找跟好后缀匹配的另一个子串
  • 好后缀子串中,查找最长的,能跟模式串的前缀子串匹配的好后缀子串
    在这里插入图片描述
    求解上图的两个数组:
    在这里插入图片描述
// b表示模式串,m表示长度,suffix,prefix数组事先申请好了
private void generateGS(char[] b, int m, int[] suffix, boolean[] prefix) {
  for (int i = 0; i < m; ++i) { // 初始化
    suffix[i] = -1;
    prefix[i] = false;
  }
  for (int i = 0; i < m - 1; ++i) { // b[0, i]
    int j = i;
    int k = 0; // 公共后缀子串长度
    while (j >= 0 && b[j] == b[m-1-k]) { // 与b[0, m-1]求公共后缀子串
      --j;
      ++k;
      suffix[k] = j+1; //j+1表示公共后缀子串在b[0, i]中的起始下标
    }
  
    if (j == -1) prefix[k] = true; //如果公共后缀子串也是模式串的前缀子串
  }
}

3.4 有3.3的两个数组后,如何根据好后缀规则滑动位数

首先:看suffix
在这里插入图片描述
其次,如果suffix[k] == -1
在这里插入图片描述
最后,如果上述都不满足,滑动m位,

在这里插入图片描述

3. 5 完整代码

// a,b表示主串和模式串;n,m表示主串和模式串的长度。
public int bm(char[] a, int n, char[] b, int m) {
  int[] bc = new int[SIZE]; // 记录模式串中每个字符最后出现的位置
  generateBC(b, m, bc); // 构建坏字符哈希表
  
  int[] suffix = new int[m];
  boolean[] prefix = new boolean[m];
  generateGS(b, m, suffix, prefix);
  
  int i = 0; // i表示主串与模式串匹配的第一个字符
  while (i <= n - m) {
    int j;
    for (j = m - 1; j >= 0; --j) { // 模式串从后往前匹配
      if (a[i+j] != b[j]) break; // 坏字符对应模式串中的下标是j
    }
    if (j < 0) {
      return i; // 匹配成功,返回主串与模式串第一个匹配的字符的位置
    }
    int x = j - bc[(int)a[i+j]];
    int y = 0;
    if (j < m-1) { // 如果有好后缀的话
      y = moveByGS(j, m, suffix, prefix);
    }
    i = i + Math.max(x, y);
  }
  return -1;
}

// j表示坏字符对应的模式串中的字符下标; m表示模式串长度
private int moveByGS(int j, int m, 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-1; ++r) {
    if (prefix[m-r] == true) {
      return r;
    }
  }
  return m;
}

4. BM算法的性能分析及优化

  • 内存分析

额外使用三个数组,bc数组的大小跟字符集大小有关,suffix数组和prefix数组的大小跟模式串长度m有关。

如果字符集很大,bc数组消耗内存比较大, 如果运行环境对内存比较苛刻,可以只使用好后缀规则(可以独立于坏字符规则),相应的效率降低。

  • 执行效率

目前的版本只是初级版本,极端情况下,预处理计算suffix数组、prefix数组的性能会比较差。比如模式串是aaaaaaa这种包含很多重复的字符的模式串,预处理的时间复杂度就是O(m^2)。

延伸阅读:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值