https://labuladong.online/algo/data-structure/trie-implement/#trieset-%E7%9A%84%E5%AE%9E%E7%8E%B0
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,初始化子节点数组长度为 Rfunc 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 字符集大小 256func 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 对应的值指针,若不存在则返回 nilfunc (tm *TrieMap[T]) Get(key string) *T { node := GetNode(tm.root, key) if node == nil || node.val == nil { return nil } return node.val}
// ContainsKey 判断是否存在指定 keyfunc (tm *TrieMap[T]) ContainsKey(key string) bool { return tm.Get(key) != nil}
// HasKeyWithPrefix 判断是否存在某个以 prefix 为前缀的 keyfunc (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 递归收集所有以当前路径为前缀的 keyfunc (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 判断是否存在匹配指定模式的 keyfunc (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 中删除 keyfunc (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 comparisonfunc stringSet(list []string) map[string]struct{} { set := make(map[string]struct{}) for _, s := range list { set[s] = struct{}{} } return set}