单词查找树,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。Trie可以看作是一个确定有限状态自动机(DFA)。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。 Trie这个术语来自于retrieval。根据词源学。本博文主要讲解了单词查找树的相关知识及其实现。

  如下所示为一棵单词查找树:

与二叉查找树不同,Trie树的键不是直接保存在节点中,而是由节点在树中的位置决定。

对于Trie树它有3个基本性质:

  1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符。

  2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。

  3. 每个节点的所有子节点包含的字符都不相同。

Trie树效率分析:

Trie树优点是最大限度地减少无谓的字符串比较,查询效率比较高。核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

  1. 插入、查找的时间复杂度均为O(M),其中M为字符串长度。
  2. 对于英文字母的字典树,其空间复杂度是26^n 级别的数字的字典树是10^n 级别的,非常庞大。

Trie树创建:

Trie树的创建要考虑的是父节点如何保存孩子节点,主要有链表和数组两种方式:

  1. 使用节点数组,因为是英文字符,可以用Node[26]来保存孩子节点(如果是数字我们可以用Node[10]),这种方式最快,但是并不是所有节点都会有很多孩子,所以这种方式浪费的空间太多

  2. 用一个链表根据需要动态添加节点。这样我们就可以省下不小的空间,但是缺点是搜索的时候需要遍历这个链表,增加了时间复杂度。

  3. 可以结合hash表来存储其对应的孩子节点,以孩子节点的值为键,指向孩子节点的指针为值。这样在理论上可以做到时间复杂度为O(1)的情况,且空间的浪费程度不高。

Trie树实现:

  对于trie树,其使用得较频繁点的功能是查找和插入,为此,此处主要讲解Trie树的插入和查找功能。

在讲解其相应的功能之前,我们先定义Trie树节点类:

class TrieNode{
/**
* 该节点的值
*/
String value;
/**
* 该节点的孩子节点
*/
Map<String,TrieNode> children;
/**
*用于判断该孩子节点是否为最终节点,即对应的单词的末尾字符
*/
boolean isEnd;
public TrieNode(){
this(null);
}
public TrieNode(String value){
this(value,false);
}
public TrieNode(String value,boolean isEnd){
this.value=value;
this.isEnd=isEnd;
children=new HashMap<String,TrieNode>();
} /**
* 用于设置当前节点的值
* @param value 当前节点的值
*/
public void setValue(String value){
this.value=value;
} /**
* 用于获取其当前节点的值
* @return 当前节点值
*/
public String getValue(){
return this.value;
}
/**
* 用于设置当前节点的字符是否为字符串的最后一个字符
* @param isEnd true表示当前字符为字符串的最后一个字符
*/
public void setIsEnd(boolean isEnd){
this.isEnd=isEnd;
} /**
* 用于获取其当前节点是否为单词末尾字符的判断
* @return 返回其对应的结果
*/
public boolean getIsEnd(){
return this.isEnd;
} /**
* 用于添加当前节点的孩子节点
* @param value 孩子节点的值
* @param node 指向当前节点的孩子节点的指针
*/
public void addChild(String value,TrieNode node){
children.put(value,node);
} /**
* 用于获取当前节点的孩子节点
* @param value 孩子节点的值
* @return 其对应的孩子节点的指针
*/
public TrieNode getChild(String value){
return children.get(value);
}
}
  1. 插入:

  对于插入操作,无非是逐一把单词的每个字符插入到对应前缀的后面,使其成为该前缀的孩子节点。需要注意的是,在插入之前,先查看前缀是否存在,如果存在就共享,否则穿件对应的节点和边。

其插入操作的相关代码如下:

public void add(String value){
TrieNode node=head;
for(int i=0;i<value.length();i++){
String v=String.valueOf(value.charAt(i));
TrieNode child=node.getChild(v);
//该字符并没有在相应的孩子节点中
if(child==null){
child=new TrieNode(v);
node.addChild(v,child);
}
//当其为该单词的最后一个字符时
if(i==value.length()-1){
child.setIsEnd(true);
}
node=child;
}
}
  1. 查找:

  对于查找操作而言,其较为简单,只需要沿着字典树的链接,从上往下查找即可。

具体代码如下:

 /**
* 用于判断其对应单词是否在其对应的字典树中
* @param value 需要查找的单词
* @return 是否在字典树中的判断
*/
public boolean search(String value){
TrieNode node = head;
for(int i=0;i<value.length();i++){
String v=String.valueOf(value.charAt(i));
TrieNode child=node.getChild(v);
if(child==null){
return false;
}
//当为最后一个字符,且其不为截止字符的时候,表示该单词不在字典树中
if(i==value.length()-1&&!child.getIsEnd()){
return false;
}
node=child;
}
return true;
}

