Trie树
别称:字典树
树形结构,专门处理字符串匹配的数据结构,解决在一组字符串集合中快速查找某个字符串的问题。
样例: 搜索引擎的提示功能,当搜索东西时,并不用把所有内容都输入进去,一定程度上节省了搜索时间。
什么是Trie树
本质: Trie树的本质,利用字符串之间的公共前缀,将重复的前缀合并在一起。
Trie树的实现
需要实现的功能有两个:
- 将字符串插入到Trie树
- 在Trie树查询一个字符串
Trie树的节点
Trie树是一个多叉树,二叉树中一个节点左右节点通过两个指针来存储,在多叉树种,如何存储一个节点的所有子节点指针?
解决: 借助散列表的思想,我们通过下标与字符映射的数组,来存储子节点的指针。
class TrieNode:
def __init__(self, data:str):
self._data = data # 存储数据
self._children = [None] * 26 # 存储指针,用来存储a~z这26个字母
self._is_ending = False # 结尾的标志
将az的下标为025,下标为0存储的是a,下标为25存储的是z,当子节点不存在就是 None
#!/usr/bin/env python
class TrieNode:
def __init__(self, data:str):
self._data = data
self._children = [None] * 26
self._is_ending = False
class Trie:
def __init__(self):
self._root = TrieNode("/")
def insert(self, text):
node = self._root
for index, char in map(lambda x:(ord(x) - ord('a'), x), text):
if not node._children[index]:
node._children[index] = TrieNode(char)
node = node._children[index]
node._is_ending = True
def find(self. pattern):
node = self._root
for index in map(lambda x:ord((x) - ord('a')), pattern)
if not node._children[index]: return False
node = node._children[index]
return node._is_ending
分析Trie树
时间复杂度
构建 Trie树 的过程,需要扫描所有字符串,时间复杂度是 O(n)
(n表示所有字符串长度和)。
查询字符串长度时k,我们只需要对比大约k个节点,就能完成查询操作,时间复杂度是 O(k)
。
内存的消耗
Trie树是非常耗内存的,用的是一种空间换时间的思路。
问题: 因为每一个节点都要维护一个足够大的数组,当相同的前缀字符串很少时,Trie不但不节省内存,还会浪费更多内存。
内存问题的解决: 牺牲查询效率将节点中的数组换成其他数据结构,存储节点的子节点指针。利用有序数组(降低空间消耗,支持更多字符),数组中的指针按照所指向的子节点中的字符的大小顺序排序,查询的时候,我们可以通过二分查找,快速查找到某个字符应该匹配的子节点的指针,但是在插入一个字符串时,为了维护数组中数据的有序性,就会插入慢一点。
Trie树的变体
缩点优化: 针对只有一个子节点的节点,而且该节点不是一个字符串的结束节点,就可以将此节点与子节点合并,这样就能节省空间。
Tire树的比较
字符串匹配问题,就是数据查找问题,实际上支持动态数据高效操作的数据结构(散列表、红黑树、跳表等),这些数据也能实现字符串查找功能。
Trie树的适用场景
- 字符串包含字符集不能太大,那么存储空间会浪费很多,虽然可以优化,但是牺牲查询、插入效率的代价。
- 要求字符串的前缀重合比较多,不然空间消耗会变大很多。
- 要用Trie树来解决问题,需要从零开始实现一个Trie树。
- 通过指针串连接的数据块是不连续的,而Trie树用到了指针,对缓存不是很友好,性能会打折扣。
针对一组字符中查找字符串问题,工程中更倾向使用散列表或红黑树,因为不需要自己实现,直接去使用库就行了。
优点: Trie树不适合精确匹配查找,精确匹配适合红黑树和散列表,Tire树适合查找前缀匹配的字符串。
缺点: 不适用与动态集合数据的查找,这种适用于红黑树和散列表。
使用场景
- 自动输入补全
- IDE代码编辑器自动补全
- 浏览器网址自动补全