0

0

高效构建霍夫曼树:无需优先队列的排序策略

DDD

DDD

发布时间:2025-10-04 19:23:01

|

197人浏览过

|

来源于php中文网

原创

高效构建霍夫曼树:无需优先队列的排序策略

本文详细阐述了一种无需优先队列构建霍夫曼树的高效方法。该策略通过初始排序符号列表,并巧妙地管理两个已排序列表(原始符号和已合并符号),确保每次都能快速选取最小频率的两个节点进行合并,从而实现霍夫曼树的构建,同时保持算法的简洁性和效率。

霍夫曼树简介与传统构建方法

霍夫曼树(huffman tree),又称最优二叉树,是一种带权路径长度最短的二叉树。它在数据压缩领域有着广泛应用,通过为出现频率高的字符分配较短的编码,为频率低的字符分配较长的编码,从而实现整体数据量的缩减。

传统的霍夫曼树构建算法通常依赖于优先队列(Priority Queue)。其基本思想是:

  1. 将所有待编码的字符及其频率作为叶子节点,插入到优先队列中(按频率升序排列)。
  2. 重复以下步骤,直到优先队列中只剩下一个节点:
    • 从优先队列中取出频率最小的两个节点。
    • 创建一个新的内部节点,其频率为这两个节点频率之和,并将这两个节点作为其左右子节点。
    • 将新创建的内部节点插入回优先队列。
  3. 优先队列中最后剩下的那个节点即为霍夫曼树的根节点。

然而,在某些特定场景或教学要求下,可能需要避免使用优先队列来实现这一过程。这要求我们寻找一种替代方案,能够在不直接使用优先队列数据结构的情况下,依然高效地找到并合并频率最小的两个节点。

无需优先队列的霍夫曼树构建算法

在不使用优先队列的情况下构建霍夫曼树,可以采用一种巧妙的“双列表排序策略”。这种方法利用了初始排序和列表合并的特性,模拟了优先队列的行为。

核心思想:双列表排序策略

该策略的核心在于维护两个已排序的列表:

  1. 原始符号列表 (Original Symbols List):存放所有初始的字符节点,按频率升序排列。
  2. 已合并符号列表 (Combined Symbols List):存放所有通过合并操作新生成的内部节点,同样按频率升序排列。

在每次合并操作中,我们只需要从这两个列表的头部(即最小频率的元素)中选取两个最小的节点进行合并。由于新合并的节点的频率总是大于或等于其子节点的频率,且通常大于之前已合并节点的频率,因此可以将新节点直接添加到“已合并符号列表”的末尾,而该列表依然能保持其升序特性。

详细步骤

以下是构建霍夫曼树的具体步骤:

  1. 初始化节点列表:

    • 为每个字符创建一个节点对象,包含字符本身、其频率以及左右子节点指针(初始为null)。
    • 将所有这些初始字符节点收集到一个列表中。
  2. 对原始符号列表进行排序:

    • 将步骤1中创建的节点列表按照节点的频率进行升序排序。这个列表将作为我们的“原始符号列表”。
  3. 创建空的已合并符号列表:

    MOKI
    MOKI

    MOKI是美图推出的一款AI短片创作工具,旨在通过AI技术自动生成分镜图并转为视频素材。

    下载
    • 初始化一个空的列表,用于存放后续合并操作产生的内部节点。这个列表将作为我们的“已合并符号列表”。
  4. 循环合并节点:

    • 当“原始符号列表”和“已合并符号列表”中的节点总数大于1时,重复以下操作:

      • 选取频率最小的两个节点:

        • 比较“原始符号列表”的第一个节点和“已合并符号列表”的第一个节点(如果两个列表都不为空)。
        • 从这两个列表中选出频率最小的节点。
        • 重复此过程,选出第二个频率最小的节点。
        • 选择逻辑:
          • 如果“原始符号列表”为空,则从“已合并符号列表”中取出两个最小的节点。
          • 如果“已合并符号列表”为空,则从“原始符号列表”中取出两个最小的节点。
          • 如果两个列表都不为空:
            • 比较两个列表的头部元素,取出频率较小的那个作为第一个节点。
            • 再次比较剩余的两个列表的头部元素,取出频率较小的那个作为第二个节点。
            • (注意:在取出节点后,要从原列表中移除该节点。)
      • 合并节点:

        • 创建一个新的内部节点。
        • 将选出的两个节点作为新节点的左右子节点。
        • 新节点的频率等于其两个子节点的频率之和。
      • 添加至已合并符号列表:

        • 将新创建的内部节点添加到“已合并符号列表”的末尾
        • 原理: 由于新节点的频率是两个较小频率之和,它必然大于或等于其子节点的频率。更重要的是,它通常会大于或等于之前已合并到此列表中的任何节点的频率(因为我们总是从最小的开始合并)。因此,将新节点添加到列表末尾,可以保持“已合并符号列表”的升序特性。
  5. 获取霍夫曼树根节点:

    • 当循环结束时,意味着只剩下一个节点。这个节点就是霍夫曼树的根节点。它可能在“原始符号列表”中(如果初始只有一个字符),也可能在“已合并符号列表”中。

