数据结构编程实践20讲(Python版)—19字典树

本文目录

    • [19 字典树(Trie)](#19 字典树(Trie))
      • [S1 说明](#S1 说明)
      • [S2 示例](#S2 示例)
      • [S3 应用1:基于 big.txt 实现单词的自动补全功能](#S3 应用1:基于 big.txt 实现单词的自动补全功能)
      • [S3 应用2:实现 IP 路由中的最长前缀匹配](#S3 应用2:实现 IP 路由中的最长前缀匹配)
      • [S3 应用3:基于 Trie 的压缩算法(LZW 算法)](#S3 应用3:基于 Trie 的压缩算法(LZW 算法))

往期链接

01 数组 02 链表 03 栈 04 队列 05 二叉树 06 二叉搜索树 07 AVL树 08 红黑树 09 B树 10 B+树
11 线段树 12 树状数组 13 图形数据结构 14 邻接矩阵 15 完全图 16 有向图 17 散列 18 哈希表

19 字典树(Trie)

S1 说明

字典树,又称为前缀树,是一种树形数据结构,主要用于存储字符串集合,用于高效地完成字符串的插入和查找操作。树的核心思想是利用字符串的公共前缀 来减少存储空间和查找时间。

字典树结构
  • 节点(Node):每个节点代表一个字符串中的一个字符。
  • 边(Edge):从父节点到子节点的连接,表示字符的连接关系。
  • 根节点(Root):一个空节点,代表空字符串的开始。
  • 结束标志(Terminal Flag):用于标识某个节点是否为某个字符串的结尾。
字典树的构建与查找
  • 构建(Insertion)
    • 从根节点开始。
    • 对于要插入的字符串,从第一个字符开始,逐个字符向下寻找对应的子节点。
    • 如果子节点存在,则移动到子节点;否则,创建新的子节点。
    • 重复上述步骤,直到处理完字符串的所有字符。
    • 标记当前节点为结束标志,表示一个完整字符串的结束。
  • 查找(Search)
    • 从根节点开始。
    • 按照要查找的字符串,从第一个字符开始,逐个字符向下寻找对应的子节点。
    • 如果在某一步无法找到对应的子节点,则表示该字符串不在Trie中。
    • 如果成功找到所有字符对应的节点,并且最后的节点标记为结束标志,则表示Trie中存在该字符串。
字典树的特点

1. 优势

  • 高效的字符串查询:Trie树可以在 O(m) 的时间内完成字符串的插入、删除和查找操作,其中 m 为字符串的长度。这与集合中元素的数量无关,避免了传统搜索算法中 O(log n) 或 O(n) 的时间复杂度。
  • 前缀匹配:Trie天然支持前缀查询,可以方便地实现以某个前缀开头的所有字符串的检索。
  • 节省存储空间:通过共享公共前缀,Trie可以减少存储重复的字符,尤其在字符串集合存在大量公共前缀的情况下。

2. 劣势

  • 空间占用较大:对于字符集较大的情况(如UNICODE字符集),Trie的节点分支数会很大,可能导致空间浪费。
  • 实现复杂度:相比于哈希表等数据结构,Trie的实现要复杂一些,维护起来也更为繁琐。
  • 不支持部分操作:Trie不适合用于需要对字符串进行排序或范围查询的场景,因为它不维护元素的顺序关系。
字典树的应用领域

1. 字符串检索与自动补全

  • 输入法:在输入法中,利用Trie可以高效地完成对用户输入前缀的匹配,提供候选词的自动补全。
  • 搜索引擎:提供搜索关键词的实时提示功能,根据用户输入的前缀,快速返回可能的搜索词。

2. 词典存储与拼写检查

  • 拼写检查器:将词典中的所有单词存储在Trie中,可以快速判断一个单词是否正确,以及提供可能的拼写建议。
  • 敏感词过滤:利用Trie存储敏感词汇,在文本处理中快速检测并屏蔽敏感词。

3. 路由表和前缀匹配

  • 网络路由表:在网络路由中,使用Trie(如Prefix Tree,前缀树)实现最长前缀匹配,快速确定数据包的转发路径。

4. 字符串统计与分析

  • 统计字符串出现次数:在Trie的节点中维护计数器,可以统计字符串或前缀的出现频率,用于文本分析和数据挖掘。

5. DNA序列分析

  • 生物信息学:DNA序列由A、C、G、T组成,利用Trie可以高效地存储和检索DNA序列片段,进行模式匹配和序列分析。

6. 压缩算法

  • 压缩算法中的字典构建:如LZ前缀编码算法,利用Trie构建编码字典,实现数据的压缩。

7. 多模式串匹配

  • Aho-Corasick算法:构建Trie树并结合失败指针,实现同时匹配多种模式串,应用于病毒检测、文本搜索等领域。

S2 示例

python 复制代码
class TrieNode:
    """Trie 树的节点"""

    def __init__(self):
        self.children = {}  # 子节点字典,键为字符,值为 TrieNode
        self.is_end_of_word = False  # 标记是否为单词的结尾


class Trie:
    """Trie 树"""

    def __init__(self):
        self.root = TrieNode()

    def insert(self, word):
        """在 Trie 中插入一个单词"""
        node = self.root
        for char in word:
            if char not in node.children:
                # 如果子节点中没有当前字符,创建一个新的 TrieNode
                node.children[char] = TrieNode()
            node = node.children[char]
        # 单词结束,标记结尾
        node.is_end_of_word = True

    def search(self, word):
        """在 Trie 中搜索一个单词"""
        node = self.root
        for char in word:
            if char not in node.children:
                return False  # 未找到当前字符,返回 False
            node = node.children[char]
        return node.is_end_of_word  # 检查是否为单词的结尾

    def starts_with(self, prefix):
        """判断 Trie 中是否存在以指定前缀 prefix 开头的单词"""
        node = self.root
        for char in prefix:
            if char not in node.children:
                return False  # 未找到当前前缀
            node = node.children[char]
        return True  # 找到前缀

    def _traverse(self, node, prefix, words):
        """辅助函数,用于深度优先遍历 Trie,收集单词"""
        if node.is_end_of_word:
            words.append(prefix)
        for char, child_node in node.children.items():
            self._traverse(child_node, prefix + char, words)

    def get_words_with_prefix(self, prefix):
        """获取 Trie 中所有以指定前缀 prefix 开头的单词"""
        node = self.root
        for char in prefix:
            if char not in node.children:
                return []  # 前缀不存在,返回空列表
            node = node.children[char]
        words = []
        self._traverse(node, prefix, words)
        return words


# 测试代码
if __name__ == "__main__":
    trie = Trie()
    # 插入单词列表
    words_to_insert = ["apple", "app", "apply", "apt", "bat", "ball", "banana"]
    for word in words_to_insert:
        trie.insert(word)

    # 测试搜索单词
    words_to_search = ["app", "apple", "apt", "apart", "batman", "banana"]
    for word in words_to_search:
        found = trie.search(word)
        print(f"单词 '{word}' 在 Trie 中{'存在' if found else '不存在'}。")

    # 测试前缀查询
    prefixes = ["app", "ba", "cat"]
    for prefix in prefixes:
        has_prefix = trie.starts_with(prefix)
        print(f"Trie 中{'存在' if has_prefix else '不存在'}以 '{prefix}' 为前缀的单词。")
        if has_prefix:
            words_with_prefix = trie.get_words_with_prefix(prefix)
            print(f"以 '{prefix}' 为前缀的单词有:{words_with_prefix}")

结果

python 复制代码
单词 'app' 在 Trie 中存在。
单词 'apple' 在 Trie 中存在。
单词 'apt' 在 Trie 中存在。
单词 'apart' 在 Trie 中不存在。
单词 'batman' 在 Trie 中不存在。
单词 'banana' 在 Trie 中存在。
Trie 中存在以 'app' 为前缀的单词。
以 'app' 为前缀的单词有:['app', 'apple', 'apply']
Trie 中存在以 'ba' 为前缀的单词。
以 'ba' 为前缀的单词有:['bat', 'ball', 'banana']
Trie 中不存在以 'cat' 为前缀的单词。

S3 应用1:基于 big.txt 实现单词的自动补全功能

需要下载包含大量英文单词和语料的 big.txt 文件,才能正常运行程序。该文件下载地址:big.txt

python 复制代码
import re
from collections import defaultdict


class TrieNode:
    """Trie 树的节点"""

    def __init__(self):
        self.children = {}  # 子节点
        self.is_end_of_word = False  # 是否为完整单词
        self.frequency = 0  # 词频,用于排序
        self.word = None  # 完整单词


class AutoCompleteTrie:
    """自动补全功能的 Trie 树"""

    def __init__(self):
        self.root = TrieNode()

    def insert(self, word, frequency=1):
        """插入单词及其词频"""
        node = self.root
        for char in word:
            if char not in node.children:
                node.children[char] = TrieNode()
            node = node.children[char]
        node.is_end_of_word = True
        node.frequency += frequency
        node.word = word

    def search(self, prefix):
        """查找所有以 prefix 为前缀的单词"""
        node = self.root
        for char in prefix:
            if char not in node.children:
                return []  # 无匹配的前缀,返回空列表
            node = node.children[char]
        # 使用优先队列(堆)存储匹配的单词,按照词频排序
        words = []
        self._dfs(node, words)
        # 按照词频从高到低排序
        words.sort(key=lambda x: (-x[1], x[0]))
        return [word for word, freq in words]

    def _dfs(self, node, words):
        """深度优先搜索,收集单词及其词频"""
        if node.is_end_of_word:
            words.append((node.word, node.frequency))
        for child in node.children.values():
            self._dfs(child, words)


def preprocess_text(file_path):
    """读取文本文件,提取单词及其出现频率"""
    word_freq = defaultdict(int)
    with open(file_path, 'r', encoding='utf-8') as f:
        for line in f:
            # 使用正则表达式提取单词,忽略大小写
            words = re.findall(r'\b[a-z]+\b', line.lower())
            for word in words:
                word_freq[word] += 1
    return word_freq


# 主程序
if __name__ == "__main__":
    trie = AutoCompleteTrie()
    # 从 big.txt 文件中提取单词及其频率
    word_freq_dict = preprocess_text('big.txt')
    print("正在构建 Trie 树,请稍候...")
    # 将单词及词频插入 Trie 树
    for word, freq in word_freq_dict.items():
        trie.insert(word, freq)
    print("Trie 树构建完成!")

    # 进入交互式查询
    while True:
        prefix = input("请输入搜索前缀(输入'exit'退出):").strip().lower()
        if prefix == 'exit':
            break
        suggestions = trie.search(prefix)
        if suggestions:
            print("自动补全建议:")
            for word in suggestions[:10]:  # 只显示前 10 个建议
                print(f"- {word}")
        else:
            print("未找到匹配的建议。")

结果

python 复制代码
正在构建 Trie 树,请稍候...
Trie 树构建完成!
请输入搜索前缀(输入'exit'退出):ty
自动补全建议:
- type
- typical
- ty
- types
- typically
- tyranny
- tyrant
- tyre
请输入搜索前缀(输入'exit'退出):an
自动补全建议:
- an
- anger
- answer
- analogy
- analysis
- ancestor
- ancient
- and
- angel
- angle
请输入搜索前缀(输入'exit'退出):exit

S3 应用2:实现 IP 路由中的最长前缀匹配

python 复制代码
class TrieNode:
    """Trie 树的节点,用于 IP 前缀匹配"""

    def __init__(self):
        self.children = {}
        self.next_hop = None  # 保存路由的下一跳信息


class IPRoutingTrie:
    """IP 路由的 Trie 实现"""

    def __init__(self):
        self.root = TrieNode()

    def insert(self, ip_prefix, next_hop):
        """
        插入路由前缀
        :param ip_prefix: 形如 '192.168.0.0/16'
        :param next_hop: 下一跳信息
        """
        ip, prefix_length = ip_prefix.split('/')
        prefix_length = int(prefix_length)
        binary_ip = self._ip_to_binary(ip)
        node = self.root
        for bit in binary_ip[:prefix_length]:
            if bit not in node.children:
                node.children[bit] = TrieNode()
            node = node.children[bit]
        node.next_hop = next_hop

    def search(self, ip_address):
        """查找目的 IP 地址的下一跳信息,使用最长前缀匹配"""
        binary_ip = self._ip_to_binary(ip_address)
        node = self.root
        last_match = None
        for bit in binary_ip:
            if bit in node.children:
                node = node.children[bit]
                if node.next_hop is not None:
                    last_match = node.next_hop
            else:
                break
        return last_match

    def _ip_to_binary(self, ip):
        """将 IP 地址转换为 32 位二进制字符串"""
        octets = ip.split('.')
        binary_ip = ''.join([format(int(octet), '08b') for octet in octets])
        return binary_ip


# 测试代码
if __name__ == "__main__":
    routing_table = IPRoutingTrie()
    # 添加路由前缀
    routing_entries = [
        ("192.168.0.0/16", "Router A"),
        ("192.168.1.0/24", "Router B"),
        ("10.0.0.0/8", "Router C"),
        ("0.0.0.0/0", "Default Gateway"),
    ]
    for prefix, next_hop in routing_entries:
        routing_table.insert(prefix, next_hop)

    # 查找目的 IP 地址的下一跳
    test_ips = [
        "192.168.1.100",
        "192.168.2.50",
        "10.1.2.3",
        "8.8.8.8",
    ]
    for ip in test_ips:
        next_hop = routing_table.search(ip)
        print(f"目的 IP {ip} 的下一跳为:{next_hop}")

结果

python 复制代码
目的 IP 192.168.1.100 的下一跳为:Router B
目的 IP 192.168.2.50 的下一跳为:Router A
目的 IP 10.1.2.3 的下一跳为:Router C
目的 IP 8.8.8.8 的下一跳为:None

S3 应用3:基于 Trie 的压缩算法(LZW 算法)

python 复制代码
class TrieNode:
    """Trie 节点,用于 LZW 压缩算法"""

    def __init__(self, index=None):
        self.children = {}  # 子节点
        self.index = index  # 节点对应的词典索引


def lzw_compress(uncompressed):
    """使用 LZW 算法进行压缩,使用 Trie 优化"""
    # 初始化词典,包含所有单字符
    dict_size = 256
    root = TrieNode()
    for i in range(dict_size):
        root.children[chr(i)] = TrieNode(index=i)

    result = []
    node = root
    w = ''
    for c in uncompressed:
        if c in node.children:
            node = node.children[c]
            w += c
        else:
            # 输出 w 的词典索引
            result.append(node.index)
            # 新的词典条目
            dict_size += 1
            node.children[c] = TrieNode(index=dict_size - 1)
            # 从根节点开始处理新字符
            node = root.children[c]
            w = c
    # 输出最后一个字符串的索引
    if w:
        result.append(node.index)
    return result


def lzw_decompress(compressed):
    """使用 LZW 算法进行解压"""
    # 初始化词典,包含所有单字符
    dict_size = 256
    dictionary = {i: chr(i) for i in range(dict_size)}

    result = []
    w = chr(compressed.pop(0))
    result.append(w)
    for k in compressed:
        if k in dictionary:
            entry = dictionary[k]
        elif k == dict_size:
            # 处理特殊情况
            entry = w + w[0]
        else:
            raise ValueError('Bad compressed k: %s' % k)
        result.append(entry)
        # 新的词典条目
        dictionary[dict_size] = w + entry[0]
        dict_size += 1
        w = entry
    return ''.join(result)


# 测试代码
if __name__ == "__main__":
    data = "TOBEORNOTTOBEORTOBEORNOT" * 3
    print("原始数据:", data)
    compressed = lzw_compress(data)
    print("压缩结果:", compressed)
    decompressed = lzw_decompress(compressed.copy())
    print("解压结果:", decompressed)
    print("压缩比:", len(compressed) / len(data))

结果

python 复制代码
原始数据: TOBEORNOTTOBEORTOBEORNOTTOBEORNOTTOBEORTOBEORNOTTOBEORNOTTOBEORTOBEORNOT
压缩结果: [84, 79, 66, 69, 79, 82, 78, 79, 84, 256, 258, 260, 265, 259, 261, 263, 268, 260, 262, 264, 257, 269, 272, 270, 275, 266, 279, 278, 278, 274]
解压结果: TOBEORNOTTOBEORTOBEORNOTTOBEORNOTTOBEORTOBEORNOTTOBEORNOTTOBEORTOBEORNOT
压缩比: 0.4166666666666667
相关推荐
学不会•1 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html
网易独家音乐人Mike Zhou3 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
安静读书3 小时前
Python解析视频FPS(帧率)、分辨率信息
python·opencv·音视频
活宝小娜4 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点4 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow4 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o4 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
小二·4 小时前
java基础面试题笔记(基础篇)
java·笔记·python
开心工作室_kaic5 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
刚刚好ā5 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue