https://labuladong.online/algo/data-structure/trie-implement/#trieset-的实现
https://labuladong.online/algo/data-structure-basic/trie-map-basic/
TrieMap 是什么?
Tire树又称字典树/前缀树,具有如下特点
根节点不包含字符 除根节点外每个节点只包含一个字符
树的每一个路径都是一个字符串
每个节点的子节点包含的字符都不相同
简单来说,TrieMap 就是一个将 Trie(前缀树)的数据结构与 Map(映射)的功能结合起来的集合。 它不仅仅是一个键值对的存储器,更是一个能够高效地处理与字符串(或任何序列)键相关的各种操作的强大工具。
核心思想:Trie + Map
-
Trie(前缀树)作为底层结构:
- Trie 的核心思想是利用键的公共前缀来共享节点,从而节省空间和提高查找效率。
- 每个节点通常代表键的一个字符或序列中的一个元素。
- 从根节点到任意一个节点的路径,代表了一个前缀。
-
Map 的功能扩展:
- 在 Trie 的基础上,将值 “挂载” 到 Trie 的特定节点上。
- 通常,当一个键的完整路径在一个 Trie 节点处结束时,这个节点会包含与该键关联的值。
- 一个节点可以有多个子节点(对应不同的下一个字符),也可以存储一个值(表示该前缀本身就是一个完整的键)。
TrieMap 的结构和工作原理
-
节点(Node): TrieMap 的基本构建单元。每个节点至少包含:
- 子节点指针(Children): 通常是一个 Map 或数组,用于存储指向下一个字符/元素的子节点的引用。例如,
Map<Character, Node>
或Node[26]
(针对英文字母)。 - 值(Value): 一个可选的字段,如果当前节点代表一个完整的键的结束,则该字段存储与该键关联的值。如果一个节点只是一个前缀,但不是一个完整的键,则此字段可能为
null
或表示没有值。 - isEndOfWord/isKey (布尔值): 一个标记,指示当前节点是否代表一个完整的键的结束。这在区分一个前缀 vs. 一个完整的键时非常有用。
- 子节点指针(Children): 通常是一个 Map 或数组,用于存储指向下一个字符/元素的子节点的引用。例如,
-
插入(
put(key, value)
):- 从根节点开始。
- 遍历键的每个字符。对于每个字符:
- 如果当前节点的子节点中已经存在指向该字符的节点,则移动到该子节点。
- 如果不存在,则创建一个新的子节点,并将其添加到当前节点的子节点集合中,然后移动到新创建的节点。
- 当遍历完所有字符后,到达最终节点。将该节点的
value
字段设置为传入的value
,并设置isEndOfWord
为true
。
-
查找(
get(key)
):- 从根节点开始。
- 遍历键的每个字符。对于每个字符:
- 如果当前节点的子节点中不存在指向该字符的节点,则说明键不存在,返回
null
。 - 如果存在,则移动到该子节点。
- 如果当前节点的子节点中不存在指向该字符的节点,则说明键不存在,返回
- 当遍历完所有字符后,到达最终节点。检查该节点的
isEndOfWord
标记。如果为true
,则返回该节点的value
;否则(如果只是一个前缀),返回null
。
-
删除(
remove(key)
):
删除操作相对复杂,需要考虑:- 找到键对应的最终节点。
- 将该节点的
value
设置为null
,并isEndOfWord
设置为false
(逻辑删除)。 - 如果删除后,该节点不再是任何其他键的前缀,并且也没有任何子节点,那么它可以从树中物理删除,需要回溯父节点并移除指向它的引用,一直回溯到第一个有其他作用(是其他键的前缀或有其他子节点)的节点。这通常需要递归实现。
TrieMap 的特性和优势
-
高效的前缀匹配和查找:
- 查找时间复杂度:O(L),其中 L 是键的长度。这比基于哈希表的 Map 在最坏情况下(哈希冲突严重)的 O(L) 或 O(N) 性能更好,而且在平均情况下哈希表是 O(1),但 TrieMap 在键长较短时表现出色,且无哈希冲突问题。
- 特别适合**“前缀搜索”或“自动补全”**:可以直接遍历一个前缀对应的节点及其所有子树,找到所有以该前缀开头的键。
-
空间效率(部分情况下):
- 当键之间有大量公共前缀时,可以显著节省空间,因为共享了节点。
- 然而,如果键之间前缀很少,或者键的字符集非常大,每个节点有大量子节点指针,那么空间开销可能会比 HashMap 大。
-
有序性(基于键的前缀):
虽然不是像 TreeMap 那样按键的整体排序,但 TrieMap 在结构上体现了键的前缀有序性,这使得前缀相关的操作非常自然和高效。 -
键可以是任意序列:
尽管最常见的是字符串,但只要能定义元素的顺序和比较,键可以是任何序列(例如,字节数组,整数数组)。
TrieMap 的应用场景
- 自动补全/拼写检查: 用户输入时,快速提供以当前输入为前缀的建议词汇。
- 路由表: 网络路由器可以使用 Trie 来存储 IP 地址或网络前缀,从而快速查找匹配的路由规则。
- 词典/字典树: 存储大量词汇,进行快速查找、前缀匹配等操作。
- IP 地址查找: 查找某个 IP 地址是否在某个大的网段中。
- DNS 解析: 查找域名对应的 IP 地址。
- 文本搜索匹配: 在文本中查找特定模式。
- 数据压缩: 通过共享前缀来降低存储冗余。
TrieMap 的潜在缺点
- 空间开销: 如果键的前缀共享不多,或者键的字符集很大(导致每个节点子节点Map/数组大而稀疏),空间效率可能不高。
- 实现复杂度: 相对于 HashMap,实现 TrieMap 更复杂,尤其是删除操作。
- 非随机访问: 无法像数组那样通过索引直接访问,访问任何一个键都需要从根节点遍历到对应节点。
与 HashMap/TreeMap 的比较
- HashMap: 最佳平均时间复杂度 O(1) 用于
get
,put
。不保证键的顺序。不擅长前缀搜索。 - TreeMap: 基于红黑树实现,所有操作都是 O(log N)。键是排序的。支持范围查询,但前缀搜索不如 TrieMap 直观和高效。
- TrieMap: 最佳时间复杂度 O(L) (键长)。特别擅长前缀搜索及自动补全。在大量键有公共前缀时空间效率高。
总结
TrieMap 是一种非常有用且强大的数据结构,它利用前缀树的特性,在处理字符串(或其他序列)键的映射和前缀相关操作时展现出卓越的性能。理解其节点结构和操作原理是掌握它的关键。在需要高效前缀搜索和存储大量相关键的场景下,TrieMap 是一个值得考虑的优秀选择。
package trie_map
// TrieNode 表示字典树中的一个节点,包含可选值和子节点指针数组
type TrieNode[T any] struct {
val *T // 节点存储的值,如果为 nil 表示不是一个完整 key
children []*TrieNode[T] // 子节点数组,长度为字符集大小 R
}
// NewTrieNode 创建一个新的 TrieNode,初始化子节点数组长度为 R
func NewTrieNode[T any](R int) *TrieNode[T] {
return &TrieNode[T]{
children: make([]*TrieNode[T], R), // 初始化长度为 R 的子节点数组
}
}
// TrieMap 是一个基于 Trie 的映射结构,支持字符串键和值的泛型映射
type TrieMap[T any] struct {
R int // 字符集大小,例如 256 表示 ASCII 字符集
size int // 当前存储的键值对数量
root *TrieNode[T] // Trie 树的根节点
}
// NewTrieMap 创建一个空的 TrieMap,使用默认的 ASCII 字符集大小 256
func NewTrieMap[T any]() *TrieMap[T] {
trieMap := &TrieMap[T]{
size: 0,
R: 256, // 默认支持 ASCII 范围内的字符
}
trieMap.root = NewTrieNode[T](trieMap.R) // 初始化根节点
return trieMap
}
// Size 返回 TrieMap 中键值对的数量
func (tm *TrieMap[T]) Size() int {
return tm.size
}
// GetNode 查找 key 对应的终止节点(若存在)
func GetNode[T any](node *TrieNode[T], key string) *TrieNode[T] {
if node == nil {
return nil
}
p := node
for i := 0; i < len(key); i++ {
if p == nil {
return nil
}
var c byte = key[i]
p = p.children[c] // 向下查找子节点
}
return p
}
// Get 返回 key 对应的值指针,若不存在则返回 nil
func (tm *TrieMap[T]) Get(key string) *T {
node := GetNode(tm.root, key)
if node == nil || node.val == nil {
return nil
}
return node.val
}
// ContainsKey 判断是否存在指定 key
func (tm *TrieMap[T]) ContainsKey(key string) bool {
return tm.Get(key) != nil
}
// HasKeyWithPrefix 判断是否存在某个以 prefix 为前缀的 key
func (tm *TrieMap[T]) HasKeyWithPrefix(prefix string) bool {
return GetNode(tm.root, prefix) != nil
}
// ShortestPrefixOf 查找 query 的最短前缀,该前缀在 TrieMap 中存在
func (tm *TrieMap[T]) ShortestPrefixOf(query string) string {
p := tm.root
for i := 0; i < len(query); i++ {
if p == nil {
break
}
if p.val != nil {
return query[:i] // 找到前缀匹配
}
var c byte = query[i]
p = p.children[c]
}
if p != nil && p.val != nil {
return query // 整个 query 是前缀
}
return "" // 没有任何前缀匹配
}
// LongestPrefixOf 查找 query 的最长前缀,该前缀在 TrieMap 中存在
func (tm *TrieMap[T]) LongestPrefixOf(query string) string {
node := tm.root
max_len := 0
for i := 0; i < len(query); i++ {
if node == nil {
break
}
if node.val != nil {
max_len = i
}
var c byte = query[i]
node = node.children[c]
}
if node != nil && node.val != nil {
return query // 整个 query 是匹配项
}
return query[:max_len] // 返回最长匹配前缀
}
// KeysWithPrefix 返回所有以 prefix 开头的键
func (tm *TrieMap[T]) KeysWithPrefix(prefix string) []string {
var keys []string = make([]string, 0)
node := GetNode[T](tm.root, prefix)
if node == nil {
return keys // 没有该前缀
}
tm.traverseForKeysWithPrefix(node, prefix, &keys)
return keys
}
// traverseForKeysWithPrefix 递归收集所有以当前路径为前缀的 key
func (tm *TrieMap[T]) traverseForKeysWithPrefix(node *TrieNode[T], currentPath string, res *[]string) {
if node == nil {
return
}
if node.val != nil {
*res = append(*res, currentPath) // 找到一个完整 key
}
for i := 0; i < tm.R; i++ {
currentPath = currentPath + string(byte(i))
tm.traverseForKeysWithPrefix(node.children[i], currentPath, res)
currentPath = currentPath[:len(currentPath)-1] // 回溯
}
}
// KeysWithPattern 查找所有匹配模式的 key,支持通配符 '.'
func (tm *TrieMap[T]) KeysWithPattern(pattern string) []string {
var keys []string = make([]string, 0)
tm.traverseForKeysWithPattern(tm.root, "", pattern, 0, &keys)
return keys
}
// traverseForKeysWithPattern 回溯遍历支持通配符的模式匹配
func (tm *TrieMap[T]) traverseForKeysWithPattern(node *TrieNode[T], path string, pattern string, i int, keys *[]string) {
if node == nil {
return
}
if i == len(pattern) {
if node.val != nil {
*keys = append(*keys, path)
}
return
}
c := pattern[i]
if c == '.' {
for j := 0; j < tm.R; j++ {
path = path + string(byte(j))
tm.traverseForKeysWithPattern(node.children[j], path, pattern, i+1, keys)
path = path[:len(path)-1]
}
} else {
path = path + string(byte(c))
tm.traverseForKeysWithPattern(node.children[c], path, pattern, i+1, keys)
path = path[:len(path)-1]
}
}
// HasKeyWithPattern 判断是否存在匹配指定模式的 key
func (tm *TrieMap[T]) HasKeyWithPattern(pattern string) bool {
return len(tm.KeysWithPattern(pattern)) > 0
}
// Put 插入或更新 key 对应的值
func (tm *TrieMap[T]) Put(key string, v T) {
if !tm.ContainsKey(key) {
tm.size++ // 是新增 key
}
tm.root = tm.putNode(tm.root, key, &v, 0)
}
// putNode 递归构建节点路径,直到 key 的末尾
func (tm *TrieMap[T]) putNode(node *TrieNode[T], key string, val *T, i int) *TrieNode[T] {
if node == nil {
node = NewTrieNode[T](tm.R)
}
if i == len(key) {
node.val = val // 在最后一个节点上存储值
return node
}
c := key[i]
node.children[c] = tm.putNode(node.children[c], key, val, i+1)
return node
}
// Remove 从 TrieMap 中删除 key
func (tm *TrieMap[T]) Remove(key string) {
if !tm.ContainsKey(key) {
return // key 不存在
}
tm.root = tm.removeNode(tm.root, key, 0)
tm.size--
}
// removeNode 删除 key 路径上的值,必要时清除无用节点
func (tm *TrieMap[T]) removeNode(node *TrieNode[T], key string, i int) *TrieNode[T] {
if node == nil {
return nil
}
if i == len(key) {
node.val = nil // 删除节点值
} else {
c := key[i]
node.children[c] = tm.removeNode(node.children[c], key, i+1)
}
if node.val != nil {
return node
}
for i := 0; i < tm.R; i++ {
if node.children[i] != nil {
return node // 有孩子不能删除
}
}
return nil // 无值无子,删除此节点
}
package trie_map
import (
"reflect"
"testing"
)
func TestTrieMap_BasicOperations(t *testing.T) {
trie := NewTrieMap[int]()
// Test Put and Get
trie.Put("apple", 100)
trie.Put("app", 200)
trie.Put("banana", 300)
if v := trie.Get("apple"); v == nil || *v != 100 {
t.Errorf("expected 100, got %v", v)
}
if v := trie.Get("app"); v == nil || *v != 200 {
t.Errorf("expected 200, got %v", v)
}
if v := trie.Get("banana"); v == nil || *v != 300 {
t.Errorf("expected 300, got %v", v)
}
if v := trie.Get("unknown"); v != nil {
t.Errorf("expected nil for unknown key, got %v", *v)
}
// Test ContainsKey
if !trie.ContainsKey("apple") {
t.Error("expected ContainsKey(\"apple\") to be true")
}
if trie.ContainsKey("unknown") {
t.Error("expected ContainsKey(\"unknown\") to be false")
}
// Test Size
if size := trie.Size(); size != 3 {
t.Errorf("expected size 3, got %d", size)
}
}
func TestTrieMap_PrefixAndPattern(t *testing.T) {
trie := NewTrieMap[int]()
trie.Put("apple", 1)
trie.Put("app", 2)
trie.Put("apricot", 3)
trie.Put("bat", 4)
trie.Put("ball", 5)
// Test KeysWithPrefix
prefixKeys := trie.KeysWithPrefix("ap")
expected := []string{"app", "apple", "apricot"}
if !reflect.DeepEqual(stringSet(prefixKeys), stringSet(expected)) {
t.Errorf("KeysWithPrefix failed, got %v, expected %v", prefixKeys, expected)
}
// Test ShortestPrefixOf
query := "applepie"
shortest := trie.ShortestPrefixOf(query)
if shortest != "app" {
t.Errorf("expected shortest prefix to be 'app', got %s", shortest)
}
// Test LongestPrefixOf
longest := trie.LongestPrefixOf(query)
if longest != "apple" {
t.Errorf("expected longest prefix to be 'apple', got %s", longest)
}
// Test KeysWithPattern
trie.Put("bake", 6)
patternKeys := trie.KeysWithPattern("ba..")
expectedPattern := []string{"ball", "bake"}
if !reflect.DeepEqual(stringSet(patternKeys), stringSet(expectedPattern)) {
t.Errorf("KeysWithPattern failed, got %v, expected %v", patternKeys, expectedPattern)
}
// Test HasKeyWithPattern
if !trie.HasKeyWithPattern("b.ll") {
t.Error("expected HasKeyWithPattern(\"b.ll\") to be true")
}
}
func TestTrieMap_Remove(t *testing.T) {
trie := NewTrieMap[int]()
trie.Put("dog", 10)
trie.Put("dot", 20)
trie.Remove("dog")
if trie.ContainsKey("dog") {
t.Error("expected 'dog' to be removed")
}
if trie.Size() != 1 {
t.Errorf("expected size to be 1 after removal, got %d", trie.Size())
}
// remove nonexistent
trie.Remove("notfound")
if trie.Size() != 1 {
t.Error("removing nonexistent key should not change size")
}
}
// Helper: make order-insensitive string slice comparison
func stringSet(list []string) map[string]struct{} {
set := make(map[string]struct{})
for _, s := range list {
set[s] = struct{}{}
}
return set
}