Skip to content

Commit ad93454

Browse files
authored
Update trie.md
1 parent 3b86dae commit ad93454

File tree

1 file changed

+184
-15
lines changed

1 file changed

+184
-15
lines changed

thinkings/trie.md

Lines changed: 184 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,217 @@
1-
# 前缀树问题
1+
# Trie(来自公众号力扣加加的活动《91天学算法》的讲义)
22

3-
## 介绍
3+
## 简介
4+
5+
字典树也叫前缀树、Trie。它本身就是一个树型结构,也就是一颗多叉树,学过树的朋友应该非常容易理解,它的核心操作是插入,查找。删除很少使用,因此这个讲义不包含删除操作。
46

57
截止目前(2020-02-04) [前缀树(字典树)](https://leetcode-cn.com/tag/trie/) 在 LeetCode 一共有 17 道题目。其中 2 道简单,8 个中等,7 个困难。
68

7-
这里总结了六道题,弄懂这几道, 那么前缀树对你应该不是大问题, 希望这个专题可以帮到正在学习前缀树的你。
9+
## 前缀树的特点
810

911
简单来说, 前缀树就是一个树。前缀树一般是将一系列的单词记录到树上, 如果这些单词没有公共前缀,则和直接用数组存没有任何区别。而如果有公共前缀, 则公共前缀仅会被存储一次。可以想象,如果一系列单词的公共前缀很多, 则会有效减少空间消耗。
1012

1113
而前缀树的意义实际上是空间换时间,这和哈希表,动态规划等的初衷是一样的。
1214

1315
其原理也很简单,正如我前面所言,其公共前缀仅会被存储一次,因此如果我想在一堆单词中找某个单词或者某个前缀是否出现,我无需进行完整遍历,而是遍历前缀树即可。本质上,使用前缀树和不使用前缀树减少的时间就是公共前缀的数目。也就是说,一堆单词没有公共前缀,使用前缀树没有任何意义。
1416

15-
知道了前缀树的作用和使用场景,接下来我们自己实现一个前缀树。关于实现可以参考 [0208.implement-trie-prefix-tree](https://github.com/azl397985856/leetcode/blob/b8e8fa5f0554926efa9039495b25ed7fc158372a/problems/208.implement-trie-prefix-tree.md)
17+
知道了前缀树的特点,接下来我们自己实现一个前缀树。关于实现可以参考 [0208.implement-trie-prefix-tree](https://github.com/azl397985856/leetcode/blob/b8e8fa5f0554926efa9039495b25ed7fc158372a/problems/208.implement-trie-prefix-tree.md)
18+
19+
## 应用场景及分析
20+
21+
正如上面所说,前缀树的核心思想是用空间换时间,利用字符串的公共前缀来降低查询的时间开销。
22+
23+
比如给你一个字符串 query,问你这个**字符串**是否在**字符串集合**中出现过,这样我们就可以将字符串集合建树,建好之后来匹配 query 是否出现,那有的朋友肯定会问,之前讲过的 hashmap 岂不是更好?
1624

17-
## API
25+
我们想一下用百度搜索时候,打个“一语”,搜索栏中会给出“一语道破”,“一语成谶(四声的 chen)”等推荐文本,这种叫模糊匹配,也就是给出一个模糊的 query,希望给出一个相关推荐列表,很明显,hashmap 并不容易做到模糊匹配,而 Trie 可以实现基于前缀的模糊搜索。
1826

19-
自己实现前缀树,首先要知道它的 api 有哪些,以及具体功能是什么。
27+
> 注意这里的模糊搜索也仅仅是基于前缀的。比如还是上面的例子,搜索“道破”就不会匹配到“一语道破”,而只能匹配“道破 xx”
2028
21-
前缀树的 api 主要有以下几个:
29+
因此,这里我的理解是:上述精确查找只是模糊查找一个特例,模糊查找 hashmap 显然做不到,并且如果在精确查找问题中,hashmap 出现过多冲突,效率还不一定比 Trie 高,有兴趣的朋友可以做一下测试,看看哪个快。
2230

23-
- `insert(word)`: 插入一个单词
24-
- `search(word)`:查找一个单词是否存在
25-
- `startWith(word)`: 查找是否存在以 word 为前缀的单词
31+
再比如给你一个长句和一堆敏感词,找出长句中所有敏感词出现的所有位置(想下,有时候我们口吐芬芳,结果发送出去却变成了\*\*\*\*,懂了吧)
2632

27-
其中 startWith 是前缀树最核心的用法,其名称前缀树就从这里而来。大家可以先拿 208 题开始,熟悉一下前缀树,然后再尝试别的题目
33+
> 小提示:实际上 AC 自动机就利用了 trie 的性质来实现敏感词的匹配,性能非常好。以至于很多编辑器都是用的 AC 自动机的算法
2834
29-
## 图解
35+
还有些其他场景,这里不过多讨论,有兴趣的可以 google 一下。
36+
37+
## 基本概念
3038

3139
一个前缀树大概是这个样子:
3240

3341
![](https://tva1.sinaimg.cn/large/007S8ZIlly1ghlug87vyfj30mz0gq406.jpg)
3442

3543
如图每一个节点存储一个字符,然后外加一个控制信息表示是否是单词结尾,实际使用过程可能会有细微差别,不过变化不大。
3644

45+
接下来,我们看下 Trie 里面的概念。
46+
47+
### 节点:
48+
49+
- 根结点无实际意义
50+
- 每一个节点代表一个字符
51+
- 每个节点中的数据结构可以自定义,如 isWord(是否是单词),count(该前缀出现的次数)等,需实际问题实际分析需要什么。
52+
53+
### Trie 的插入
54+
55+
- 假定给出几个单词如[she,he,her,good,god]构造出一个 Trie 如下图:
56+
57+
![Trie%200c1c1245b4df467e91ceb6931c94701d/Untitled.png](Trie%200c1c1245b4df467e91ceb6931c94701d/Untitled.png)
58+
59+
- 也就是说从根结点出发到某一粉色节点所经过的字符组成的单词,在单词列表中出现过,当然我们也可以给树的每个节点加个 count 属性,代表根结点到该节点所构成的字符串前缀出现的次数
60+
61+
![Trie%200c1c1245b4df467e91ceb6931c94701d/Untitled%201.png](Trie%200c1c1245b4df467e91ceb6931c94701d/Untitled%201.png)
62+
63+
可以看出树的构造非常简单,插入新单词的时候就从根结点出发一个字符一个字符插入,有对应的字符节点就更新对应的属性,没有就创建一个!
64+
65+
### Trie 的查询
66+
67+
查询更简单了,给定一个 Trie 和一个单词,和插入的过程类似,一个字符一个字符找
68+
69+
- 若中途有个字符没有对应节点 →Trie 不含该单词
70+
- 若字符串遍历完了,都有对应节点,但最后一个字符对应的节点并不是粉色的,也就不是一个单词 →Trie 不含该单词
71+
72+
## Trie 模版
73+
74+
了解了 Trie 的使用场景以及基本的 API, 那么最后就是用代码来实现了。
75+
76+
这里我提供了 Python 和 Java 两种语言的代码。
77+
78+
Java:
79+
80+
```java
81+
class Trie {
82+
83+
TrieNode root;
84+
85+
public Trie() {
86+
87+
root = new TrieNode();
88+
}
89+
90+
public void insert(String word) {
91+
92+
TrieNode node = root;
93+
94+
for (int i = 0; i < word.length(); i++) {
95+
96+
if (node.children[word.charAt(i) - 'a'] == null)
97+
node.children[word.charAt(i) - 'a'] = new TrieNode();
98+
99+
node = node.children[word.charAt(i) - 'a'];
100+
node.preCount++;
101+
}
102+
103+
node.count++;
104+
}
105+
106+
public boolean search(String word) {
107+
108+
TrieNode node = root;
109+
110+
for (int i = 0; i < word.length(); i++) {
111+
112+
if (node.children[word.charAt(i) - 'a'] == null)
113+
return false;
114+
115+
node = node.children[word.charAt(i) - 'a'];
116+
}
117+
118+
return node.count > 0;
119+
}
120+
121+
public boolean startsWith(String prefix) {
122+
123+
TrieNode node = root;
124+
125+
for (int i = 0; i < prefix.length(); i++) {
126+
127+
if (node.children[prefix.charAt(i) - 'a'] == null)
128+
return false;
129+
node = node.children[prefix.charAt(i) - 'a'];
130+
}
131+
132+
return node.preCount > 0;
133+
}
134+
135+
private class TrieNode {
136+
137+
int count; //表示以该处节点构成的串的个数
138+
int preCount; //表示以该处节点构成的前缀的字串的个数
139+
TrieNode[] children;
140+
141+
TrieNode() {
142+
143+
children = new TrieNode[26];
144+
count = 0;
145+
preCount = 0;
146+
}
147+
}
148+
}
149+
```
150+
151+
Python:
152+
153+
```python
154+
class TrieNode:
155+
def __init__(self):
156+
self.count = 0
157+
self.preCount = 0
158+
self.children = {}
159+
160+
class Trie:
161+
162+
def __init__(self):
163+
self.root = TrieNode()
164+
165+
def insert(self, word):
166+
node = self.root
167+
for ch in word:
168+
if ch not in node.children:
169+
node.children[ch] = TrieNode()
170+
node = node.children[ch]
171+
node.preCount += 1
172+
node.count += 1
173+
174+
def search(self, word):
175+
node = self.root
176+
for ch in word:
177+
if ch not in node.children:
178+
return False
179+
node = node.children[ch]
180+
return node.count > 0
181+
182+
def startsWith(self, prefix):
183+
node = self.root
184+
for ch in prefix:
185+
if ch not in node.children:
186+
return False
187+
node = node.children[ch]
188+
return node.preCount > 0
189+
```
190+
191+
**复杂度分析**
192+
193+
- 插入和查询的时间复杂度自然是$O(len(key))$,key 是待插入(查找)的字串。
194+
195+
- 建树的最坏空间复杂度是$O(m^{n})$, m 是字符集中字符个数,n 是字符串长度。
196+
197+
## 题目推荐
198+
37199
以下是本专题的六道题目的题解,内容会持续更新,感谢你的关注~
38200

39201
- [0208.实现 Trie (前缀树)](https://github.com/azl397985856/leetcode/blob/b8e8fa5f0554926efa9039495b25ed7fc158372a/problems/208.implement-trie-prefix-tree.md)
40202
- [0211.添加与搜索单词 - 数据结构设计](https://github.com/azl397985856/leetcode/blob/b0b69f8f11dace3a9040b54532105d42e88e6599/problems/211.add-and-search-word-data-structure-design.md)
41203
- [0212.单词搜索 II](https://github.com/azl397985856/leetcode/blob/b0b69f8f11dace3a9040b54532105d42e88e6599/problems/212.word-search-ii.md)
42204
- [0472.连接词](https://github.com/azl397985856/leetcode/blob/master/problems/472.concatenated-words.md)
205+
- [648. 单词替换](https://leetcode-cn.com/problems/replace-words/)
43206
- [0820.单词的压缩编码](https://github.com/azl397985856/leetcode/blob/master/problems/820.short-encoding-of-words.md)
44-
- [1032.字符流](../problems/1032.stream-of-characters.md)
207+
- [1032.字符流](https://github.com/azl397985856/leetcode/blob/master/problems/1032.stream-of-characters.md)
208+
209+
## 总结
210+
211+
前缀树的核心思想是用空间换时间,利用字符串的公共前缀来降低查询的时间开销。因此如果题目中公共前缀比较多,就可以考虑使用前缀树来优化。
212+
213+
前缀树的基本操作就是插入和查询,其中查询可以完整查询,也可以前缀查询,其中基于前缀查询才是前缀树的灵魂,也是其名字的来源。
45214

46-
## 相关题目
215+
最后给大家提供了两种语言的前缀树模板,大家如果需要用,直接将其封装成标准 API 调用即可。
47216

48-
- [648. 单词替换](https://leetcode-cn.com/problems/replace-words/) (换皮题)
217+
基于前缀树的题目变化通常不大, 使用模板就可以解决。如何知道该使用前缀树优化是一个难点,不过大家只要牢牢记一点即可,那就是**算法的复杂度瓶颈在字符串查找,并且字符串有很多公共前缀,就可以用前缀树优化**

0 commit comments

Comments
 (0)