Trie树
定义
Trie树,也叫“字典树”,是一种树形结构,专门用来处理字符串匹配的数据结构。
Trie树的本质,利用字符串间的公共前缀,将重复的字符合并在一起,形成一个树形结构,并且给叶子节点打上标记。其中,根节点不包含任何信息,每个节点表示一个字符串中的一个字符,从根节点到叶子节点的一个路径,表示一个字符串。
Trie树是一个多叉树。
多叉树存储 todo
实现
插入字符串
查询字符串
代码实现
package main
import (
"fmt"
)
type TrieNode struct {
data interface{}
isEnding bool
child map[rune]*TrieNode
}
func NewTrieNode(data interface{}) *TrieNode {
return &TrieNode{
data: fmt.Sprint(data),
isEnding: false,
//child: make(map[rune]*TrieNode, 26), // 纯小写英文时候
child: make(map[rune]*TrieNode),
}
}
func (this *TrieNode) Insert(s string) bool {
ss := []rune(s)
if len(ss) == 0 {
return false
}
for _, index := range ss {
if this.child[index] == nil {
node := NewTrieNode(index)
this.child[index] = node
}
this = this.child[index]
}
this.isEnding = true
return true
}
func (this *TrieNode) Find(s string) bool {
ss := []rune(s)
l := len(ss)
if l == 0 {
return false
}
for _, index := range ss {
if this.child[index] == nil {
return false
} else {
this = this.child[index]
}
}
if this.isEnding {
return true
}
return false
}
func main() {
root := NewTrieNode("/")
b := root.Insert("a中国")
fmt.Println(b)
fmt.Println(root)
bb := root.Find("a中国")
fmt.Println(bb)
b = root.Insert("b中c")
fmt.Println(b)
b = root.Insert("b中国hfjh")
fmt.Println(b)
fmt.Println(root)
}
时间复杂度
从插入和查找实现代码分析复杂度很简单,而且时间复杂度非常高效,一旦建好trie树以后,查找一个字符串K,仅需要遍历一遍K即可,非常高效。
插入
O(n) n为要插入的字符串长度
查找
O(k) k为模式串字符串长度
优点
从时间复杂度可以看出,非常高效。
缺点
比较耗内存,因为每个节点都要维护一个哈希map,比如纯英文,长度也许是26个字符就好,如果要是含有中文等,那这个map的长度就会更大,所以,trie树是以空间换时间的思路。
当然,我们不可否认,Trie 树尽管有可能很浪费内存,但是确实非常高效。那为了解决这个内存问题,我们是否有其他办法呢?我们可以稍微牺牲一点查询的效率,将每个节点中的数组换成其他数据结构,来存储一个节点的子节点指针。用哪种数据结构呢?我们的选择其实有很多,比如有序数组、跳表、散列表、红黑树等。假设我们用有序数组,数组中的指针按照所指向的子节点中的字符的大小顺序排列。查询的时候,我们可以通过二分查找的方法,快速查找到某个字符应该匹配的子节点的指针。但是,在往 Trie 树中插入一个字符串的时候,我们为了维护数组中数据的有序性,就会稍微慢了点。替换成其他数据结构的思路是类似的,这里我就不一一分析了,你可以结合前面学过的内容,自己分析一下。
Trie 树与散列表、红黑树的比较实际上,字符串的匹配问题,笼统上讲,其实就是数据的查找问题。对于支持动态数据高效操作的数据结构,我们前面已经讲过好多了,比如散列表、红黑树、跳表等等。实际上,这些数据结构也可以实现在一组字符串中查找字符串的功能。我们选了两种数据结构,散列表和红黑树,跟 Trie 树比较一下,看看它们各自的优缺点和应用场景。在刚刚讲的这个场景,在一组字符串中查找字符串,Trie 树实际上表现得并不好。它对要处理的字符串有及其严苛的要求。第一,字符串中包含的字符集不能太大。我们前面讲到,如果字符集太大,那存储空间可能就会浪费很多。即便可以优化,但也要付出牺牲查询、插入效率的代价。第二,要求字符串的前缀重合比较多,不然空间消耗会变大很多。第三,如果要用 Trie 树解决问题,那我们就要自己从零开始实现一个 Trie 树,还要保证没有 bug,这个在工程上是将简单问题复杂化,除非必须,一般不建议这样做。
第四,我们知道,通过指针串起来的数据块是不连续的,而 Trie 树中用到了指针,所以,对缓存并不友好,性能上会打个折扣。综合这几点,针对在一组字符串中查找字符串的问题,我们在工程中,更倾向于用散列表或者红黑树。因为这两种数据结构,我们都不需要自己去实现,直接利用编程语言中提供的现成类库就行了。讲到这里,你可能要疑惑了,讲了半天,我对 Trie 树一通否定,还让你用红黑树或者散列表,那 Trie 树是不是就没用了呢?是不是今天的内容就白学了呢?实际上,Trie 树只是不适合精确匹配查找,这种问题更适合用散列表或者红黑树来解决。Trie 树比较适合的是查找前缀匹配的字符串,也就是类似百度的输入前缀后提示补全字符串功能。
应用
Trie 树的优势并不在于,用它来做动态集合数据的查找,因为,这个工作完全可以用更加合适的散列表或者红黑树来替代。Trie 树最有优势的是查找前缀匹配的字符串,比如搜索引擎中的关键词提示功能这个场景,就比较适合用它来解决,也是 Trie 树比较经典的应用场景。