文章目录

  • 搜索中的关键词提示
  • Trie树的介绍
  • Trie树的实现
  • 搜索的关键词提示
  • Trie树 VS 红黑树、哈希

搜索中的关键词提示


当我们在搜索引擎中进行搜索时,有时仅仅输入了搜索内容的一部分,搜索引擎就会提示我们可能的一些选择,这样我们就不再需要将查询词完整的输入,大大节约了我们的时间。

而实现这一功能的基石,正是Trie树


Trie树的介绍

Trie树又叫做字典树、前缀树。顾名思义,它是一个用于处理多模式字符串匹配的多叉树,用来在一组字符串中快速的找到某个字符串。

其本质就是共享字符串的公共前缀,即利用字符串之间的公共前缀,将重复的前缀合并在一起。

对于每一个节点来说,它的每个子节点就代表着一种字符,例如我们插入一个hi,其就会在根节点寻找是否存在h节点,如果没有则新建一个,接着进入h节点,寻找是否存在i,如果没有也新建一个。接着到了i之后,当前字符串插入结束,为了标识hi是一个完整的单词而不是前缀,就会在该节点中做一个标记。

如下图,我们往里面插入几个字符串,插入流程如下。


此时,如果我们想在里面寻找是否存在hello

此时找到了这个单词,并且结束的位置o上存在标记,说明这时一个完整的单词,查找成功

接着查找se

虽然找到了se,但是此时并不存在标记,所以当前的se是前缀也不是单词,查找失败。


Trie树的实现

了解了思路后下面就可以开始实现了

首先进行数据结构的选择

我们从上面描述的原理可以得知,由于Trie树需要标记多种不同的字符,所以不可能是二叉树,而是一个多叉树,所以我们就需要对子节点的数据结构进行一个选择

我们可以选用有序数组、哈希表、红黑树、跳表等数据结构,例如我们的字典树中只存在英文字母的时候,就可以选择用一个大小为26的数组来存储,例如以下结构,这时很常见的一种实现方法。

但是如果想要实现一个关键词提示功能,字符的范围绝不限制于字母,还可能有数字、标点符号、各种语言等,所以对于子数组的大小,我们无法得知。

这里我选择使用哈希表来完成,哈希表可以动态扩容,我们就不必关心字符的种类数量,并且可以用O(1)的时间复杂度来找到一个字符是否存在,大大的提高了效率。

struct TrieNode
{char _data;        //当前字符bool _isEnd;  //标记当前是否能构成一个单词unordered_map<char, TrieNode*> _subNode;   //子节点
};

代码实现如下,具体的实现思路写在了注释中。

#include<unordered_map>
#include<string>
#include<vector>using std::vector;
using std::string;
using std::unordered_map;
using std::make_pair;//Trie树节点
struct TrieNode
{TrieNode(char data = '\0'): _data(data), _isEnd(false){}char _data;     //当前字符bool _isEnd;  //标记当前是否能构成一个单词unordered_map<char, TrieNode*> _subNode;   //子节点
};//Trie树
class TrieTree
{public:TrieTree(): _root(new TrieNode()){}~TrieTree(){delete _root;}//防拷贝TrieTree(const TrieTree&) = delete;TrieTree& operator=(const TrieTree&) = delete;//插入字符串void insert(const string& str){if (str.empty()){return;}TrieNode* cur = _root;for (size_t i = 0; i < str.size(); i++){//如果找不到该字符,则在对应层中插入if (cur->_subNode.find(str[i]) == cur->_subNode.end()){cur->_subNode.insert(make_pair(str[i], new TrieNode(str[i])));}cur = cur->_subNode[str[i]];}//标记该单词存在cur->_isEnd = true;}//查找字符串bool find(const string& str){if (str.empty()){return false;}TrieNode* cur = _root;for (size_t i = 0; i < str.size(); i++){if (cur->_subNode.find(str[i]) == cur->_subNode.end()){return false;}cur = cur->_subNode[str[i]];}//如果当前匹配到的是一个前缀而非一个完整的单词,则返回错误return cur->_isEnd == true ? true : false;}//查找是否存在以包含str为前缀的字符串bool startsWith(const string& str){if (str.empty()){return false;}TrieNode* cur = _root;for (size_t i = 0; i < str.size(); i++){if (cur->_subNode.find(str[i]) == cur->_subNode.end()){return false;}cur = cur->_subNode[str[i]];}return true;}//返回所有以str为前缀的字符串vector<string> getPrefixWords(const string& str){if (str.empty()){return {};}TrieNode* cur = _root;for (size_t i = 0; i < str.size(); i++){if (cur->_subNode.find(str[i]) == cur->_subNode.end()){return {};}cur = cur->_subNode[str[i]];}vector<string> res;string s = str;_getPrefixWords(cur, s, res);return res;}private://使用回溯来找到所有包含该前缀的字符串void _getPrefixWords(TrieNode* cur, string& str, vector<string>& res){//如果当前能构成一个单词,则加入结果集中if (cur->_isEnd){res.push_back(str);}//匹配当前所有可能字符for (const auto& sub : cur->_subNode){str.push_back(sub.first);    //匹配当前字符_getPrefixWords(sub.second, str, res);  //匹配下一个字符str.pop_back();    //回溯,尝试其他结果}}TrieNode* _root;    //根节点,存储空字符
};

