多模式字符串匹配:Trie树

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 树比较经典的应用场景。

你可能感兴趣的:(数据结构与算法)