示例伪代码

class Node:
    def __init__(self, char, freq, left=None, right=None):
        self.char = char
        self.freq = freq
        self.left = left
        self.right = right

    # 用于比较节点,便于排序
    def __lt__(self, other):
        return self.freq < other.freq

def build_huffman_tree_without_priority_queue(frequencies):
    # 1. 初始化节点列表
    original_nodes = []
    for char, freq in frequencies.items():
        original_nodes.append(Node(char, freq))

    # 2. 对原始符号列表进行排序
    original_nodes.sort()

    # 3. 创建空的已合并符号列表
    combined_nodes = []

    # 辅助函数:从两个列表中选择频率最小的节点
    def get_min_node(list1, list2):
        if not list1:
            return list2.pop(0)
        if not list2:
            return list1.pop(0)

        if list1[0].freq < list2[0].freq:
            return list1.pop(0)
        else:
            return list2.pop(0)

    # 4. 循环合并节点
    while len(original_nodes) + len(combined_nodes) > 1:
        node1 = get_min_node(original_nodes, combined_nodes)
        node2 = get_min_node(original_nodes, combined_nodes)

        # 合并节点
        merged_freq = node1.freq + node2.freq
        merged_node = Node(None, merged_freq, node1, node2) # 内部节点字符为None

        # 添加至已合并符号列表末尾
        combined_nodes.append(merged_node)

        # 保持 combined_nodes 列表的排序(虽然通常是追加,但为了严谨性或处理特殊情况,可以考虑重新排序或插入排序)
        # 实际上,由于新节点的频率总是大于或等于其子节点,且通常大于已合并列表中的其他节点,直接追加即可保持排序。
        # 如果有特殊情况导致不保持,则需要在此处进行插入排序。但对于霍夫曼树,通常无需。
        # 这里为了简化,假设直接追加是有效的,因为新节点频率最大。

    # 5. 获取霍夫曼树根节点
    if original_nodes:
        return original_nodes[0]
    else:
        return combined_nodes[0]

# 示例使用
frequencies = {'a': 5, 'b': 9, 'c': 12, 'd': 13, 'e': 16, 'f': 45}
huffman_root = build_huffman_tree_without_priority_queue(frequencies)

# 打印霍夫曼编码 (需要一个额外的函数来遍历树并生成编码)
def generate_huffman_codes(node, current_code="", codes={}):
    if node is None:
        return
    if node.char is not None: # 是叶子节点
        codes[node.char] = current_code
        return
    generate_huffman_codes(node.left, current_code + "0", codes)
    generate_huffman_codes(node.right, current_code + "1", codes)
    return codes

huffman_codes = generate_huffman_codes(huffman_root)
print("霍夫曼编码:", huffman_codes)

# 预期输出示例 (编码可能因左右子树分配而异,但频率排序是固定的)
# 霍夫曼编码: {'a': '1100', 'b': '1101', 'c': '100', 'd': '101', 'e': '111', 'f': '0'}

算法优势与原理分析

  • 效率: 初始排序的时间复杂度为 O(N log N),其中 N 是字符的数量。随后的合并操作,每次选择两个最小节点并将其添加到 combined_nodes 列表末尾,此过程重复 N-1 次。每次选择操作(比较两个列表的头部)是 O(1),添加操作也是 O(1)。因此,合并阶段的总时间复杂度为 O(N)。整体算法的时间复杂度由初始排序决定,为 O(N log N),与使用优先队列的霍夫曼算法复杂度相同。
  • 简洁性: 避免了优先队列复杂的内部实现,通过简单的列表操作即可完成。
  • 原理: 关键在于新合并的节点频率总是大于或等于其子节点的频率。当我们从两个已排序列表的头部取出最小元素进行合并时,新生成的父节点的频率必然大于或等于这两个被合并子节点的频率。由于我们总是从“现有”的最小节点开始合并,新生成的父节点其频率也必然大于或等于之前已经合并并加入 combined_nodes 列表的节点。因此,将其直接追加到 combined_nodes 列表的末尾,可以保持该列表的升序特性。