下面进行一个简单的测试,演示一下上面实现的所有函数

#include<iostream>
#include"TrieTree.hpp"using namespace std;int main()
{TrieTree trie;trie.insert("hello");trie.insert("helo");trie.insert("hill");trie.insert("world");trie.insert("test");trie.insert("cpp");trie.insert("我");trie.insert("我爱学习");trie.insert("我爱C++");trie.insert("你");//测试中文,查找以我为前缀的所有字符串for (auto str : trie.getPrefixWords("我")){cout << str << endl;}//测试英文,查找以h为前缀的所有字符串for (auto str : trie.getPrefixWords("h")){cout << str << endl;}cout << trie.find("我") << endl;cout << trie.find("它") << endl;cout << trie.find("cpp") << endl;cout << trie.find("java") << endl;cout << trie.startsWith("h") << endl;return 0;
}


搜索的关键词提示

回到我们开头说的那种情景,在我们往搜索引擎中输入字符串的过程时,它就会把这个词作为一个前缀子串在Trie树中匹配,并且找到所有满足条件的结果,这也就是我们实现的getPrefixWords函数的功能,这就是搜索关键词提示的最基本的算法原理。

但是在搜索引擎背后的技术,不仅仅只有这么简单。当我们在搜索时,即使我们不以前缀输入,而是输入其中的一个片段,又或者我们输入的查询词错误,他也会校正后返回正确的结果,这又是如何做到的呢?我们是否能将这个功能更加广泛化,如实现编译器、输入法的自动补全等?

在这里就先挖一个坑,或许未来我会写一篇有关这方面的博客来具体讲一讲它们背后的原理。

如果想要了解搜索引擎的简单原理,可以参考我的往期博客,这是C++实现的一个简单的站内搜索引擎。
【项目介绍】搜索引擎


Trie树 VS 红黑树、哈希

通常来说,在查找问题上我们都会使用红黑树和哈希等数据结构,那么和他们比起来,Trie树有什么优势吗?
从上面的实现可以看出,构建一个Trie树的时间复杂度为O(N * M)(N为字符串数,M为字符串长度,这里可直接视为总字符数),而字符串的查找时间为O(M)

虽然这个效率确实挺高,但是比起上述的数据结构,并没有什么突出的地方,并且Trie树还有以下几种严重的缺点

缺点

  • 内存消耗大,从上面可以看出来,Trie树是典型的以时间换空间的做法,为了维护每一个节点的子节点花费了大量的空间。
  • 要求字符串的前缀重合多,否则为了维护子节点消耗的空间会变多
  • 常见的语言如JAVA、C++库中都实现了红黑树、哈希表,而没有实现Trie树,所以需要自己实现

