Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >深入理解Trie树

深入理解Trie树

作者头像
我是攻城师
发布于 2019-06-03 10:46:56
发布于 2019-06-03 10:46:56
2.1K00
代码可运行
举报
文章被收录于专栏:我是攻城师我是攻城师
运行总次数:0
代码可运行

前言

前面的文章介绍过各种高效的的数据结构,比如二叉搜索树,AVL树,红黑树,B树,跳跃表等,今天我们再来学习一种多路树,叫做Trie树。

什么是Trie树

在计算机科学中,Trie,又称前缀树或字典树,是一种有序树,用于保存关联数组。其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。Trie树的名称来源于搜索引擎中的专有名词的retrieval,发音和单词try一样。

比如下面的这个Trie树包含“Cat”,“Cut”,“Cute”,“To”,“B”五个单词,其存储图示如下:

从上面我们可以发现,前缀一样的单词,在实际存储中只存储了一份,并且如果单词后面有.符号表从root到这个位置是一个单词,前缀相同的单词会复用一样的公共节点。

Trie树的应用场景

Trie最典型的应用场景是用于搜索引擎的suggest功能,比如我们在google中,每输入一个英文字母,搜索引擎都会给过我们返回以这个字母为前缀的相关的结果,如下:

除以之外,还有常见的IDE里面自动补全功能和各种通讯录的自动补全功能等。

Trie树的工作原理

这里以英文单词为例,我们知道英语单词由26个字母组成,每一个字母都是这26个字母中的其中一个,假如现在我们想为英语单词的suggest功能,那么使用Trie树就非常适合。因为总共的可能性只有26,所以我们可以定义一个长度为26的数组来存储所有的可能性,然后附带一个boolean类型的变量,来标记当前层是否是一个单词。

如下定义:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
   static   class TrieNode{       TrieNode[] children;       boolean isWord;
       public TrieNode(){           children=new TrieNode[26];       }    }
如何插入

Trie树的结构,通常需要一个虚拟的head节点来辅助,head节点并不存储数据,仅仅用于操作方便,在插入的时候,会分解单词为一个字符数组,然后依次插入其中的每一个字母到Trie树里面,如果插入的位置不存在该字母,那么代表第一次插入,就需要新建一个TrieNode节点, 如果插入的地方已经存在,那么就直接继续插入下一个字母,直到整个单词的每一个字母都插入完毕后,在最后一个TrieNode节点处标记到目前这个节点处,代表一个完整的单词。

如何查询

查询主要有两种形式,第一种是判断是否存在某个单词在Trie树里面,第二种是判断指定的前缀是否在Trie树里面存在。

这两种case的检索方式大致一样,就是从head节点入手,判断这个单词的第一个字母是否存在,如果就跳到第二级继续搜索,知道遍历完整个字母,返回最后一个节点,然后判断如果该节点有数据,并且有完整单词标记,那么就证明该单词存在,如果没有单词标记,那么证明该前缀存在。如果判断返回的节点没有数据,那么就证明当前的Trie树里面不包含某个单词或者输入的指定的前缀。

如何删除

Trie树的相对来说稍微复杂点,举个例子,比如包含6个单词的Trie树:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
("abc")("xy")("xyz")("abb")("xyzb"), ("word")

图示如下:

我们看删除的几种情况:

(1)如果要删除的单词不存在,则不做任何操作

(2)如果要删除的单词是没有任何字母被作为公共前缀,那么就要删除每个字母,如上图的单词word

(3)如果要删除的单词全部字母都是公共前缀,那么仅仅在这个单词的尾部标记不是完整单词即可,如上图的单词xyz

(4)如果要删除的单词是超出了公共前缀,那么仅仅删除多出的部分即可,如上图的xyzb,在删除的时候仅仅删除字母b即可。

