------ 本文是学习算法的笔记,《数据结构与算法之美》,极客时间的课程 ------

搜索引擎的搜索关键词提示功能,你应该不陌生吧!当你在搜索引擎的搜索框上,输入要搜索的文字的某一部分的时候,搜索引擎就会自动弹出下拉框,里面是各种关键词提示。你可以直接从下拉框中选择你要搜索的东西。

尽管这个功能我们几乎天天在用,作为一名工程师,你是否思考过,它是怎么实现的呢?它底层使用的是哪一种数据结构和算法呢?

像Google、百度这样的搜索引擎,它人的关键词提示功能非常全面和精准,肯定做了很多优化,但万变不离其宗,底层最基本的原理就是今天要讲的这种数据结构:Trie树

什么是“Trie 树”?

Trie 树也叫“字典树”,这是一个树形结构。它是专门处理字符串匹配的数据结构,用来解决在一组字符串集合中快速查找某个字符的问题。

当然,这样一个问题可以有多种解决方法,比如散列表、红黑树,或者我们前面几节讲到的一些字符串匹配算法,但是,Trie 树在这个问题的解决上,有它特有的优点。不仅如此,Trie 树能解决的问题也不限于此,我们一会儿慢慢分析。

Trie 树到底长什么样子。举个简单的例子,我们有6个字符串,它们分别是:how, hi, her, hello, so, see。我们希望在里面多次查找某个字符串是否存在。如果每次查找,都是要查找的字符串跟这6个字符串依次进行字符串匹配,那效率就比较低,有没有更高效的方法呢?

这个时候,我们就可以先对这6个字符串做五预处理,组织成Trie 树的结构,之后每次查找,都是在Trie 树中进行匹配查找。**Trie 树的本质,就是利用字符串之间的公共前缀,将重复的前缀合并在一起。**最后构造出来的就是下面这个图中的样子。

其中,根节点不包含任何信息。每个节点表示现代战争字符串中的字符,从根节点到红色节点的一条路径表示一个字符串(注意:红色节点并不都是叶子节点)

为了让你更容易理解Trie 树是怎么构造出来的,我画了一个Trie 树的构造分解过程。构造过程的每一步,都相当于往Trie 树中插入一个字符串。当所有的字符串都插入完成之后,Trie 树就构造好了。

当我们在Trie 树中查找一个字符串的时候,比如查找字符串“her”,那我们将要查找的字符串分割成单个的字符h, e, r,然后从Trie 树的根节点开始匹配。如图所示,绿色的就是在Trie 树中匹配的路径。

如果,我们要查找的是字符串"he"呢?我们还用上面两样的方法,从根节点开始,沿着某条路径来匹配,如图所示,绿色的路径,是字符串“he”匹配路径。但是,考核体系的最后一个节点“e”并不是红色的。也就是说,“he”是某个字符串的前缀子串,但不能完全匹配任何字符串。

如何实现一棵Trie 树?

知道了Trie 树长什么样子,我们现在来看下,如何用代码来实现一个Trie 树。

从刚刚Trie 树的介绍来看,Trie 树主要有两个操作,一**个是将字符串集合构成Trie 树。**这个过程分解开来的话,就是一个字符串插入到Trie 树的过程。另一个是在Trie 树中查询一个字符串。

了解了Trie 树的两个主要操作之后,我们再来看下,如何存储一个Trie 树?

从前面的图中,我们可以看出,Trie 树是一个多叉树。我们知道,二叉树中,一个节点的左右节点是通过两个指针来存储的。那对于多叉树来说,我们怎么存储一个节点的所有节点的指针呢?

我先介绍其中一种存储方式,也是经典存储方式,大部分数据结构和算法的书籍中都是这么讲的。还记得我们前面讲的散列表吗?借助散列表的思想,我们通过一个下标与字符一一映射的数组,来存储节点的指针。这句话稍微有点抽象,看图就好理解了。

假设我们的字符串只有从 a 到 z 这26个小写字母,我们在数组中下标为0的位置存储指向子节点 a 的指针,下标为1的位置存储指向子节点 b 的指针,以此类推,下标为25的位置,存储的是指向子节点 z 的指针。如果某个字符的子节点还在,我们就对应的下标的位置存储 null。