完整代码如下:

import java.util.HashMap;
import java.util.Map; /**
* @author 学徒
* 用于实现Trie树
*/
public class Trie {
/**
* 该trie树的根节点
*/
private TrieNode head=new TrieNode();
/**
* Trie树对应的节点类
*/
private class TrieNode{
/**
* 该节点的值
*/
String value;
/**
* 该节点的孩子节点
*/
Map<String,TrieNode> children;
/**
*用于判断该孩子节点是否为最终节点,即对应的单词的末尾字符
*/
boolean isEnd;
public TrieNode(){
this(null);
}
public TrieNode(String value){
this(value,false);
}
public TrieNode(String value,boolean isEnd){
this.value=value;
this.isEnd=isEnd;
children=new HashMap<String,TrieNode>();
} /**
* 用于设置当前节点的值
* @param value 当前节点的值
*/
public void setValue(String value){
this.value=value;
} /**
* 用于获取其当前节点的值
* @return 当前节点值
*/
public String getValue(){
return this.value;
}
/**
* 用于设置当前节点的字符是否为字符串的最后一个字符
* @param isEnd true表示当前字符为字符串的最后一个字符
*/
public void setIsEnd(boolean isEnd){
this.isEnd=isEnd;
} /**
* 用于获取其当前节点是否为单词末尾字符的判断
* @return 返回其对应的结果
*/
public boolean getIsEnd(){
return this.isEnd;
} /**
* 用于添加当前节点的孩子节点
* @param value 孩子节点的值
* @param node 指向当前节点的孩子节点的指针
*/
public void addChild(String value,TrieNode node){
children.put(value,node);
} /**
* 用于获取当前节点的孩子节点
* @param value 孩子节点的值
* @return 其对应的孩子节点的指针
*/
public TrieNode getChild(String value){
return children.get(value);
}
} /**
* 用于Trie树的插入操作
* @param value
*/
public void add(String value){
TrieNode node=head;
for(int i=0;i<value.length();i++){
String v=String.valueOf(value.charAt(i));
TrieNode child=node.getChild(v);
//该字符并没有在相应的孩子节点中
if(child==null){
child=new TrieNode(v);
node.addChild(v,child);
}
//当其为该单词的最后一个字符时
if(i==value.length()-1){
child.setIsEnd(true);
}
node=child;
}
} /**
* 用于判断其对应单词是否在其对应的字典树中
* @param value 需要查找的单词
* @return 是否在字典树中的判断
*/
public boolean search(String value){
TrieNode node = head;
for(int i=0;i<value.length();i++){
String v=String.valueOf(value.charAt(i));
TrieNode child=node.getChild(v);
if(child==null){
return false;
}
//当为最后一个字符,且其不为截止字符的时候,表示该单词不在字典树中
if(i==value.length()-1&&!child.getIsEnd()){
return false;
}
node=child;
}
return true;
}
}

Trie树应用场景:

1. 字符串检索

  事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。

举例:

  给出N个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。

给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。

2. 字符串最长公共前缀

  Trie树利用多个字符串的公共前缀来节省存储空间,反之,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。

举例:

  给出N个小写英文字母串,以及Q 个询问,即询问某两个串的最长公共前缀的长度是多少?

   解决方案: 首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线(Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。

而最近公共祖先问题同样是一个经典问题,可以用下面几种方法:

  1. 利用并查集(Disjoint Set),可以采用经典的Tarjan 算法;

  2. 求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;

3. 排序

  Trie树是一棵多叉树,只要先序遍历整棵树,输出相应的字符串便是按字典序排序的结果。

比如给你N个互不相同的仅由一个单词构成的英文名,让你将它们按字典序从小到大排序输出。

4. 作为其他数据结构和算法的辅助结构

  如后缀树,AC自动机等

5. 词频统计

  trie树在这里的应用类似哈夫曼树,

比如词频统计使用哈希表或者堆都可以,但是如果内存有限,就可以用trie树来压缩空间,因为trie树的公共前缀都是用一个节点保存的。

6. 字符串搜索的前缀匹配

  trie树常用于搜索提示。如当输入一个网址,可以自动搜索出可能的选择。当没有完全匹配的搜索结果,可以返回前缀最相似的可能。

  Trie树检索的时间复杂度可以做到O(M),M是要检索单词的长度,如果使用暴力检索,需要指数级O(M^2)的时间复杂度。

回到目录|·(工)·)

博文参考自:字典树(Trie树)的实现及应用