按照上面所描述的,难道Trie真的那么无用吗?错了,Trie树只是不适合那种精确的匹配查找,它的优势在于查找前缀匹配的字符串,也就是我们开头的那种场景。

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

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

    ------ 本文是学习算法的笔记,<数据结构与算法之美>,极客时间的课程 ------ 搜索引擎的搜索关键词提示功能,你应该不陌生吧!当你在搜索引擎的搜索框上,输入要搜索的文字的某一部分 ...

  2. dfa算法c语言,DFA跟trie字典树实现敏感词过滤(python和c语言)

    DFA和trie字典树实现敏感词过滤(python和c语言) 现在做的项目都是用python开发,需要用做关键词检查,过滤关键词,之前用c语言做过这样的事情,用字典树,蛮高效的,内存小,检查快. 到了 ...

  3. C语言实现Trie字典树 (附完整源码)

    实现Trie字典树 TrieNode结构体 实现实现Trie字典树的完整源码(定义,实现,main函数测试) TrieNode结构体 typedef struct TrieNode {struct T ...

  4. POJ3764-The xor-longest Path【Trie(字典树)】

    正题 POJ题目链接:http://poj.org/problem?id=3764 其实loj也有题目:https://loj.ac/problem/10056 题目大意 一棵树,求两个点使他们的之间 ...

  5. 提高篇 第二部分 字符串算法 第3章 Trie字典树

    Trie(字典树)解析及其在编程竞赛中的典型应用举例 - Reqaw - 博客园 『一本通』Trie字典树 - YeLingqi - 博客园 字典树(Trie Tree) - 仰望高端玩家的小清新 - ...

  6. Trie(字典)树详解

    什么是Trie(字典)树 顾名思义,这棵树和字典类似,通过百度我们又能知道Trie树是一种树形结构,是一种哈希树的变种.典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引 ...

  7. 数据结构与算法(十一)Trie字典树

    本文主要包括以下内容: Trie字典树的基本概念 Trie字典树的基本操作 插入 查找 前缀查询 删除 基于链表的Trie字典树 基于Trie的Set性能对比 LeetCode相关线段树的问题 Lee ...

  8. LeetCode 208 实现 Trie (字典树)

    208 实现 Trie (字典树) 题目: 实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作. 示例: Trie trie = new Tri ...

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

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

最新文章

  1. Java——重载和重写
  2. 事件控制寄存器(AFIO_EVCR) (2011-09-09 13:51:58)
  3. Python正则表达式之零宽断言(4)
  4. CUBRID学习笔记 31 通过select创建表
  5. 海豚php表格,表格选取(1.4.3+) · DolphinPHP1.5.0完全开发手册-基于ThinkPHP5.1.41LTS的快速开发框架 · 看云...
  6. 界面设计方法(2) — 2.界面的布局
  7. 10010序列检测器的三段式状态机实现(verilog)
  8. oracle注射,中国联通沃支付一处Oracle数据库注射
  9. mysql查询重复名字的数据都查出来_mysql 查询重复字段 内容
  10. 计算机科学理论数学研讨会,2017年奇异摄动理论及其应用学术研讨会会议-上海交通大学数学系.DOC...
  11. opencv物体识别-识别水果
  12. TypeError: xx takes 1 positional argument but 4 were given
  13. html中如何实现倒计时
  14. 冒泡排序 python内置_除了冒泡排序,你知道Python内建的排序算法吗?
  15. 项目管理之项目的挣值计算问题
  16. 动态合批和静态合批的区别
  17. Java语言的特性和优点
  18. 为sourceinsight添加makefile、kco…
  19. self.font = core.getfont(font, size, index, encoding, layout_engine=layout_engine) OSError: cannot o
  20. Linux C获取本机IP

热门文章

  1. 设计模式之_Iterator_01
  2. html中加入一个计时器,向html中的计时器添加毫秒
  3. JavaFX 中 FX 一词的由来
  4. Zookeeper的一次迁移故障
  5. Anconda安装教程
  6. 架设mediawiki服务器续--安装VisualEditor编辑器
  7. StackOverflow 每月5.6亿PV,但只用25台服务器
  8. Wikioi 1081 线段树成段更新单点查询
  9. Hibernate中二级缓存配置
  10. Linux学习笔记033_8文本处理