class TrieNode{char data;TridNode childNode[26];
}

当我们在Trie 树中查找字符串的时候,我们就可以字符的ASCII码减去 “a” 的 ASCII码,迅速找到匹配的子节点的指针。比如, d 的ASCII码减去 a 的ASCII码就是 3,,那节点 d 的指针就存储在数组中下标为3的位置中。

描述了这么多,有可能还是有点懵,那结合代码看看,有助于理解。

    public class Trie{private TrieNode root = new TrieNode('/');// 往Trie树中插入一个字符串public void insert(char[] text) {TrieNode p = root;for (int i = 0; i < text.length; i++) {int index = text[i] - 'a';if(p.children[index] == null) {TrieNode newNode = new TrieNode(text[i]);p.children[index] = newNode;}p = p.children[index];}p.isEndingChar = true;}// 在Trie树中查找一个字符串public boolean find(char[] pattern) {TrieNode p = root;for (int i = 0; i < pattern.length; i++) {int index = pattern[i] - 'a';if(p.children[index] == null) {return false; // 不存在pattern}p = p.children[index];}if(p.isEndingChar == false) {return false; // 不能完全匹配,只是前缀}else {return true; // 找到 pattern}}public class TrieNode{public char data;public TrieNode[] children = new TrieNode[26];public boolean isEndingChar = false;public TrieNode(char data) {this.data = data;}}}

Trie树的实现,你现在应该搞懂了。现在,我们来看下,在Trie树中,查找某个字符串的时间复杂度是多少?

如果要在一组字符串中,频繁地查询某些字符串,用Trie树是会非常高效。构建Trie树的过程,需要扫描所有的字符串,时间复杂度是O(n)(n表示所有字符串的长度和)。但是一旦构建成功之后,后续的查询操作会非常高效。

每次查询时,如果要查询的字符串长度是 k,那我们只需要比对大约 k 个节点,就能完成查询操作。跟原本那组字符串的长度和个数没有身体倍棒关系。所以说,构建好Trie树后,在其中查找字符串的时间复杂度是O(k),k 表示要查找的字符串的长度。

Trie树真的很耗内存吗?

前面我们讲了Trie树的实现,也分析了时间复杂度。现在你应该知道,Trie树是一种非常独特的、高效的字符串匹配方法。但是,关于Trie树,你有没有听说过一种说法:
“Trie树是非常耗内存的,用的是一种空间换时间的思路”。这是什么原因呢?

刚刚我们在讲Trie树的实现的时候,讲到用数组来存储一个子节点的指针。如果字符串中包含从 a 到 z 这26个字符,那每个节点都要存储一个长度为26的数组,并且每个数组存储一个8字节指针(或者是4字节,这个大小跟CPU、操作系统、编译哭喊等有关)。而且,即便一个节点只有很少的子节点,远小于26个,比如3、4个,我们也要维护一个长度为26的数组。

我们前面讲过,Trie树的本质是愕然重复存储一组字符串的相同前缀子串,但是现在每个字符(对应一个节点)的存储远远大于1个字节。按照我们上面举的例子,数组长度为26,每个元素是8字节,那每个节点就会额外需要26*8=208个字节。而且这还是只包含26个字符的情况。

如果字符串中不仅包含小写字母,还包括大宝字母、数字、甚至是中文,那需要存储空间就更多了。所以也就是说,在某些情况下,Trie树不一定会节省存储空间。在重复前缀并不多的情况下,Trie树不但不能节省内存,还有可能会浪费更多的内存。

当然,我们不可不认,Trie树尽管有可能很浪费内存,但是确实非常高效。那为了解决这个内存问题,我们是否有其他办法呢?

我们可以稍微牺牲一点查询的效率,将每个节点中的数组换成其他数据结构,来存储一个节点的子节点指针。用哪种数据结构呢?其实有很多选择,比如有序数组、跳表、散列表、红黑树等等。

假设我们用有序数组,数组中的指针按照指向的子节点中的字符的大小顺序排列。查询的时候,我们可以通过二分查找的方法,快速查找某个字符应该匹配子节点的指针。但是,在往Trie树中插入一个字符串的时候,我们为了维护数组中的有序性,就会稍微慢了点。

替换成其他数据结构的思路是类似的,这里就不一一分析了。实际上,Trie树的变体有很多,都可以在一定程度上解决内存消耗的问题。比如,缩点优化,就是对只有一个子节点的节点,而且此节点不是一个串的结束点,可以将些节点与子节点合并。这样可以节省空间,但却增加了编码的难度。这里就不展开讲解了。

Trie树与散列表、红黑树的比较

实际上,字符串匹配的问题,笼统上讲,其实就是数据查找的问题。对于支持动态数据高效操作的数据结构,我们前面已经讲过好多了,比如散列表、红黑树、跳表等等。这些结构也可以实现在一组字符串中查找字符串的功能。我们选了两种数据结构,散列表和红黑树,跟Trie树比较一下,看看它们各自的优缺点和应用场景。

在刚刚讲的这个场景,在一组字符串中查找字符串,Trie树实际上表现的并不好。它对要处理的字符串有及其严苛的要求。

第一,字符串中包含的字符集不能太大。我们前面讲到,如果字符集太大,那存储空间可能会浪费很多。即便可能优化,但也要付出牺牲查询、插入效率的代价。

第二,要示字符串的前缀重合比较多,不然空间消耗会变大很多。

第三,如果要用Trie树解决问题,那我们就要自己从0开始实现一个Trie树。除非必须,一般不建议这样做。

第四,通过指针串起来的数据是不连续的,而Trie树用到了指针,对缓存并不友好,性能上会打个折扣。

综合这几点,针对在一组字串中查找字符串的问题,我们在工程中,更倾向于用散列表或者红黑树。因为这两种数据结构,我们都不需要自己去实现,直接利用编程语言中提供的现成类库就行了。

实际上,Trie树只是不适全精确匹配查找,这种问题更适合用散列表或者红黑树来解决。Trie树比较适合的是查找前缀匹配的字符串,也就是类似开篇问题的那种场景。

解答开篇

如何利用Trie树实现搜索关键词的提示功能?

我们假设关键词库由用户的热门搜索关键词组成。我们将这个词库构建成一个Trie树。当用户输入其中某个单词的时候,把这个词作为一个前缀子串在Trie树中匹配。为了讲解方便,我们假设词库里只有how, hi, her, hello, so, see这6个关键词。当用户输入了字母 h 的时候,我们把以 h 为前缀的 how, hi, her, hello展示在搜索提示框肉。当用户继续键入字母 e 的时候,我们就把以he为前缀的 hello、her展示在搜索提示框内。这就是搜索关键词提示的最基本的算法原理。

实际上,搜索引擎的搜索关键词提示功能无非我讲的这么简单。如果再稍微深入一点,你就会想到,上面的解决办法会遇到下面几个问题:

  • 对于中文来说,词库中的数据又该如何构建成Trie 树呢?
  • 如果词库中有很多关键词,在搜索提示的时候,用户输入关键词,作为前缀在Trie 树中可以匹配的关键词也有很多,如何选择展示哪些内容呢?
  • 像Google这样的搜索引擎,用户单词拼写错误的情况下,Google还是可以用正确的拼写来做关键词提示,这个又是怎么做到的呢?

这些问题,我们会在实战篇里讲。

Trie 树:如何实现搜索引擎的搜索关键词提示功能相关推荐

  1. 35.Trie树:如何实现搜索引擎的搜索关键词提示功能

    文章目录 1. 什么是"Trie树"? 2. 如何实现一棵Trie树? 3.Trie树真的很耗内存吗? 4.Trie树与散列表.红黑树的比较 5. 解答开篇 问题:搜索引擎的关键词 ...

  2. Trie 树——搜索关键词提示

    当你在搜索引擎中输入想要搜索的一部分内容时,搜索引擎就会自动弹出下拉框,里面是各种关键词提示,这个功能是怎么实现的呢?其实底层最基本的就是 Trie 树这种数据结构. 1. 什么是 "Tri ...

  3. Trie 树——搜索关键词提示 1

    当你在搜索引擎中输入想要搜索的一部分内容时,搜索引擎就会自动弹出下拉框,里面是各种关键词提示,这个功能是怎么实现的呢?其实底层最基本的就是 Trie 树这种数据结构. 1. 什么是 "Tri ...

  4. python字典实现关键字检索_如何实现搜索框的关键词提示功能

    我们都使用过主流的搜索引擎,谷歌. bing,当然还有搜狗.百度之类.当你搜索某一关键词时,它会贴心在下拉框补全一些热门关键词,像下图这样: 搜索关键词提示 你点击某一关键词,页面就直接跳转到结果页面 ...

  5. android搜索框功能实现_巧用 Trie 树,实现搜索引擎关键词提示功能

    来源 | 码海责编 | Carol封图 | CSDN 付费下载于视觉中国我们几乎每天都在用搜索引擎搜索信息,相信大家肯定有注意过这样一个细节:当输入某个字符的时候,搜索引框底下会出现多个推荐词,如下, ...

  6. Trie(字典树) : 如何实现搜索引擎的关键词提示功能?

    文章目录 搜索中的关键词提示 Trie树的介绍 Trie树的实现 搜索的关键词提示 Trie树 VS 红黑树.哈希 搜索中的关键词提示 当我们在搜索引擎中进行搜索时,有时仅仅输入了搜索内容的一部分,搜 ...

  7. 巧用 Trie 树,实现搜索引擎关键词提示功能

    来源 | 码海 责编 | Carol 封图 | CSDN 付费下载于视觉中国 我们几乎每天都在用搜索引擎搜索信息,相信大家肯定有注意过这样一个细节:当输入某个字符的时候,搜索引框底下会出现多个推荐词, ...

  8. 站长工具|百度搜索框提示功能

    百度向站长开放免费"百度搜索框"代码和"百度搜索框提示"代码.只需进行简单的设置, 即可将" 百度搜索框( 带提示功能)"功能快速加入到您的 ...

  9. ajax实现搜索提示源码,Jquery实现搜索框提示功能示例代码

    博客的前某一篇文章中http://www.jb51.net/article/35175.htm写过一个用Ajax来实现一个文本框输入的提示功能.最近在一个管理项目的项目中,使用后发现,真的反应很慢,数 ...

最新文章

  1. 2022-2028年中国盲盒产业研究及前瞻分析报告
  2. python csv性能_性能:Python pandas DataFrame.to_csv附加逐渐变慢
  3. BRCM5.02编译一 : 缺少工具链路
  4. Docker教程-安装
  5. [Head First设计模式]生活中学设计模式——组合模式
  6. java 枚举源码解析
  7. 判断是否离开当前页面
  8. Hbase权威指南(含目录,高清,免费)
  9. 对Python中yield的理解
  10. ACAD shx字体格式之BigFont
  11. 拓扑排序算法分析(通俗易懂)
  12. Windows密码复杂性要求
  13. 迈向高算力、跨域融合新拐点,智能座舱各路玩家如何卡位?
  14. 为什么计算机能读懂 1 和 0 ?
  15. web性能压力测试工具:WebBench详解
  16. 从此爱情与我无关,只做一个嗜钱如命的渣男!
  17. 【老生谈算法】matlab实现RSA算法源码——RSA算法
  18. ZjDroid--脱壳神器介绍
  19. 什么是服务端与客户端详解
  20. python 网格交易源码_Python版简单网格策略

热门文章

  1. Wampserver安装提示没有找到 msvcp120.dll mysql.exe
  2. 虚幻4皮肤材质_PBR:应用于虚幻引擎4贴图和材质创建的启示 - 纳金网
  3. 用python写父亲节祝福代码_父亲节,程序员几条代码硬核示爱
  4. C# 打印Label的两种方式及实现
  5. 书论10 王羲之《题卫夫人<笔阵图>后》
  6. linux下无对应分辨率,关于linux 下显示分辨率问题1440*900?
  7. 安全联盟:双12购物需认准网站认证标识
  8. 堆积如山:探索数据结构中的堆
  9. 虚幻浏览器插件 调试网页
  10. SABSA企业安全架构-白皮书