K:单词查找树(Trie)的更多相关文章

  1. cogs 293. [NOI 2000] 单词查找树 Trie树字典树

    293. [NOI 2000] 单词查找树 ★★☆   输入文件:trie.in   输出文件:trie.out   简单对比时间限制:1 s   内存限制:128 MB 在进行文法分析的时候,通常需 ...

  2. codevs 1729 单词查找树

    二次联通门 : codevs 1729 单词查找树 /* codevs 1729 单词查找树 Trie树 统计节点个数 建一棵Trie树 插入单词时每新开一个节点就计数器加1 */ #include ...

  3. Trie树,又称单词查找树、字典

    在百度或淘宝搜索时,每输入字符都会出现搜索建议,比如输入“北京”,搜索框下面会以北京为前缀,展示“北京爱情故事”.“北京公交”.“北京医院”等等搜索词.实现这类技术后台所采用的数据结构是什么?[中国某 ...

  4. 【数据结构】关于前缀树(单词查找树,Trie)

    前缀树的说明和用途 前缀树又叫单词查找树,Trie,是一类常用的数据结构,其特点是以空间换时间,在查找字符串时有极大的时间优势,其查找的时间复杂度与键的数量无关,在能找到时,最大的时间复杂度也仅为键的 ...

  5. COGS 293.[NOI2000] 单词查找树

    ★   输入文件:trie.in   输出文件:trie.out   简单对比 时间限制:1 s   内存限制:128 MB 在进行文法分析的时候,通常需要检测一个单词是否在我们的单词列表里.为了提高 ...

  6. [NOI2000] 单词查找树

    ★★   输入文件:trie.in   输出文件:trie.out   简单对比 时间限制:1 s   内存限制:128 MB 在进行文法分析的时候,通常需要检测一个单词是否在我们的单词列表里.为了提 ...

  7. 293. [NOI2000] 单词查找树——COGS

    293. [NOI2000] 单词查找树 ★★   输入文件:trie.in   输出文件:trie.out   简单对比时间限制:1 s   内存限制:128 MB 在进行文法分析的时候,通常需要检 ...

  8. 解题报告:luogu P5755 [NOI2000]单词查找树

    题目链接:P5755 [NOI2000]单词查找树 曾几何时,NOI 也有这么水的题( 裸的\(Trie\),只用维护插入即可,记得\(+1\)就好了,真没用讲的. \(Code\): #includ ...

  9. 【NOI2000】 单词查找树

    问题描述 在进行文法分析的时候,通常需要检测一个单词是否在我们的单词列表里.为了提高查找和定位的速度,通常都画出与单词列表所对应的单词查找树,其特点如下: 根结点不包含字母,除根结点外每一个结点都仅包 ...

随机推荐

  1. maven項目創建紅叉

    出现问题的原因: 解决方案: 在pom.xml中配置jdk的版本

  2. 【文文殿下】【BZOJ4804】欧拉心算

    题解 显然有 \(ans=\sum _{i=1} ^{n} \lfloor \frac{n}{i} \rfloor \sum _{d|i} \mu(d) \phi (\frac{i}{d})\) 前半 ...

  3. 静态分析第三发 so文件分析(小黄人快跑)

    本文作者:i春秋作家——HAI_ 0×00 工具 1.IDA pro 2.Android Killer 0×01 环境 小黄人快跑 下载地址http://download.csdn.net/downl ...

  4. express 重新加载

    1,res.location() 2. res.redirect() location()与redirect()的比较: Express的response对象,是对Node.js原生对象ServerR ...

  5. RabbitMQ交换机规则实例

    RabbitMQ Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct.fanout.topic.headers .headers 匹配 AMQP 消息的 header ...

  6. HTML5技术要点

    HTML5技术要点 1.HTML5视频 <!DOCTYPE HTML> <html> <body> <video src="/i/movie.ogg ...

  7. 如何取消mysql授权并删除用户

    如何查看授权的所有用户SELECT DISTINCT CONCAT('User: ''',user,'''@''',host,''';') AS query FROM mysql.user;撤销已经赋 ...

  8. 使用Hive UDF和GeoIP库为Hive加入IP识别功能

    Hive是基于Hadoop的数据管理系统,作为分析人员的即时分析工具和ETL等工作的执行引擎,对于如今的大数据管理与分析.处理有着非常大的 意义.GeoIP是一套IP映射数据库,它定时更新,并且提供了 ...

  9. es6中let,const区别与其用法

    ECMAScript 是什么? 首先,我们都知道JavaScript由三部分组成:ECMAScript,DOM,BOM: 其中的ECMAScript是Javascript的语法规范. ECMAScri ...

  10. R语言中常用包(二)

    数据导入 以下R包主要用于数据导入和保存数据 feather:一种快速,轻量级的文件格式.在R和python上都可使用readr:实现表格数据的快速导入.中文介绍可参考这里readxl:读取Micro ...