数据结构学习笔记 - 字符串匹配

字符串匹配

简介

字符串匹配都不陌生, 例如Java中的indexOf(), Python中的find(), 他们底层就是依赖以下的字符串匹配算法
我们从字符串A中查找字符串B, 则A就是主串, B就是模式串
字符串匹配算法很多, BF和RK比较简单, BM和KMP比较难但更高效, 这些都是单模式串匹配, Trie树和AC自动机可以多模式串匹配

BF算法(Brute Force)

中文叫暴力匹配算法, 也叫朴素匹配算法, 简单好懂但性能不高
例如主串A长度n, 模式串B长度m
核心就是在主串中, 检查起始位置分别是0, 1, 2 … n-m, 且长度为m的全部n-m+1个子串, 看有没有跟模式串匹配的
时间复杂度最坏情况是每次对比m个字符, 对比n-m+1次, 为O(n*m), 看上去时间复杂度很高, 但实际开发中却是一个比较常用的字符串匹配算法
两点原因

  • 大部分情况下, 模式串和主串的长度都不会太长, 且子串不匹配时不需要对比m个字符, 所以实际执行效率还可以
  • 朴素字符串匹配算法思想简单, 代码实现也简单, 不容易出错, 符合KISS(Keep it Simple and Stupid)设计原则, 在满足性能要求的前提下, 简单是首选

RK算法(Rabin-Karp)

名字由来是两位发明者Rabin和Karp, 这个算法也不难, 是上面BF算法的升级版
算法思路是, 通过哈希算法对主串中的n-m+1个子串分别求哈希值, 然后逐个与模式串的哈希值比较大小, 因为哈希值是一个数字, 所以模式串和子串比较的效率就提高了
具体实现起来重点在哈希算法的设计, 假设要匹配的字符串的字符集中只包含k个字符, 我们就可以用一个k进制数来表示一个字符串, 再把这个k进制数转成十进制数, 作为子串的哈希值
再细处不讲了, 用到了自己查
这种哈希算法有一个特点, 在主串中, 相邻两个子串的哈希值的计算公式有交集, 使用前一个子串的哈希值可以很快计算出下一个子串的哈希值
时间复杂度, 计算子串哈希值部分, 扫描一遍主串即可, 所以O(n), 模式串与子串比较的时间复杂度O(1), 一共比较n-m+1个, 所以匹配部分时间复杂度也是O(n), RK算法的整体时间复杂度为O(n)
当模式串很长时, 以上哈希算法结果可能会超过整型数据范围, 则可以适当允许散列冲突的出现, 极端情况时间复杂度会退化成O(n*m), 但一般基本不会出现

BM算法(Boyer-Moore)

它是一种非常高效的字符串匹配算法, 但原理复杂, 比较难懂, 在一些文本编辑器中应用较多
核心思想: BF和RK算法都是遇到不匹配的字符时向后滑动一位继续对比, BM是根据自己的规则, 一次向后滑动好几位, 所以效率就提高了
即当模式串和主串某个字符不匹配时, 能够跳过一些肯定不会匹配的情况, 将模式串往后滑动几位
具体包含两部分:

  1. 坏字符规则(bad character rule)
    在匹配过程中从模式串的末尾往前倒着匹配, 当发现某个字符没法匹配时, 把这个主串中的字符叫做坏字符
    然后拿坏字符在模式串中查找, 若模式串中不存在这个字符, 则可以直接将模式串滑动到这个字符后一位开始比较
    若坏字符在模式串中存在, 则把模式串滑动到坏字符和模式串的最后出现坏字符的位置对齐, 然后开始比较
    利用以上操作, BM算法在最好情况下的时间复杂度非常低, 可以降到O(n/m), 但只靠坏字符规则处理不了全部情况, 所以还需要好后缀规则(更复杂)
  2. 好后缀规则(good suffix shift)
    在匹配过程中可能后三个字符是匹配的, 到第四个字符不匹配了, 那么后三个字符就是好后缀记做u
    拿u在模式串中查找, 如果找到了相匹配的u*, 就把模式串滑动到u与u对齐
    如果没有相匹配的u
    , 但模式串的前缀有可能和u的部分有重合, 则滑动到重合部分对齐
    以上就是两个最重要的规则, 在匹配中遇到字符不匹配时, 分别计算以上两个规则向后滑动的位数, 取两个数中最大的
    代码实现(todo)

