前一段时间面试字节的时候,被问到gin框架的路由结构。gin框架的路由结构采用的一般是前缀树来实现,于是被要求手写前缀树来实现路由的注册和查找。
本文以 leetcode 1233为例介绍一下前缀树(字典树)。
原题描述
原题为有很多类似于unit风格的文件夹,如\tmp, \usr\local, 且保证这些文件夹是合法的,不会存在类似于usr或者\usr\local\之类的,现在要求删除所有的子文件夹,返回剩余的文件夹。
设计思路
该题可以采用前缀树加深度优先搜索来实现。
前缀树又名字典树,本质上讲就是一个多叉树,每一个节点保存一个字符或者一个字符串,基于公共前缀可以以较少的内存空间保存这些数据,并且实现较高的查询速率。
在本题中,可以用树中的每一个节点来保存一个文件夹的一个级别,然后节点额外维护一个变量,标识是否有文件夹结束在这一个节点上。然后用深度优先搜索搜索这根树,如果搜索到一个节点,发现有一个文件夹结束在这一个节点上,说明这个分支不用搜索了,因为这个分支下如果还有文件夹也一定是结束在这一个节点上的文件夹的子文件夹。
代码实现
首先是前缀树的实现,代码如下:
type PrefixTreeNode[T comparable] struct {
key T // 当前树节点名称
children map[T]*PrefixTreeNode[T] // 子节点
isEnd bool // 表示是否有一个元素结束在该节点上
value interface{}
}
func NewPrefixTreeNode[T comparable](key T, value interface{}, isEnd bool) *PrefixTreeNode[T] {
return &PrefixTreeNode[T]{key: key, value: value, isEnd: isEnd, children: make(map[T]*PrefixTreeNode[T])}
}
type PrefixTree[T comparable] struct {
root *PrefixTreeNode[T]
}
func NewPrefixTree[T comparable]() *PrefixTree[T] {
return &PrefixTree[T]{root: &PrefixTreeNode[T]{children: make(map[T]*PrefixTreeNode[T])}}
}
// Insert 根据keyPath插入一系列节点,最后一个节点的值赋值或者更新为value
func (t *PrefixTree[T]) Insert(keyPath []T, value interface{}) {
tr := t.root
n := len(keyPath)
i := 0
for i < n {
if nextNode, ok := tr.children[keyPath[i]]; ok {
tr = nextNode
i++
} else {
for i < n {
newNode := NewPrefixTreeNode(keyPath[i], nil, false)
tr.children[keyPath[i]] = newNode
tr = newNode
i++
}
}
}
tr.isEnd = true
tr.value = value
}
// Search 搜索前缀树,返回最后一个节点绑定的值,如果没找到,则第二个返回值为false
func (t *PrefixTree[T]) Search(keyPath []T) (interface{}, bool) {
tr := t.root
n := len(keyPath)
i := 0
for i < n {
if nextNode, ok := tr.children[keyPath[i]]; ok {
tr = nextNode
i++
} else {
return nil, false
}
}
return tr.value, true
}
// GetShortestKey 返回前缀树所有特殊前缀,树中不存在其他前缀以这些前缀为前缀
func (t *PrefixTree[T]) GetShortestKey() [][]T {
res := make([][]T, 0)
var dfs func(curNode *PrefixTreeNode[T], cur []T)
dfs = func(curNode *PrefixTreeNode[T], cur []T) {
if curNode.isEnd {
res = append(res, append([]T(nil), cur...))
return
}
for _, child := range curNode.children {
dfs(child, append(cur, child.key))
}
}
dfs(t.root, make([]T, 0))
return res
}
在实现上,使用了泛型和闭包,闭包就是在函数中定义函数,这样做的好处可以增加可读性,减少冗余代码和变量,适用于该函数独特使用的工具函数。然后再来看深度优先搜索的代码实现,如下:
func removeSubfolders(folder []string) []string {
t := NewPrefixTree[string]()
for _, f := range folder {
paths := strings.Split(f, "/")
newPaths := make([]string, 0)
for i := 0; i < len(paths); i++ {
if len(paths[i]) > 0 {
newPaths = append(newPaths, "/" + paths[i])
}
}
t.Insert(newPaths, nil)
}
res := t.GetShortestKey()
ans := make([]string, len(res))
for i, each := range res {
ans[i] = strings.Join(each, "")
}
return ans
}
leetcode运行截图如下:

复杂度分析
- 时间复杂度: O ( n m ) O(nm) O(nm), 其中n为文件夹的数量,m为最长层级文件夹的层级数
- 空间复杂度: O ( m n ) O(mn) O(mn)
970

被折叠的 条评论
为什么被折叠?