(5)如果要删除的单词是两条路径的公共前缀,那么仅仅删除非公共前缀的部分即可,这种情况与(4)类似,但不同的是(4)是在一条路径上,而(5)是在两条路径上,如上图要删除abc,因为前缀ab是共用的,所以仅仅删除abc的c字母即可。

上面就是删除的全部情况,不过在Trie树里面,重要的部分是插入和检索部分,删除部分可能比较少的使用。

Trie树的实现方式

Trie树有两种实现策略,第一种使用数组存储所有的可能性,第二种是使用的Map存储所有的可能性,使用数组的方式需要对存储的字符和数组的index之间要有明确的映射规则,这样便于查询,比如如果想做中文的suggest,一种的可行的办法就是将整个中文字符集映射到具体的数值上,然后与上面字母的操作类似,如果选择使用Map作为可能性存储,相对来说会更加灵活一点,毕竟Map在大多数编程语言里面都有封装好的动态实现。

下面给出的是基于数组实现Trie树的方式,基于Map的实现方式可以参考我的github的代码:

https://github.com/qindongliang/Java-Note

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class TrieByArray {

   static   class TrieNode{       TrieNode[] children;//存储所有的可能性       boolean isWord;//是否是一个完整单词       public TrieNode(){           children=new TrieNode[26];       }    }

    TrieNode root;
   public TrieByArray(){       root=new TrieNode();   }

   public void insert(String word){
       TrieNode prev=root;
       for (int i = 0; i < word.length(); i++) {
           if(prev.children[word.charAt(i)-'a']==null){               prev.children[word.charAt(i)-'a']=new TrieNode();           }
           prev=prev.children[word.charAt(i)-'a'];
       }
       prev.isWord =true;
   }
   public boolean search(String word){       TrieNode result=checkExists(word);       if(result!=null&&result.isWord){           return true;       }       return false;
   }

   public TrieNode checkExists(String query){
       TrieNode cur=root;
       for (int i = 0; i < query.length(); i++) {           //待查询的字符不存在           if(cur.children[query.charAt(i)-'a']==null){               return cur.children[query.charAt(i)-'a'];           }else{               cur=cur.children[query.charAt(i)-'a'];;           }       }
       return cur;   }

   public void delete(String key){     if(root==null||key==null){         return;     }
       deleteWord(root,key,key.length(),0);
   }
   private boolean hasChildren(TrieNode currentNode){       for (int i = 0; i < currentNode.children.length; i++) {           if(currentNode.children[i]!=null){               return true;           }       }       return false;   }

   public boolean deleteWord(TrieNode currentNode,String word,int length,int level){       boolean deletedSelf=false;       if(level==length){            //如果当前节点是最后,并且没有孩子节点           if(!hasChildren(currentNode)){               currentNode=null;               deletedSelf=true;           }else{//有孩子节点               currentNode.isWord =false;//则将其置为非单词属性即可               deletedSelf=false;           }
       }else {           //由下而上递归删除           TrieNode childNode=currentNode.children[word.charAt(level)-'a'];           boolean childDeleted=deleteWord(childNode,word,length,level+1);           if(childDeleted){//如果单词的最后的字符没有孩子节点,就可以被删除,然后需要继续向上递归判断其前一个字符是否是需要删除               currentNode.children[word.charAt(level)-'a']=null;//设置子节点为null               if(currentNode.isWord){//判断父节点是否是一个word的结束,如果是说明是公共前缀就不能再删除了                   deletedSelf=false;               }else if(hasChildren(currentNode)){//如果这个父节点还有孩子节点,说明也是公共前缀,也不能再删除了                  deletedSelf=false;               }else{//到这一步,说明父节点也是要删除单词唯一的的字符,可以继续向上删除                   currentNode=null;                   deletedSelf=true;               }           }else {//如果不需要被删除,则向上传递false即可               deletedSelf=false;           }       }
       return deletedSelf;   }


   public boolean startsWith(String prefix){       TrieNode result=checkExists(prefix);       if(result!=null){           return true;       }       return false;    }






    public static void main(String[] args) {
       TrieByArray trie=new TrieByArray();       trie.example1();//       trie.example2();//       trie.example3();

   }
   private void example1(){       TrieByArray trie = new TrieByArray();
        trie.insert("apple");        System.out.println(trie.search("apple"));   // returns true
        System.out.println(trie.search("app"));     // returns false        System.out.println(trie.startsWith("app")); // returns true        trie.insert("app");        System.out.println(trie.search("app"));     // returns true   }
   private void example2(){       TrieByArray trie = new TrieByArray();
       trie.insert("gat");       trie.insert("gag");       System.out.println(trie.search("gat"));//true       System.out.println(trie.startsWith("ga"));//true       trie.delete("gat");       System.out.println(trie.search("gat"));//false       System.out.println(trie.startsWith("ga"));//true       System.out.println(trie.search("gag"));//true   }
   private void example3(){       TrieByArray trie = new TrieByArray();       trie.insert("abcd");       trie.insert("abc");
       System.out.println(trie.search("abc"));       System.out.println(trie.startsWith("abc"));
       trie.delete("abc");
       System.out.println(trie.search("abc"));       System.out.println(trie.search("abcd"));       System.out.println(trie.startsWith("abc"));   }

}

Trie树的时间和空间复杂度分析

Trie树的时间复杂度为O(k),k=该字符串(单词或者前缀)的长度,在最好最坏的case均为O(k)。

Trie树的空间复杂度,如果重复的前缀比较多,那么一定程度上使用Trie是可以节省存储空间的,但如果重复的前缀不多,这样就会导致Trie消耗大量内存,为什么这么说?

还记得上面数组的实现方式吗?在TrieNode里面需要把每一种可能性都要提前存储到一个数组,方便查找,拿英文单词为例,一个单词cat,看起来只需要3个char字符的空间就可以了,但实际上每个字符都需要存储一个26大小的指针数组,这样就需要O(n*k)的空间来存储,k=字符串的长度,n=共有多少个这样不重复的字符,这么算下来,其实Trie树的原理,可以看做是拿空间换时间的实现策略。

那么有的同学会说使用数组的方式为了查找快速,没办法只能那么存,那么使用Map的方式是不是就节省内存了?其实并不是,不要忘记,Map的底层也是通过数组+链表+红黑树的方式实现的,初始化容量和每次扩容也都是需要开辟大量的前置空间来存储的。所以相比数组的方式可能还要更浪费内存。

总结

本文详细的介绍了Trie树相关的内容,Trie树作为一种特殊的数据结构,虽然在实际开发中并不常用,但其在特定的场景下比如suggest自动补全功能,可以发挥的出色的表现。Trie树本质上是一种通过空间换时间的策略,来提高检索性能,所以在使用的时候要注意内存限制。当然,Trie树在空间上也是有优化策略的,比如对部分前缀或者后缀进行压缩,这样以来能够节省不必要的指针存储,这种实现需要更复杂的编码来支持,感兴趣的朋友可以自己研究下。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-05-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 我是攻城师 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
漫画:什么是“前缀树”?
如上图所示,我们在百度输入框输入ap两个字母,下拉菜单就会自动列举出包含该前缀的所有单词,比如api、app、apple等等。
小灰
2023/09/26
2800
漫画:什么是“前缀树”?
【图解算法】模板+变式——带你彻底搞懂字典树(Trie树)
接下来将对经典的字典树进行代码实现;接着做几个变体题目深入理解字典树的强大;最后回到日常生活,瞧瞧字典树怎样融入到了我们的生活之中 >_<
全栈程序员站长
2022/10/04
1.4K0
【图解算法】模板+变式——带你彻底搞懂字典树(Trie树)
一种好用的树结构:Trie树
在计算机科学中,trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。
致Great
2022/05/13
5540
一种好用的树结构:Trie树
力扣208——实现 Trie (前缀树)
实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。
健程之道
2020/02/12
4570
力扣208——实现 Trie (前缀树)
☆打卡算法☆LeetCode 208. 实现 Trie (前缀树) 算法解析
大家好,我是小魔龙,Unity3D软件工程师,VR、AR,虚拟仿真方向,不定时更新软件开发技巧,生活感悟,觉得有用记得一键三连哦。
恬静的小魔龙
2022/09/27
4740
☆打卡算法☆LeetCode 208. 实现 Trie (前缀树)   算法解析
字典树的数据结构_数据结构快速排序
上一篇我们介绍了 线段树(Segment Tree),本文主要介绍Trie字典树。
全栈程序员站长
2022/10/04
4330
字典树的数据结构_数据结构快速排序
208. 实现 Trie (前缀树)
请你实现 Trie 类: Trie() 初始化前缀树对象。 void insert(String word) 向前缀树中插入字符串 word 。 boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。 boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。 输入 ["T
编程张无忌
2021/07/08
3910
剑指Offer——Trie树(字典树)
Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
全栈程序员站长
2022/10/03
9690
剑指Offer——Trie树(字典树)
【设计数据结构】实现 Trie (前缀树)
这是 LeetCode 上的「208. 实现 Trie (前缀树)」,难度为「中等」。
宫水三叶的刷题日记
2021/10/08
1.6K0
【设计数据结构】实现 Trie (前缀树)
LeetCode 208.实现Trie(字典树) - JavaScript
实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。
心谭博客
2020/04/21
6860
Trie - 208. Implement Trie (Prefix Tree)
Implement a trie with insert, search, and startsWith methods.
ppxai
2020/09/23
5550
从Trie树到双数组Trie树
Trie树 原理 又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,能在常数时间O(len)内实现插入和查
JadePeng
2018/03/12
3.2K0
从Trie树到双数组Trie树
leetcode刷题(55)——208. 实现 Trie (前缀树)
实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。
老马的编程之旅
2022/06/22
2090
实现 Trie (前缀树)
Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
狼啸风云
2024/02/03
1790
实现 Trie (前缀树)
一种基于defaultdict的前缀树Python实现
前缀树(Trie 树,也称为字典树、单词查找树)是一种树形数据结构,用于高效地存储和检索字符串集合中的键。前缀树的主要优势在于能够快速地查找具有相同前缀的字符串,并且对于大量的字符串集合,它可以提供较高的检索效率。
杜逸先
2023/12/06
3880
Leetcode 208 solution: Implement Trie (Prefix Tree)
https://blog.baozitraining.org/2019/04/leetcode-solution-208-implement-trie.html
包子面试培训
2019/04/30
6250
让你的代码更CPP一点(前缀树示例)
不知道各位写C++代码的童鞋们,有没有发现一个现象,自己写的CPP代码怎么那么像C代码呢?笔者也深有感触,但是自从C++11标准出现以后,CPP的代码就开始精简很多了,风格也极大的发生了变化,今天笔者就开始整理一些C++的新特性,并展示如何在实际应用中使用!让你的代码更Cpp些!
算法工程师之路
2019/08/05
6640
数据结构之Trie字典树
Trie 树,也叫“字典树”或“前缀树”。顾名思义,它是一个树形结构。但与二分搜索树、红黑树等不同的是,Trie 树是一种多叉树,即每个节点可以有 m 个子节点。它是一种专门处理字符串匹配的数据结构,用来解决在一组字符串集合中快速查找某个字符串的问题。
端碗吹水
2021/01/29
8560
golang刷leetcode 前缀树
实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。
golangLeetcode
2022/08/02
4730
查找-多路查找详解篇
学编程的小程
2023/10/11
3170
查找-多路查找详解篇
相关推荐
漫画:什么是“前缀树”?
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验