在学习gin的前缀树的过程中,很难理解其中的逻辑,后来看了部分源代码之后,突然领悟,还得从简单添加功能到复杂,所以,先写一个静态的前缀树,后面再添加功能到复杂
gopackage urltree
import (
"fmt"
"log"
"strings"
"gitee.com/duzy2023/urltree/internel"
)
// 首先创建一个静态匹配前缀树
type nodeType uint8 // 节点类型
const (
static nodeType = iota // 静态类型
)
type node struct {
fullpath string // 全路径,只有最后一个节点才有
path string // 当前节点在整个路径中的字段
children []*node
nType nodeType // 节点类型
indices string //子节点path的第一个字符
}
// 获取新节点
func NewRoot() *node {
n := &node{
fullpath: "/",
children: make([]*node, 0),
}
return n
}
// 找到两个路径段最长相同前缀的下标
func longestCommonPrefix(apath, bpath string) int {
i := 0
max_ := min(len(apath), len(bpath))
for i < max_ && apath[i] == bpath[i] {
i++
}
return i
}
// 添加静态路由
func (n *node) AddRoute(fullpath string) error {
if !strings.HasPrefix(fullpath, "/") {
return fmt.Errorf("path must start with '/' but get '%s'", fullpath)
}
// 由于后面会对fullpath进行处理,所以这里对fullpath进行备份
_fullpath := fullpath
// 1.
// 先判断是否是空节点
// 没有子节点并且path为空,那么就是空节点
if len(n.children) == 0 && len(n.path) == 0 {
log.Printf("Empty Node")
n.path = fullpath
n.fullpath = _fullpath
return nil
}
// 对于非空节点进行处理
parentFullPathIndex := 0
walk:
for {
// 2.计算最长公共前缀(LCP)
i := longestCommonPrefix(fullpath, n.path)
// 3.节点分裂
// 如果LCP小于当前节点的path长度,就需要分裂
if i < len(n.path) {
child := node{
path: n.path[i:],
nType: n.nType,
indices: n.indices,
children: n.children,
fullpath: n.fullpath,
}
n.children = []*node{&child}
n.indices = internel.BytesToString([]byte{n.path[i]})
n.path = fullpath[:i]
n.fullpath = ""
}
// 4.处理剩余路径
if i < len(fullpath) {
fullpath = fullpath[i:]
nextChar := fullpath[0]
// 5. 查找匹配的子节点
for j, idxChar := range n.indices {
if byte(idxChar) == nextChar {
parentFullPathIndex += len(n.path)
n = n.children[j]
continue walk
}
}
// 6.没有匹配的子节点,创建新的节点
n.indices += internel.BytesToString([]byte{nextChar})
newChild := &node{
path: fullpath,
fullpath: _fullpath,
nType: static,
}
n.addChild(newChild)
return nil
}
// 7.完全匹配
// 说明已经注册了一个完整路径,报错
if n.fullpath != "" {
return fmt.Errorf("path '%s' is already registered", _fullpath)
}
// 如果n.fullpath == "",那么,就表明这个路由还没被注册
n.fullpath = _fullpath
return nil
}
}
func (n *node) addChild(child *node) {
n.children = append(n.children, child)
}
// FindRoute 查找路由 (可选功能)
// path: 待查找的路径
// returns: 找到的节点的 fullpath,如果没找到则返回空字符串
func (n *node) FindRoute(path string) string {
parentPathIndex := 0
currentNode := n
for {
// 计算当前节点 path 与查找路径的 LCP
i := longestCommonPrefix(path, currentNode.path)
// 如果 LCP 不等于当前节点的 path,说明匹配失败
if i != len(currentNode.path) {
return "" // 路径不存在
}
// 如果 path 已经完全匹配(LCP 等于 path 长度)
if i == len(path) {
// 检查当前节点是否有 fullpath(代表它是一个注册的终点)
return currentNode.fullpath // 返回注册时的完整路径
}
// path 还有剩余,继续向下查找
path = path[i:]
nextChar := path[0]
// 在 indices 中查找下一个字符
foundIndex := -1
for j, idxChar := range currentNode.indices {
if byte(idxChar) == nextChar {
foundIndex = j
break
}
}
if foundIndex == -1 {
return "" // 没有找到匹配的子节点,路径不存在
}
// 更新 parentPathIndex 并跳转到子节点
parentPathIndex += len(currentNode.path)
currentNode = currentNode.children[foundIndex]
}
}
// PrintTree 用于调试:打印树结构 (辅助函数)
func (n *node) PrintTree(prefix string, isLast bool) {
if n == nil {
return
}
connector := "├── "
if isLast {
connector = "└── "
}
fmt.Printf("%s%s path: '%s', fullpath: '%s'\n", prefix, connector, n.path, n.fullpath)
childCount := len(n.children)
for i, child := range n.children {
isLastChild := i == childCount-1
newPrefix := prefix
if !isLast {
newPrefix += "│ " // 如果当前节点不是父节点的最后一个孩子,则竖线继续
} else {
newPrefix += " " // 如果是最后一个孩子,则用空格填充
}
child.PrintTree(newPrefix, isLastChild)
}
}
其中的FindTree和PrintTree是使用AI生成的,AI真好用