单词查找树,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。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. 萝卜保卫战3内购破解+Toast窗口增加(Love版)

    涉及到一些不同的破解的方法,以及不同的破解思路,还有一些重要权限的删除等. 作者:HAI_ 这次目标是经常玩的萝卜保卫战,不知不觉,已经更新到3了.详细分析请参考https://bbs.ichunqi ...

  2. Sysbench0.5初体验

    最近工作中需要测试数据库的OLTP的性能,参考了下MariaDB的benchmark中的测试脚本,发现脚本中已经使用了Sysbench-0.5,可以在这里https://launchpad.net/s ...

  3. 对一致性hash原理的理解

    一致性hash算法解决的核心问题是,当solt数发生变化的时候能够尽量少的移动数据.该算法最早在<Consistent Hashing and Random Trees:Distributed ...

  4. 使用git工具删除github上的文件或者文件夹

    解决 使用git工具删除github上的文件或者文件夹 当我们需要从github上删除一些我们不需要的文件或者文件夹时,如果通过github来操作的话,将会很麻烦,因为github只允许删除一个仓库, ...

  5. 【pkuwc2018】 【loj2537】 Minmax DP+线段树合并

    今年年初的时候参加了PKUWC,结果当时这一题想了快$2h$都没有想出来.... 哇我太菜啦.... 昨天突然去搜了下哪里有题,发现$loj$上有于是就去做了下. 结果第一题我5分钟就把所有细节都想好 ...

  6. 转MVC3介绍

    第一节:Asp.Net MVC3项目介绍 让我们先看一下,一个普通的Asp.Net MVC3项目的样例,如下图所示 跟WebFrom还是有区别的,如果你已经了解Asp.Net MVC2的话,那就感觉异 ...

  7. Swift 学习指引

    以下指引是基于最新的 Swift 4.0 为基础为而言. 如你在参考3.0以下版本, 那你就不要说你会 Swift, 3.0 之前是 Objective-C 的搬迁(80%),是不成熟的语言, 看着很 ...

  8. LeetCode All in One题解汇总(持续更新中...)

    突然很想刷刷题,LeetCode是一个不错的选择,忽略了输入输出,更好的突出了算法,省去了不少时间. dalao们发现了任何错误,或是代码无法通过,或是有更好的解法,或是有任何疑问和建议的话,可以在对 ...

  9. JS实现一个基于对象的链表

    JS实现一个基于对象的链表 /*JS实现一个基于对象的链表*/ function Node(element){ this.element = element;//节点存储的元素 this.next = ...

  10. 导出数据库数据成txt格式

    set verify off; set colsep ‘分隔符’; set echo off; set feedback off; set heading off; set pagesize 0; s ...