注意事项

  1. 节点结构: 确保自定义的节点类包含字符(或标识符)、频率以及左右子节点的引用。
  2. 空列表处理: 在 get_min_node 辅助函数中,需要妥善处理其中一个列表为空的情况,确保算法能够正确地从非空列表中获取节点。
  3. 频率相同时的处理: 如果存在多个节点频率相同,初始排序和后续选择时,它们的相对顺序可能会影响最终霍夫曼树的形态,但不会影响其最优性(即带权路径长度)。如果需要保证霍夫曼树的唯一性(例如,为了测试),可能需要引入第二个排序标准(如字符的ASCII值)。
  4. 实际应用: 构建霍夫曼树只是第一步,后续还需要遍历树来生成每个字符的霍夫曼编码,以及实现编码和解码功能。

总结

通过采用“双列表排序策略”,我们成功地在不使用优先队列的情况下构建了霍夫曼树。这种方法不仅在理论上与传统优先队列方法具有相同的渐进时间复杂度,而且通过巧妙地利用列表的排序特性,提供了一种简洁而高效的实现途径。这对于理解数据结构和算法的底层机制,以及在特定约束下解决问题,都具有重要的启发意义。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

235

2023.09.22

java中null的用法
java中null的用法

在Java中,null表示一个引用类型的变量不指向任何对象。可以将null赋值给任何引用类型的变量,包括类、接口、数组、字符串等。想了解更多null的相关内容,可以阅读本专题下面的文章。

437

2024.03.01

mysql标识符无效错误怎么解决
mysql标识符无效错误怎么解决

mysql标识符无效错误的解决办法:1、检查标识符是否被其他表或数据库使用;2、检查标识符是否包含特殊字符;3、使用引号包裹标识符;4、使用反引号包裹标识符;5、检查MySQL的配置文件等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

183

2023.12.04

Python标识符有哪些
Python标识符有哪些

Python标识符有变量标识符、函数标识符、类标识符、模块标识符、下划线开头的标识符、双下划线开头、双下划线结尾的标识符、整型标识符、浮点型标识符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

286

2024.02.23

java标识符合集
java标识符合集

本专题整合了java标识符相关内容,想了解更多详细内容,请阅读下面的文章。

258

2025.06.11

c++标识符介绍
c++标识符介绍

本专题整合了c++标识符相关内容,阅读专题下面的文章了解更多详细内容。

123

2025.08.07

treenode的用法
treenode的用法

​在计算机编程领域,TreeNode是一种常见的数据结构,通常用于构建树形结构。在不同的编程语言中,TreeNode可能有不同的实现方式和用法,通常用于表示树的节点信息。更多关于treenode相关问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

537

2023.12.01

C++ 高效算法与数据结构
C++ 高效算法与数据结构

本专题讲解 C++ 中常用算法与数据结构的实现与优化,涵盖排序算法(快速排序、归并排序)、查找算法、图算法、动态规划、贪心算法等,并结合实际案例分析如何选择最优算法来提高程序效率。通过深入理解数据结构(链表、树、堆、哈希表等),帮助开发者提升 在复杂应用中的算法设计与性能优化能力。

17

2025.12.22

Python 自然语言处理(NLP)基础与实战
Python 自然语言处理(NLP)基础与实战

本专题系统讲解 Python 在自然语言处理(NLP)领域的基础方法与实战应用,涵盖文本预处理(分词、去停用词)、词性标注、命名实体识别、关键词提取、情感分析,以及常用 NLP 库(NLTK、spaCy)的核心用法。通过真实文本案例,帮助学习者掌握 使用 Python 进行文本分析与语言数据处理的完整流程,适用于内容分析、舆情监测与智能文本应用场景。

9

2026.01.27

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
HTML5/CSS3/JavaScript/ES6入门课程
HTML5/CSS3/JavaScript/ES6入门课程

共102课时 | 6.8万人学习

前端基础到实战(HTML5+CSS3+ES6+NPM)
前端基础到实战(HTML5+CSS3+ES6+NPM)

共162课时 | 19万人学习

第二十二期_前端开发
第二十二期_前端开发

共119课时 | 12.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号