• 前缀树(Trie):理解基本性质与应用


    前缀树,也称为字典树,是一种常见的数据结构,用于高效存储和检索字符串集合。

    基本性质:

    • 根结点不包含字符,除根结点外每一个结点都只包含一个字符。 这意味着前缀树的每个节点代表一个字符,从根节点到叶节点的路径构成一个字符串。

    • 从根结点到某一结点,路径上经过的字符连接起来,为该结点对应的字符串。 前缀树的路径表示了存储在树中的字符串。

    • 每个结点的所有子结点包含的字符都不相同。 这确保了树的每个分支都代表不同的字符,避免了冲突。

    • Trie 的形状和单词的插入或删除顺序无关,也就是说对于任意给定的一组单词,Trie 的形状都是唯一的。 这是前缀树的一个重要特性,使其在存储和检索字符串集合时非常强大。

    复杂度分析

    例子

    “apple”
    “applet”
    “bat”
    “cat”

    	          (root)
    	         /  |   \
    	        a   b     c
    	       /   / \    /
    	      p   e   a   a
    	     /   /     \    \
    	    p   e       t    t
    	   /      \    
    	  l         r  
    	 /             
    	e
       /
      t               
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    └──a
    │   
    │  └──p
    │  │   
    │  │  └──p
    │  │  │   
    │  │  │  └──l
    │  │  │  │   
    │  │  │  │  └──e (end)
    │  │  │  │  │   
    │  │  │  │  │  └──t (end)
    │  │  │  │  │  │   
    └──b
    │   
    │  └──a
    │  │   
    │  │  └──t (end)
    │  │  │   
    │  └──e
    │  │   
    │  │  └──e (end)
    │  │  │   
    │  │  │  └──r (end)
    │  │  │  │   
    └──c
    │   
    │  └──a
    │  │   
    │  │  └──t (end)
    │  │  │   
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    前缀树代码

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    复杂度分析

    时间复杂度:

    插入操作 (Insert 方法):在最坏情况下,需要遍历输入单词的所有字符,并在前缀树中插入相应的节点。如果树的高度是 h,那么插入的时间复杂度是 O(h),其中 h 取决于输入数据的特性。对于平衡的前缀树,h 大约等于输入单词的最大长度。因此,插入操作的平均时间复杂度是 O(L),其中 L 是输入单词的平均长度。

    搜索操作 (Search 方法):搜索操作与输入单词的长度成正比,因为它需要遍历输入单词的所有字符并在前缀树中查找相应的节点。因此,搜索操作的时间复杂度也是 O(L),其中 L 是输入单词的长度。

    空间复杂度:

    在前缀树中存储单词所需的空间取决于输入单词的数量和长度。每个字符都需要一个节点,并且节点之间可能共享相同的字符(例如,“apple” 和 “applet” 共享 “app” 部分的节点)。因此,前缀树的空间复杂度与存储的单词数量和字符数量成正比。在最坏情况下,空间复杂度可能是 O(NM),其中 N 是单词的数量,M 是单词的总字符数量。

    额外的空间消耗来自于程序的堆栈空间,用于递归或循环插入和搜索操作。这个堆栈空间消耗通常较小,取决于树的深度和递归的深度。

    总的来说,前缀树的时间复杂度主要取决于输入数据的特性,而空间复杂度取决于存储的单词数量和字符数量。前缀树在处理大量文本数据和字符串匹配时非常有效,但在存储大量短字符串时可能会消耗较多的内存。

    应用场景

    前缀树在许多字符串处理和文本搜索的应用中非常有用。一些常见的应用场景包括:

    • 词频统计: 前缀树可以用于有效地统计一组文本中的单词出现频率。通过在前缀树中存储单词,并在搜索时递增计数,可以快速计算出单词的频率。

    • 前缀统计: 前缀树可以用于查找具有特定前缀的单词或字符串。这在自动补全和搜索引擎中非常有用,因为用户可以输入前缀并获得相关建议。

    • 字典和拼写检查: 前缀树可以用于实现字典和拼写检查功能。通过将字典中的单词存储在前缀树中,可以快速检查拼写错误并提供建议。

    • IP地址路由: 前缀树还用于路由表中,以快速查找最长匹配的IP地址前缀。

    总的来说,前缀树是一种非常强大的数据结构,适用于处理字符串和文本数据的各种应用。通过了解其基本性质和实现,可以更好地理解如何使用它来解决各种问题。

  • 相关阅读:
    在字节4年,一个27岁女软件测试工程师的心路历程
    一本通1084;幂的末尾
    尚医通(四)
    javaWeb项目-校园交友网站功能介绍
    Dubbo消费端调用全过程实现分析-负载均衡与集群容错
    文旅元宇宙解决方案|人工智能、虚拟数字人、导览系统深度应用
    基于stm32控制的ESP8266在设备模式下通讯
    SolVES4.1学习2——导入数据运行模型
    世界杯竞猜项目Dapp-第一章(合约开发)
    【C++】传递‘类非静态成员函数’用作回调函数
  • 原文地址:https://blog.csdn.net/zhaoxilengfeng/article/details/132574966