KMP算法(Knuth Morris Pratt)

最知名的字符串匹配算法, 出了名的不好懂, 根据三位作者的名字来命名的(D.E.Knuth, J.H.Morris V.R.Pratt)
核心思想和BM非常相近, 在模式串与主串匹配的过程中, 当遇到不可匹配的字符时, 找到一些规律将模式串往后多滑动几位, 跳过肯定不会匹配的情况
不能匹配的那个字符依旧叫坏字符, 已经匹配的前面那段字符串叫作好前缀, 当遇到坏字符, 向后滑动时, 其实就是拿主串中的好前缀的后缀子串模式串的好前缀的前缀子串比较,而主串的好前缀和模式串的好前缀是等价的, 说白了就是拿好前缀本身, 用它自己的后缀子串和前缀子串比较, 查找最长的那个相等的子串, 则这个子串, 在前缀里叫最长可匹配前缀子串, 在后缀里叫最长可匹配后缀子串
而好前缀其实可以不涉及到主串, 单用模式串就能解决, 所以可以进行预处理, 在匹配过程中直接调用
此处提前构造一个数组叫next, 也叫失效函数, 用来存储模式串中每个好前缀的符合上面条件的最长的可匹配前缀子串的最后一个字符的下标
所以next数组下标就等于每个前缀最后一个字符的下标, 也就是好前缀的长度减一, 数组的值就是上面这个最长可匹配前缀子串结尾字符下标
而KMP最复杂的部分, 就是next数组的预处理, 不过next数组里前一个元素和后一个元素是有联系的, 可以快速推导出来, 具体看代码示例及注释
空间复杂度O(m), m为模式串长度, 时间复杂度O(n+m)

Trie树

一个专门处理字符串匹配的树形数据结构, 用来解决在一组字符串集合中快速查找某个字符串的问题
Trie树的本质, 就是利用字符串之间的公共前缀, 将重复的前缀合并在一起, 构造成一颗多叉树, 根节点不存信息, 红色节点表示一个字符串的结尾
Trie数主要有两个操作, 把字符串集构造成Trie树和在Trie树中查询一个字符串
存储的话, 简单点可以用每个节点一个数组, 因为是多叉树, 可能有很多子节点, 对应到数组里的不同元素
但Trie树比较耗内存, 用的是空间换时间的思路, 但确实非常高效, 而对于浪费内存的问题, 可以用有序数组, 跳表, 散列表, 红黑树等代替数组, 牺牲一点查询效率
Trie树不适合精确匹配查找, 更适合查找前缀匹配的字符串, 比如各种自动补全, 比如输入法的联想输入, IDE的自动补全, 浏览器搜索的自动补全等

AC自动机(Aho-Corasick)

AC自动机实际上就是在Trie树上加了类似KMP算法里的next数组, 把next数组构建在了树上, 即失败指针
AC自动机可用于实现高效的敏感词过滤系统, 时间复杂度可近似于O(n), 性能非常高
AC自动机的构建包含两个操作

  • 把多个模式串构建成Trie树
  • 在Trie树上构建失败指针(具体原理和next数组十分相似)
    简单了解, 具体实现看代码示例(todo)

总结

  • BF算法, 简单易懂, 场景简单的情况下推荐使用
  • RK算法, 适用于字符集范围不大, 模式串不太长的情况
  • BM算法, 实现较复杂, 性能好, 编辑器中字符串查找应用较多, 据说最高效最常用
  • KMP算法, 最知名, 也很复杂, 和BM算法类似, 更稳定
  • Trie树, 因为树状结构, 适用于公共前缀较多场景, 比如自动补全, 浏览器预测输入等
  • AC自动机, 能做到大量文本中多模式精确匹配, 适用于敏感词过滤等
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值