这里聊一聊在搜索中常用的字符串搜索使用的一些数据结构
搜索中的字符串需求的场景有哪些呢
- 提示词需求,用户输入一个字符串的一部分,给用户提示更加完整的字符串
- 字符串完整匹配,一般用在分词后的词典的匹配,用于召回用户搜索相关的内容
- 敏感词过滤,运营方会提供一个敏感词列表,如果用户的query中包含敏感词则不再进行召回,也有可能是要将召回的内容进行过滤,去除那些包含敏感词的内容
前两个场景更加相似,是在多个单词中查找某一个单词。当然提示词实际上是在多个有位置关系的单词中查找,这个在后面学习lucene的数据结构的时候在重点学习一下
第3个场景是在文本内容中查找多个可能存在的词汇,是在字符串中进行多个模式的匹配。
下面总结一些常见的数据结构,看能否对上面的数据进行支撑。
数据结构 | 优缺点 |
---|---|
排序列表Array/List | 使用二分法查找,只能完成搜索场景中的提示词+字符完整匹配,连续的大内存很难分配 |
HashMap/TreeMap | 性能高,内存消耗大,几乎是原始数据的三倍, 只能完成搜索场景中的字符完整匹配 |
Skip List | 跳跃表,可快速查找词语,在lucene、redis、Hbase等均有实现。相对于TreeMap等结构,特别适合高并发场景(Skip List介绍),能完成搜索场景中的提示词+字符完整匹配 |
Trie | 适合英文词典,对前缀进行了很好的压缩,如果系统中存在大量字符串且这些字符串基本没有公共前缀,则相应的trie树将非常消耗内存(数据结构之trie树),能够支持搜索场景中的三种搜索模式 |
AC-Trie | AC-Trie 相对于Trie来说,是引入了快速回退的字典表,实际上是在trie上构建的一个有限状态自动机,能够快速进行多个模式串的匹配,帮助适合英文词典,如果系统中存在大量字符串且这些字符串基本没有公共前缀,则相应的trie树将非常消耗内存(数据结构之trie树),能够支持搜索场景中的三种搜索模式 |
Double Array Trie | 适合做中文词典,使用了两个数组来构建trie树,一般要求提前知道有哪些字典串(否则容易产生地址冲突),内存占用小,查找性能接近基于多个数组实现的trie,很多分词工具均采用此种算法 |
Finite State Transducers (FST) | 一种有限状态转移机,Lucene 4有开源实现,并大量使用,主要是为了能够节约存储,相对Trie,不仅对前缀进行了压缩,还对后缀进行了压缩 |
在搜索中实际使用的基本上就是上面的几个数据结构按照场景选择即可
同时单模式查找最常见的算法就是BM,KMP算法了,在这里也一并回顾一下。
其实想要尽快在一个字符串中判断模式串是否存在的话,只能找规律,看看在每次匹配失败的时候不用像BF算法那样只移动一个字符然后从头匹配,进而提升算法效率。
BM算法:
BM算法听说在实际测试中比KMP更加高效
BM匹配的规则是:
- 从模式串的后端往前逆序匹配。
- 匹配失败的时候(无法匹配的主串的字符叫坏字符,已经匹配的叫好后缀)按照 max(坏字符rule,好后缀rule)进行移动
- 坏字符rule:分析模式串,每个字符进行索引,当遇到坏字符的时候,查找模式串中是否有该字符,没有的话直接将模式串的开始移动到坏字符的后一位,有的话则将模式串中对应的字符移动到坏字符的位置,然后重新开始进行匹配。
- 好后缀规则:对模式串进行分析,针对每个后缀(字符串尾部的一段),查找前面是否有重复的字符串,如果有的话就可以将重复的字符串移动过来对齐即可,如果没有的话,看从头开始的字符串是否有和当前后缀的子后缀完全匹配的字符串,有的话就可以进行移动对齐操作,没有的话则是将模式串的第一个字符直接移动到原来模式串结尾所在位置的后面 ,这个也需要对模式串进行预处理。需要有suffix,prefix数组,使用贪婪算法就可以相对容易的解决这个问题。
KMP算法:
KMP不知道为啥这么出名,可能是因为出道比较早的原因吧
KMP的匹配规则是:
- 从模式串的开始往后进行顺序匹配
- 遇到无法匹配的字符,将主串的该字符称为坏字符,前面已经匹配的字符串为好前缀,使用好前缀规则,将模式串进行后移。
- 好前缀规则: 对模式串进行分析,假设一个前缀strA已经匹配了,随后遇到了一个坏字符charB,那么此时可以知道模式串必须进行移动了,那么最长可以移动多少呢,就看strA的前缀和后缀重合的最大长度即可,最长可匹配子前缀进行移动,移动到对应的后缀的位置。如果strA的前后缀没有重合的,那么整个模式串直接移动到模式串的第一个字符与charB对齐即可。难点就是在于构建回退的next[]数组,以便于在匹配失败的时候能够快速回退。
你不需要精通所有的,只需要更加专注和聚焦你擅长的工作即可