前缀树,也称为字典树,是一种常见的数据结构,用于高效存储和检索字符串集合。
根结点不包含字符,除根结点外每一个结点都只包含一个字符。 这意味着前缀树的每个节点代表一个字符,从根节点到叶节点的路径构成一个字符串。
从根结点到某一结点,路径上经过的字符连接起来,为该结点对应的字符串。 前缀树的路径表示了存储在树中的字符串。
每个结点的所有子结点包含的字符都不相同。 这确保了树的每个分支都代表不同的字符,避免了冲突。
Trie 的形状和单词的插入或删除顺序无关,也就是说对于任意给定的一组单词,Trie 的形状都是唯一的。 这是前缀树的一个重要特性,使其在存储和检索字符串集合时非常强大。
“apple”
“applet”
“bat”
“cat”
(root)
/ | \
a b c
/ / \ /
p e a a
/ / \ \
p e t t
/ \
l r
/
e
/
t
└──a
│
│ └──p
│ │
│ │ └──p
│ │ │
│ │ │ └──l
│ │ │ │
│ │ │ │ └──e (end)
│ │ │ │ │
│ │ │ │ │ └──t (end)
│ │ │ │ │ │
└──b
│
│ └──a
│ │
│ │ └──t (end)
│ │ │
│ └──e
│ │
│ │ └──e (end)
│ │ │
│ │ │ └──r (end)
│ │ │ │
└──c
│
│ └──a
│ │
│ │ └──t (end)
│ │ │
package main
import (
"fmt"
)
type TrieNode struct {
children map[rune]*TrieNode
isEnd bool
}
func NewTrieNode() *TrieNode {
return &TrieNode{children: make(map[rune]*TrieNode)}
}
func (node *TrieNode) Insert(word string) {
for _, char := range word {
if _, exists := node.children[char]; !exists {
node.children[char] = NewTrieNode()
}
node = node.children[char]
}
node.isEnd = true
}
func (node *TrieNode) Search(word string) bool {
for _, char := range word {
if _, exists := node.children[char]; !exists {
return false
}
node = node.children[char]
}
return node.isEnd
}
func (node *TrieNode) PrintTrie(prefix string) {
if node == nil {
return
}
fmt.Printf("%s%s\n", prefix, string(' '))
for char, child := range node.children {
fmt.Printf("%s└──%s", prefix, string(char))
if child.isEnd {
fmt.Println(" (end)")
} else {
fmt.Println()
}
child.PrintTrie(prefix + "│ ")
}
}
func main() {
root := NewTrieNode()
words := []string{"apple", "applet", "bat", "cat", "bee", "beer"}
for _, word := range words {
root.Insert(word)
}
fmt.Println(root.Search("apple")) // 输出 true
fmt.Println(root.Search("app")) // 输出 false
fmt.Println(root.Search("beer")) // 输出 true
fmt.Println(root.Search("batt")) // 输出 false
}
插入操作 (Insert 方法):在最坏情况下,需要遍历输入单词的所有字符,并在前缀树中插入相应的节点。如果树的高度是 h,那么插入的时间复杂度是 O(h),其中 h 取决于输入数据的特性。对于平衡的前缀树,h 大约等于输入单词的最大长度。因此,插入操作的平均时间复杂度是 O(L),其中 L 是输入单词的平均长度。
搜索操作 (Search 方法):搜索操作与输入单词的长度成正比,因为它需要遍历输入单词的所有字符并在前缀树中查找相应的节点。因此,搜索操作的时间复杂度也是 O(L),其中 L 是输入单词的长度。
空间复杂度:
在前缀树中存储单词所需的空间取决于输入单词的数量和长度。每个字符都需要一个节点,并且节点之间可能共享相同的字符(例如,“apple” 和 “applet” 共享 “app” 部分的节点)。因此,前缀树的空间复杂度与存储的单词数量和字符数量成正比。在最坏情况下,空间复杂度可能是 O(NM),其中 N 是单词的数量,M 是单词的总字符数量。
额外的空间消耗来自于程序的堆栈空间,用于递归或循环插入和搜索操作。这个堆栈空间消耗通常较小,取决于树的深度和递归的深度。
总的来说,前缀树的时间复杂度主要取决于输入数据的特性,而空间复杂度取决于存储的单词数量和字符数量。前缀树在处理大量文本数据和字符串匹配时非常有效,但在存储大量短字符串时可能会消耗较多的内存。
前缀树在许多字符串处理和文本搜索的应用中非常有用。一些常见的应用场景包括:
词频统计: 前缀树可以用于有效地统计一组文本中的单词出现频率。通过在前缀树中存储单词,并在搜索时递增计数,可以快速计算出单词的频率。
前缀统计: 前缀树可以用于查找具有特定前缀的单词或字符串。这在自动补全和搜索引擎中非常有用,因为用户可以输入前缀并获得相关建议。
字典和拼写检查: 前缀树可以用于实现字典和拼写检查功能。通过将字典中的单词存储在前缀树中,可以快速检查拼写错误并提供建议。
IP地址路由: 前缀树还用于路由表中,以快速查找最长匹配的IP地址前缀。
总的来说,前缀树是一种非常强大的数据结构,适用于处理字符串和文本数据的各种应用。通过了解其基本性质和实现,可以更好地理解如何使用它来解决各种问题。