前言
- go的 gin框架 路由模块使用的也是前缀树,所以要先了解前缀树这个数据结构相关的知识点,动手简单实现一下。
- 实现前缀树, 也可以参考力扣第 208号算法题:208. 实现 Trie (前缀树)
- 这篇文章也是后面讲解 go web框架gin框架路由的数据结构基础。 看完这个,可以再去看 我的下一篇文章 gin 源码解析:https://blog.csdn.net/pythonstrat/article/details/121423122
目录结构
Trie
Trie.go
main.go
Trie 的实现
- 实现写法, 可以参照模仿 container/list 双向链表的写法实现
- 下面代码中使用的是 slice 存储子节点, 线性查找遍历, 后面会进行 hash优化。
package Trie
// 字典树:
/*
1. 单词结尾表示:需要使用一个 bool 类型,辨识当前节点是否是结尾字符
叶子结点是 单词结尾
非叶子节点 也可能是单词的结尾
2. 实现功能
增加单词
搜索单词
*/
// 定义树的节点
type Node struct {
// 1. 保存当前字符
C byte
// 2. 若干个子节点
Children []*Node
// 3. 标识节点是否是一个单词最后的一个字符
isWord bool
}
// 给子节点 列表添加新的节点
func (n *Node) Add(c byte) {
// 构造节点
node := Node{
C: c,
}
// 定义的 slice 类型的,不用初始化,也可以正常使用append方法
n.Children = append(n.Children, &node)
}
// 返回子节点 列表 长度
func (n *Node) Len() int {
return len(n.Children)
}
// 设置节点的 isword
func (n *Node) SetIsword(flag bool) {
n.isWord = flag
}
// 整棵树的根节点
type Trie struct {
// 字典树的根节点
Root *Node
}
// 随便初始化根节点为 "/" 吧
func (t *Trie) Init() *Trie {
t.Root = &Node{
'/',
make([]*Node, 0),
false,
}
return t
}
// 外部调用初始化一个 字典树
func New() *Trie { return new(Trie).Init() }
// 添加单词
// 时间复杂度:
func (t *Trie) Add(word string) {
// 转换为 字符
w := []byte(word)
// 找到每一个节点
cur := t.Root
// 遍历 word的每一个字符
for _, c := range w {
// 1. 查找子节点中是否包含当前字符
index := t.containsChar(c, cur.Children)
if index == -1 {
// 添加新节点
cur.Add(c)
// 更新cur 指向它
index = cur.Len() - 1
}
// cur 指向下一个子节点
cur = cur.Children[index]
}
// 将最后一个节点字符的 尾设置为 true
cur.SetIsword(true)
}
// 查找子节点中是否包含指定字符: 私有方法
func (t *Trie) containsChar(c byte, childrens []*Node) int {
// 线性遍历查找
for i, node := range childrens {
// 查找到既返回索引
if node.C == c {
return i
}
}
return -1
}
// 判断是否包含指定的单词
// 不仅能找到单词,并且单词的结尾必须是 isWord = true 才说明有这个单词
func (t *Trie) Contains(word string) bool {
// 转换为 字符
w := []byte(word)
// 找到每一个节点
cur := t.Root
// 遍历 word的每一个字符
for _, c := range w {
// 1. 查找子节点中是否包含当前字符
index := t.containsChar(c, cur.Children)
if index == -1 {
return false
}
// cur 指向下一个子节点
cur = cur.Children[index]
}
// 判断最后一个单词是否是 单词末尾 cur.isWord == true,
// 直接缩写为 cur.isWord
return cur.isWord
}
使用hash优化效率,改造代码线性查找
package Trie
// 字典树:
/*
1. 单词结尾表示:需要使用一个 bool 类型,辨识当前节点是否是结尾字符
叶子结点是 单词结尾
非叶子节点 也可能是单词的结尾
2. 实现功能
增加单词
搜索单词
*/
// 定义树的节点
type Node struct {
// 1. 保存当前字符
//C byte
// 使用hash存储,就不需要当前的字符了,它已经在key中了
// 2. 若干个子节点: 字符: 节点
Children map[byte]*Node
// 3. 标识节点是否是一个单词最后的一个字符
isWord bool
}
// 给子节点 列表添加新的节点
func (n *Node) Add(c byte) {
// 构造节点: nil 类型 map 不可直接使用添加元素等
node := Node{
Children: make(map[byte]*Node, 0),
}
// 天啊及
n.Children[c] = &node
}
// 返回子节点 列表 长度
//func (n *Node) Len() int {
// return len(n.Children)
//}
// 设置节点的 isword
func (n *Node) SetIsword(flag bool) {
n.isWord = flag
}
// 整棵树的根节点
type Trie struct {
// 字典树的根节点
Root *Node
}
// 随便初始化根节点为 "/" 吧
func (t *Trie) Init() *Trie {
t.Root = &Node{
make(map[byte]*Node, 0),
false,
}
return t
}
// 外部调用初始化一个 字典树
func New() *Trie { return new(Trie).Init() }
// 添加单词
// 时间复杂度:
func (t *Trie) Add(word string) {
// 转换为 字符
w := []byte(word)
// 找到每一个节点
cur := t.Root
// 遍历 word的每一个字符
for _, c := range w {
// 1. 查找子节点中是否包含当前字符
// 如果不包含, 添加一下
if _, ok := cur.Children[c]; !ok {
// 添加新节点
cur.Add(c)
}
// cur 指向下一个子节点
cur = cur.Children[c]
}
// 将最后一个节点字符的 尾设置为 true
cur.SetIsword(true)
}
查找子节点中是否包含指定字符: 私有方法
//func (t *Trie) containsChar(c byte, childrens []*Node) int {
// // 线性遍历查找
// for i, node := range childrens {
// // 查找到既返回索引
// if node.C == c {
// return i
// }
// }
// return -1
//}
// 判断是否包含指定的单词
// 不仅能找到单词,并且单词的结尾必须是 isWord = true 才说明有这个单词
func (t *Trie) Contains(word string) bool {
// 转换为 字符
w := []byte(word)
// 找到每一个节点
cur := t.Root
// 遍历 word的每一个字符
for _, c := range w {
// 1. 查找子节点中是否包含当前字符
if _, ok := cur.Children[c]; !ok {
return false
}
// cur 指向下一个子节点
cur = cur.Children[c]
}
// 判断最后一个单词是否是 单词末尾 cur.isWord == true,
// 直接缩写为 cur.isWord
return cur.isWord
}
运行main方法,执行测试用例
package main
import (
"01/Trie字典树/Trie"
"fmt"
)
func main() {
// 创建字典树
t := Trie.New()
// 编写测试
t.Add("big")
t.Add("pat")
t.Add("bigger")
t.Add("dog")
t.Add("door")
// 测试
fmt.Println(t.Contains("big"))
fmt.Println(t.Contains("bigg"))
fmt.Println(t.Contains("bigger"))
fmt.Println(t.Contains("biggerr"))
}
- 选择不同的数据结构,实现不同的算法,对性能和空间的提升是有的,并且缩短了代码量。
基数树:
- 前面的前缀树可以看出来, 查找速度是跟 树的深度有关的。 并且从在单一分支的情况,且对单一超长的 word或者是比较稀疏的单词的时候,存储很费力不讨好,会导致树总是只有单个分支,大大提高树的深度 和 存储空间。
- 扩展到许多框架的路由匹配
- 应对大量的路由匹配, radix 树可以提高你应对大量匹配的能力。
- 这篇文章有个简单的介绍还可以, 可以看下,演进过程。
- https://ethbook.abyteahead.com/ch4/radix.html