Trie 树,也叫“字典树”。顾名思义,它是一个树形结构。它是一种专门处理字符串匹配的数据结构,用来解决在一组字符串集合中快速查找某个字符串的问题。

现在,我们先来看下,Trie 树到底长什么样子。

我举个简单的例子来说明一下。我们有 6 个字符串,它们分别是:how,hi,her,hello,so,see。我们希望在里面多次查找某个字符串是否存在。如果每次查找,都是拿要查找的字符串跟这 6 个字符串依次进行字符串匹配,那效率就比较低,有没有更高效的方法呢?

这个时候,我们就可以先对这 6 个字符串做一下预处理,组织成 Trie 树的结构,之后每次查找,都是在 Trie 树中进行匹配查找。Trie 树的本质,就是利用字符串之间的公共前缀,将重复的前缀合并在一起。最后构造出来的就是下面这个图中的样子。

其中,根节点不包含任何信息。每个节点表示一个字符串中的字符,从根节点到红色节点的一条路径表示一个字符串(注意:红色节点并不都是叶子节点)。

 为了让你更容易理解 Trie 树是怎么构造出来的,我画了一个 Trie 树构造的分解过程。构造过程的每一步,都相当于往 Trie 树中插入一个字符串。当所有字符串都插入完成之后,Trie 树就构造好了。

 当我们在 Trie 树中查找一个字符串的时候,比如查找字符串“her”,那我们将要查找的字符串分割成单个的字符 h,e,r,然后从 Trie 树的根节点开始匹配。如图所示,绿色的路径就是在 Trie 树中匹配的路径。

 如果我们要查找的是字符串“he”呢?我们还用上面同样的方法,从根节点开始,沿着某条路径来匹配,如图所示,绿色的路径,是字符串“he”匹配的路径。但是,路径的最后一个节点“e”并不是红色的。也就是说,“he”是某个字符串的前缀子串,但并不能完全匹配任何字符串。

Golang实现

1、基于数组的代码实现

以下代码用于判断a-z小写的单词是否存在

package main

import "fmt"

const Size = 26 //数组大小为26

type TireNode struct {
	Data   uint8
	IsWord bool
	Next   []*TireNode
}
type TireHead struct {
	Len  int
	Next []*TireNode
}

func NewTire() *TireHead {
	//next := make([]*TireNode, Size, Size)
	head := &TireHead{
		Len:  0,
		Next: nil,
	}
	return head
}
func (t *TireHead) Insert(str string) {
	//index := str[0] - 'a'
	if t.Next == nil {
		t.Next = make([]*TireNode, Size, Size)
	}
	cur := t.Next
	index := uint8(0)
	for i := 0; i < len(str); i++ {
		index = str[i] - 'a'   //只能比较 a-z的Size个字符(26个)
		if cur[index] == nil { //TODO 注意:如果节点为空,需要创建
			newNode := &TireNode{
				Data:   str[i],
				IsWord: false,
				Next:   make([]*TireNode, Size, Size),
			}
			cur[index] = newNode
		} else {
			cur[index].Data = str[i] //todo 如果节点已经有了,就直接使用可以了
		}
		if i == len(str)-1 {
			cur[index].IsWord = true //todo 最后一个字符对应的 标志位需要设置为true,代表是一个完整的字符
		}
		cur = cur[index].Next
	}
}
func (t *TireHead) isExist(str string) bool {
	if t.Next == nil {
		return false
	}
	cur := t.Next
	index := uint8(0)
	for i := 0; i < len(str); i++ {
		index = str[i] - 'a'
		if cur[index] == nil {
			//todo 特别要注意条件的判断:如果找到对应的内容为nil了,就直接返回。
			// 不然从nil对应的变量中取数据,包报错
			return false
		}
		if cur[index].Data != str[i] {
			return false
		}
		if i == len(str)-1 && cur[index].IsWord == true {
			return true
		}
		cur = cur[index].Next
		if cur == nil {
			return false
		}
	}
	return false
}
func main() {
	tire := NewTire()
	//tire.Insert("ab")
	//tire.Insert("ok")
	tire.Insert("he")
	tire.Insert("ok")
	tire.Insert("hello")
	fmt.Println("he:",tire.isExist("a"))
	fmt.Println("ok:",tire.isExist("ok"))
	fmt.Println("hel:",tire.isExist("hel"))
}

2、基于hashmap的代码实现

package main

import (
	"fmt"
)

//数据节点;存储数据:数据值、是否结束标志位、下一个节点指针
type TNode struct {
	Data   byte
	IsWord bool
	Next   map[byte]*TNode
}

//根节点,存储数据为:1、字符串个数;2、数据节点map序列
type TNRoot struct {
	Len  int
	Node map[byte]*TNode
}

func CreateTire() TNRoot {
	tn := make(map[byte]*TNode)
	//创建节点的时候,需要将node赋值
	tRoot := TNRoot{
		Len:  0,
		Node: tn, //todo 技巧:需要赋值,不然后续没有办法操作
	}
	return tRoot
}
func (tn *TNRoot) insert(str string) {
	cur := tn.Node
	for i := 0; i < len(str); i++ {
		if cur[str[i]] == nil {
			newNode := &TNode{
				Data:   str[i],
				IsWord: false,
				Next:   make(map[byte]*TNode), //TODO 技巧:下一个节点需要实例化,不然是空,后续无法操作。
			}
			cur[str[i]] = newNode
		}
		if i == len(str)-1 {
			cur[str[i]].IsWord = true
		}
		cur = cur[str[i]].Next
	}
	tn.Len++

}
func (tn *TNRoot) isExist(str string) bool {
	if tn.Node == nil {
		return false
	}
	cur := tn.Node
	for i := 0; i < len(str); i++ {
		if cur[str[i]] == nil { //如果比较的数据不存在,直接返回
			return false
		}
		if cur[str[i]].Data != str[i] {
			return false
		}
		if i == len(str)-1 { //判断最后一个比较的字符是否是结束标志位
			if cur[str[i]].IsWord == true {
				return true
			}
		}
		cur = cur[str[i]].Next
	}
	return false
}
func main() {
	tire := CreateTire()
	tire.insert("hello")
	tire.insert("ok")
	tire.insert("he")
	fmt.Println(tire.isExist("ok"))
	fmt.Println(tire.isExist("he"))
	fmt.Println(tire.isExist("hel"))
	fmt.Println("tire树的字符串数量:", tire.Len)
}