首页 > 其他 > 详细

208. 实现 Trie (前缀树)

时间:2020-10-07 21:42:21      阅读:34      评论:0      收藏:0      [点我收藏+]

208. 实现 Trie (前缀树)

题目描述

实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。

示例:

Trie trie = new Trie();

trie.insert("apple");
trie.search("apple"); // 返回 true
trie.search("app"); // 返回 false
trie.startsWith("app"); // 返回 true
trie.insert("app"); 
trie.search("app"); // 返回 true

说明:

你可以假设所有的输入都是由小写字母 a-z 构成的。
保证所有输入均为非空字符串。

参考:https://leetcode-cn.com/problems/implement-trie-prefix-tree/solution/trie-tree-de-shi-xian-gua-he-chu-xue-zhe-by-huwt/

定义类 Trie

正常的树节点定义是怎么样的

struct TreeNode {
    VALUETYPE value;    //结点值
    TreeNode* children[NUM];    //指向孩子结点
};

下面是Trie的结点定义,体会二者的不同

 1 class Trie {
 2 
 3     boolean isEnd = false;      // 标记是否为最终结点
 4     Trie[] next;        // 所有孩子结点
 5 
 6     /** Initialize your data structure here. */
 7     public Trie() {
 8         next = new Trie[26];    // 26个孩子结点
 9     }
10 }

插入

描述:向 Trie 中插入一个单词 word

实现:这个操作和构建链表很像。首先从根结点的子结点开始与 word 第一个字符进行匹配,一直匹配到前缀链上没有对应的字符,这时开始不断开辟新的结点,直到插入完 word 的最后一个字符,同时还要将最后一个结点isEnd = true;,表示它是一个单词的末尾。

 1 public void insert(String word) {
 2     // 有则共享且继续向下遍历,否则新建结点
 3     Trie node = this;       // node指针用来遍历
 4     for(char ch : word.toCharArray()){
 5         int index = ch - a;
 6         if(node.next[index] == null){   // 新建结点
 7             node.next[index] = new Trie();
 8         }
 9         node = node.next[index];    // 指针后移
10     }
11     node.isEnd = true;      // 标记为最终结点
12 }

查找

描述:查找 Trie 中是否存在单词 word

实现:从根结点的子结点开始,一直向下匹配即可,如果出现结点值为空就返回 false,如果匹配到了最后一个字符,那我们只需判断 node->isEnd即可。

 1 public boolean search(String word) {
 2     // 一直向下遍历,如果某个结点没找到,直接返回false,结束查找后判断是否为最终结点
 3     Trie node = this;
 4     for(char ch : word.toCharArray()){
 5         int index = ch - a;
 6         if(node.next[index] == null){
 7             return false;
 8         }
 9         node = node.next[index];    // 指针后移
10     }
11     return node.isEnd;
12 }

前缀匹配

描述:判断 Trie 中是或有以 prefix 为前缀的单词

实现:和 search 操作类似,只是不需要判断最后一个字符结点的isEnd,因为既然能匹配到最后一个字符,那后面一定有单词是以它为前缀的。

 1 public boolean startsWith(String prefix) {
 2     // 和search方法其实差不多,但是结束判断后直接返回true, 因为是判断前缀而已
 3     Trie node = this;
 4     for(char ch : prefix.toCharArray()){
 5         int index = ch - a;
 6         if(node.next[index] == null){
 7             return false;
 8         }
 9         node = node.next[index];    // 指针后移
10     }
11     return true;
12 }

完整代码为:

 1 class Trie {
 2 
 3     boolean isEnd = false;      // 标记是否为最终结点
 4     Trie[] next;        // 所有孩子结点
 5 
 6     /** Initialize your data structure here. */
 7     public Trie() {
 8         next = new Trie[26];    // 26个孩子结点
 9     }
10     
11     /** Inserts a word into the trie. */
12     public void insert(String word) {
13         // 有则共享且继续向下遍历,否则新建结点
14         Trie node = this;       // node指针用来遍历
15         for(char ch : word.toCharArray()){
16             int index = ch - a;
17             if(node.next[index] == null){   // 新建结点
18                 node.next[index] = new Trie();
19             }
20             node = node.next[index];    // 指针后移
21         }
22         node.isEnd = true;      // 标记为最终结点
23     }
24     
25     /** Returns if the word is in the trie. */
26     public boolean search(String word) {
27         // 一直向下遍历,如果某个结点没找到,直接返回false,结束查找后判断是否为最终结点
28         Trie node = this;
29         for(char ch : word.toCharArray()){
30             int index = ch - a;
31             if(node.next[index] == null){
32                 return false;
33             }
34             node = node.next[index];    // 指针后移
35         }
36         return node.isEnd;
37     }
38     
39     /** Returns if there is any word in the trie that starts with the given prefix. */
40     public boolean startsWith(String prefix) {
41         // 和search方法其实差不多,但是结束判断后直接返回true, 因为是判断前缀而已
42         Trie node = this;
43         for(char ch : prefix.toCharArray()){
44             int index = ch - a;
45             if(node.next[index] == null){
46                 return false;
47             }
48             node = node.next[index];    // 指针后移
49         }
50         return true;
51     }
52 }

leetcode执行用时:41 ms > 85.59%, 内存消耗:49.4 MB > 33.00%

复杂度分析:

时间复杂度:插入、查询、前缀匹配都是O(n), n为为字符串长度

空间复杂度: 每个结点都需要一个26大小的数组,Trie 的每个结点中都保留着一个字母表,这是很耗费空间的。如果 Trie 的高度为 n,字母表的大小为 m,最坏的情况是 Trie 中还不存在前缀相同的单词,那空间复杂度就为 O(m^n)。

扩展:

字典树(TrieTree),又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。
Trie的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
技术分享图片
它有3个基本性质:
根节点不包含字符,除根节点外每一个节点都只包含一个字符。
从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
每个节点的所有子节点包含的字符都不相同

应用

Trie树典型应用是用于快速检索(最长前缀匹配),自动补全,拼写检查,统计,排序和保存大量的字符串,所以经常被搜索引擎系统用于文本词频统计,搜索提示等场景。它的优点是最大限度地减少无谓的字符串比较,查询效率比较高。

Trie树与二叉搜索树

数据规模为n时,二叉搜索树插入、查找、删除操作的时间复杂度通常只有O(logn),最坏情况下整棵树所有的节点都只有一个子节点,退变成一个线性表,此时插入、查找、删除操作的时间复杂度是O(n)。

? ?通常情况下,Trie树的高度n要远大于搜索字符串的长度m,故查找操作的时间复杂度通常为O(m),最坏情况下的时间复杂度才为O(n)。很容易看出,Trie树最坏情况下的查找也快过二叉搜索树。

Trie树与Hash表

既然有了其他的数据结构,如平衡树和哈希表,使我们能够在字符串数据集中搜索单词。为什么我们还需要 Trie 树呢?尽管哈希表可以在 O(1) 时间内寻找键值,却无法高效的完成以下操作:
  • 找到具有同一前缀的全部键值。
  • 按词典序枚举字符串的数据集。
Trie 树优于哈希表的另一个理由是,随着哈希表大小增加,会出现大量的冲突,时间复杂度可能增加到 O(n),其中 n 是插入的键的数量。与哈希表相比,Trie 树在存储多个具有相同前缀的键时可以使用较少的空间。此时 Trie 树只需要O(m) 的时间复杂度,其中 m 为键长。而在平衡树中查找键值需要 O(mlogn) 时间复杂度。

208. 实现 Trie (前缀树)

原文:https://www.cnblogs.com/hi3254014978/p/13777810.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!