【数据结构】- 详解哈夫曼树(用 C 语言实现哈夫曼树的构造和哈夫曼编码)

news2024/11/27 20:48:51

目录

一、哈夫曼树的基本概念

二、哈夫曼树的构造算法

2.1 - 哈夫曼树的构造过程

2.2 - 哈夫曼树的存储表示

2.3 - 算法实现

三、哈夫曼编码

3.1 - 哈夫曼编码的主要思想

3.2 - 哈夫曼编码的性质

3.3 - 算法实现


 


一、哈夫曼树的基本概念

哈夫曼树的定义,涉及路径、路径长度、权等概念,下面先给出这些概念的定义,然后再介绍哈夫曼树。

  1. 路径:从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径。

  2. 路径长度:路径上的分支数目称作路径长度。

  3. 树的路径长度:从树根到每一结点的路径长度之和。

  4. :赋予某个实体的一个量,对实体的某个或某些属性的数值化描述。在数据结构中,实体有结点(元素)和边(关系)两大类,所以对应有结点权边权。结点权和边权具体代表什么意义,由具体情况决定。如果在一棵树中的结点上带有权值,则对应就有带权树等概念

  5. 结点的带权路径长度:从树根到该结点的路径长度与该结点上权值的乘积。

  6. 树的带权路径长度:树中所有叶子结点的带权路径长度之和,通常记作 。

在含有 n 个叶子结点的二叉树中,每个叶子结点的权值为 ,则其中 WPL 最小的二叉树称作最优二叉树或哈夫曼(Huffman)树

例如,下图所示的 3 棵二叉树中,都含有 4 个叶子结点 a、b、c、d,分别带权 7、5、2、4。

其中 (c) 树的带权路径长度最小,可以验证,它恰为哈夫曼树。

哈夫曼树中具有不同权值的叶子结点的分布有什么特点呢?从上面的例子中,可以直观地发现,在哈夫曼树中,权值越大的结点离根结点越近。根据这个特点,哈夫曼最早给出了一个构造哈夫曼树的方法,称哈夫曼算法。


二、哈夫曼树的构造算法

2.1 - 哈夫曼树的构造过程

  1. 根据给定的 n 个权值 ,构造 n 棵只有根结点的二叉树,这 n 棵二叉树构成一个森林 F(Forest)。

  2. 在森林 F 中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左、右子树上根结点的权值之和。

  3. 在森林 F 中删除这两棵树,同时将新得到的二叉树加入 F 中。

  4. 重复 2 和 3,直到 F 中只含一棵树为止,这棵树便是哈夫曼树。

在构造哈夫曼树时,首先选择权值小的结点,这样保证权值大的结点离根结点较近,这样一来,在计算树的带权路径长度时,自然会得到最小带权路径长度,这种生成算法是一种典型的贪心法

例如,下图所示为上图 (c) 所示的哈夫曼树的构造过程。其中,根结点上标注的数字是所赋的权。

注意:哈夫曼树并不唯一,但 WPL 必然相同且最优

2.2 - 哈夫曼树的存储表示

typedef struct HTNode
{
    int weight;  // 结点的权值
    int parent;  // 结点的双亲的下标
    int left;  // 结点的左孩子的下标
    int right;  // 结点的右孩子的下标
}HTNode;
​
typedef struct HTree
{
    HTNode* data;
    int size;
}HTree;

哈夫曼树中的各结点存储在 data 指向的一个大小为 2n - 1 的动态分配的数组中

解释 1:n 个叶子结点进行 n - 1 次合并,生成 n - 1 个度为 2 的新结点,所以总结点数 N 为 2n - 1

解释 2:因为哈夫曼树中没有度为 1 的结点,所以一棵有 n 个叶子结点的哈夫曼树总共有 2n - 1 结点,即 N = N0 + N1 + N2 = 2N0 - 1 = 2n - 1

将叶子结点集中存储在 data[0] ~ data[n - 1] 中,将非叶子结点存储在 data[n] ~ data[2n - 2] 中

2.3 - 算法实现

构造哈夫曼树算法的实现可以分为两大部分:

  1. 初始化:首先动态申请 2n - 1 个单元,然后循环 2n - 1 次,将所有单元中结点的双亲、左孩子、右孩子的下标都初始化为 -1,如果是前 n 个单元,还要初始化这 n 个单元中叶子结点的权值。

  2. 创建树:循环 n - 1 次,通过 n - 1 次的选择、删除与合并来创建哈夫曼树。

HuffmanTree.c

#include "HuffmanTree.h"
#include <stdlib.h>
​
// 在 HT->data[0] ~ HT->data[end] 中选择两个双亲的下标为 -1 且权值最小的结点,
// 并通过输出型参数 pMinIndex1 和 pMinIndex2 返回它们在 HT->data 中的下标
void Select(HTree* HT, int end, int* pMinIndex1, int* pMinIndex2)
{
    int min1 = INT_MAX;
    for (int i = 0; i <= end; ++i)
    {
        if (HT->data[i].parent == -1 && HT->data[i].weight < min1)
        {
            min1 = HT->data[i].weight;
            *pMinIndex1 = i;
        }
    }
    int min2 = INT_MAX;
    for (int i = 0; i <= end; ++i)
    {
        if (i != *pMinIndex1 && 
            HT->data[i].parent == -1 && HT->data[i].weight < min2)
        {
            min2 = HT->data[i].weight;
            *pMinIndex2 = i;
        }
    }
}
​
HTree* CreateHuffmanTree(int* weight, int n)
{
    // 初始化
    HTree* HT = (HTree*)malloc(sizeof(HTree));
    HT->data = (HTNode*)malloc(sizeof(HTNode) * (2 * n - 1));
    HT->size = 2 * n - 1;
    for (int i = 0; i < 2 * n - 1; ++i)
    {
        if (i < n)
            HT->data[i].weight = weight[i];
​
        HT->data[i].parent = -1;
        HT->data[i].left = -1;
        HT->data[i].right = -1;
    }
    // 创建树
    for (int i = n; i < 2 * n - 1; ++i)
    {
        int minIndex1, minIndex2;
        // 选择
        Select(HT, i - 1, &minIndex1, &minIndex2); 
        // 删除
        HT->data[minIndex1].parent = i;
        HT->data[minIndex2].parent = i;
        // 合并
        HT->data[i].left = minIndex1;
        HT->data[i].right = minIndex2;
        HT->data[i].weight = HT->data[minIndex1].weight + HT->data[minIndex2].weight;
    }
    return HT;
}

Test.c

#include "HuffmanTree.h"  // 包含了哈夫曼树的存储表示以及函数声明
#include <stdio.h>
#include <stdlib.h>
​
void PreOrder(HTree* HT, int rootIndex)
{
    if (rootIndex == -1)
        return;
​
    printf("%d ", HT->data[rootIndex].weight);
    PreOrder(HT, HT->data[rootIndex].left);
    PreOrder(HT, HT->data[rootIndex].right);
}
​
void DestroyHuffmanTree(HTree* HT)
{
    free(HT->data);
    HT->data = NULL;
    HT->size = 0;
}
​
int main()
{
    int weight[8] = { 5, 29, 7, 8, 14, 23, 3, 11 };
    HTree* HT = CreateHuffmanTree(weight, 8);
    PreOrder(HT, HT->size - 1);
    // 100 42 19 8 3 5 11 23 58 29 29 14 15 7 8
    printf("\n");
    DestroyHuffmanTree(HT);
    return 0;
}

 


三、哈夫曼编码

3.1 - 哈夫曼编码的主要思想

在数据通信、数据压缩问题中,需要将数据文件转换成二进制字符 0、1 组成的二进制串,称之为编码

假设待压缩的数据为 "abcdabcdaaaaabbbdd",数据包含 18 个字符,其中只有 a(7 个)、b(5 个)、c(2 个)、d(4 个) 四种字符,如果采用等长编码,每个字符编码取两位即可,编码总长度为 36 位。下表所示为一种等长编码方案。

字符编码
a00
b01
c10
d11

但这并非最优的编码方案,因为每个字符出现的频率不同,如果在编码时考虑字符出现的频率,使频率高的字符采用尽可能短的编码,频率低的字符采用稍长的编码,来构造一种不等长编码,则会获得更好的空间效率,这也是文件压缩技术的核心思想。下表所示为一种不等长编码方案,采用这种编码方案,编码总长度为 35 位。

字符编码
a0
b10
c110
d111

但是对于不等长编码,如果设计得不合理,便会给解码带来困难。例如,对于下表所示的另一种不等长编码方案。

字符编码
a0
b01
c010
d111

采用该编码方案后,上述数据编码后为 "00101111100101111100000010101111111"。但是这样的编码数据无法翻译,例如,传过去的字符串中前 4 个字符的子串 "0010" 就可有不同的译法,或是 "aba",或是 "ac"。因此,若要设计长度不等的编码,必须满足一个条件:任何一个字符的编码都不是另一个字符的编码的前缀(最左子串)

那么如何设计有效的用于数据压缩的二进制编码呢?我们可以利用哈夫曼树来设计。第二个表格所示的编码是以字符 a、b、c、d 在数据串 "abcdabcdaaaaabbbdd" 中出现的次数 7、5、2、4 为权值,构造下图所示的哈夫曼树,约定左分支标记为 0,右分支标记为 1,则根结点到每个叶子结点路径上的 0、1 序列即为相应字符的编码

a、b、c、d 的哈夫曼编码分别为 0、10、110 和 111。

3.2 - 哈夫曼编码的性质

  1. 性质一:哈夫曼编码是前缀编码

    前缀编码:如果在一个编码方案中,任一个编码都不是其他任何编码的前缀(最左子串),则称编码是前缀编码。

    证明:哈夫曼编码是从根结点到叶子结点路径上的编码序列,由树的特点可知,若路径 A 是另一条路径 B 的左部分,则 B 经过了 A,则 A 的终点一定不是叶子结点。而哈夫曼编码对应路径的终点一定为叶子结点,因此,任一哈夫曼编码都不会与任意其他哈夫曼编码的前缀部分完全重叠,因此哈夫曼编码是前缀编码。

  2. 性质二:哈夫曼编码是最优前缀编码

    对于包含 n 个字符的数据文件,分别以它们出现次数为权值构造哈夫曼树,则利用该树对应的哈夫曼编码对文件进行编码,能使文件压缩后对应的二进制文件的长度最短。、

    证明:由哈夫曼树的构造算法可知,出现次数较多的字符对应的编码较短,这便直观地说明了该定理是成立的。

3.3 - 算法实现

在构造哈夫曼树之后,求哈夫曼编码的主要思想是:依次以叶子结点出发,向上回溯至根结点位置。回溯时走左分支则生成代码 0,走右分支则生成代码 1

由于每个哈夫曼编码是变长编码,因此使用一个字符指针数组来存放每个字符编码串的首地址。

#include "HuffmanTree.h"
#include <stdlib.h>
#include <string.h>

char** CreateHuffmanCode(HTree* HT)
{
	int n = (HT->size + 1) / 2;
	char** HC = (char**)malloc(sizeof(char*) * n);
	char* tmp = (char*)malloc(sizeof(char) * n);  // 字符编码的长度一定小于 n
	tmp[n - 1] = '\0';
	// 逐个求 n 个字符的哈夫曼编码
	for (int i = 0; i < n; ++i)
	{
		int pos = n - 2;
		int cur = i;
		int parent = HT->data[i].parent;
		while (parent != -1)
		{
			if (HT->data[parent].left == cur)
				tmp[pos--] = '0';
			else
				tmp[pos--] = '1';
			// 向上回溯
			cur = parent;
			parent = HT->data[parent].parent;
		}
		HC[i] = (char*)malloc(sizeof(char) * (n - 1 - pos));
		strcpy(HC[i], &tmp[pos + 1]);
	}
	free(tmp);
	tmp = NULL;
	return HC;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1291214.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

电脑字体大小怎么设置?学会这3个方法,轻松调节!

“感觉我近视又加深了&#xff0c;最近看电脑居然感觉字体有点小。我想把字体放大一点但却不知道应该怎么操作&#xff0c;有没有朋友可以指导一下我呀&#xff1f;” 在我们的日常生活中&#xff0c;电脑已经成为我们获取信息、交流和娱乐的重要工具。字体大小作为电脑显示的基…

厦门基础城建中排水管网作用,助力提升城市韧性

在厦门这个美丽的海滨城市&#xff0c;城市建设与发展日新月异&#xff0c;其中&#xff0c;城市生命线下的排水管网监测系统作为城市基础设施的重要组成部分&#xff0c;对保障城市安全、提升城市品质发挥着关键作用。 对于厦门城市建设中的排水管网监测系统安装策略 1. 合理…

【头歌系统数据库实验】实验2 MySQL软件操作及建库建表建数据

目录 第1关&#xff1a;创建数据库 第2关&#xff1a;创建供应商表S&#xff0c;并插入数据 第3关&#xff1a;创建零件表P&#xff0c;并插入数据 第4关&#xff1a;创建工程项目表J&#xff0c;并插入数据 第5关&#xff1a;创建供应情况表SPJ&#xff0c;并插入数据 …

软件开发文档的内容

软件开发文档是开发过程中用于记录、指导和沟通的重要工具。它可以包含多个文档&#xff0c;每个文档都有其特定的格式和目的。以下是一些常见的软件开发文档及其可能的格式&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#…

模拟目录管理 - 华为OD统一考试(C卷)

OD统一考试(C卷) 分值: 200分 题解: Java / Python / C++ 题目描述 实现一个模拟目录管理功能的软件,输入一个命令序列,输出最后一条命令运行结果。 支持命令: 1)创建目录命令: mkdir 目录名称,如mkdir abc为在当前目录创建abc目录,如果已存在同名目录则不执行任何操作…

HTML程序大全(2):通用注册模版

一、正常情况效果 二、某项没有填写的效果 三、没有勾选同意项的效果 四、代码 <!DOCTYPE html> <html> <head><meta charset"UTF-8"><title>注册</title><style>body {font-family: Arial, sans-serif;background-color…

小航助学题库白名单竞赛考级蓝桥杯等考scratch(16级)(含题库教师学生账号)

需要在线模拟训练的题库账号请点击 小航助学编程在线模拟试卷系统&#xff08;含题库答题软件账号&#xff09; 需要在线模拟训练的题库账号请点击 小航助学编程在线模拟试卷系统&#xff08;含题库答题软件账号&#xff09;

【重点】【矩阵】48. 旋转图像

题目 参考答案 法1&#xff1a;辅助矩阵 class Solution {public void rotate(int[][] matrix) {int n matrix.length;int[][] newMatrix new int[n][];for (int i 0;i < n; i) {newMatrix[i] matrix[i].clone();}for (int i 0; i < n; i) {for (int j 0; j <…

代码随想录算法训练营第四十四天 _ 动态规划_完全背包问题、518.零钱兑换II、377.组合总和IV。

学习目标&#xff1a; 动态规划五部曲&#xff1a; ① 确定dp[i]的含义 ② 求递推公式 ③ dp数组如何初始化 ④ 确定遍历顺序 ⑤ 打印递归数组 ---- 调试 引用自代码随想录&#xff01; 60天训练营打卡计划&#xff01; 学习内容&#xff1a; 完全背包问题 – 二维dp数组 动…

sklearn随机森林 测试 路面点云分类

一、特征5个坐标 坐标-特征-类别 训练数据 二、模型训练 记录分享给有需要的人&#xff0c;代码质量勿喷 import numpy as np import pandas as pd import joblib#region 1 读取数据 dir D:\\py\\RandomForest\\ filename1 trainRS filename2 .csv path dirfilename1file…

C# 图解教程 第5版 —— 第16章 接口

文章目录 16.1 什么是接口16.2 声明接口16.3 实现接口16.4 接口是引用类型16.5 接口和 as 运算符16.6 实现多个接口16.7 实现具有重复成员的接口16.8 多个接口的引用&#xff08;*&#xff09;16.9 派生成员作为实现&#xff08;*&#xff09;16.10 显示接口成员实现16.11 接口…

Matlab 曲线动态绘制

axes(handles.axes1); % 选定所画坐标轴 figure也可 h1 animatedline; h1.Color b; h1.LineWidth 2; h1.LineStyle -; % 线属性设置 for i 1 : length(x)addpoints(h1,x(i),y(i)); % x/y为待绘制曲线数据drawnow;pause(0.01); % 画点间停顿 end 示例&#xff1a; figure…

如何在Web应用中添加一个JavaScript Excel查看器

前言 在现代的Web应用开发中&#xff0c;Excel文件的处理和展示是一项常见的需求。为了提供更好的用户体验和功能&#xff0c;经常需要在Web应用中添加一个JavaScript Excel查看器&#xff0c;小编今天将为大家展示如何借助葡萄城公司的纯前端表格控件——SpreadJS来创建一个E…

Spark RDD惰性计算的自主优化

原创/朱季谦 RDD&#xff08;弹性分布式数据集&#xff09;中的数据就如final定义一般&#xff0c;只可读而无法修改&#xff0c;若要对RDD进行转换或操作&#xff0c;那就需要创建一个新的RDD来保存结果。故而就需要用到转换和行动的算子。 Spark运行是惰性的&#xff0c;在…

网络安全(四)--Linux 主机防火墙

7.1. 介绍 防火墙&#xff08;Firewall&#xff09;&#xff0c;也称防护墙&#xff0c;是由Check Point创立者Gil Shwed于1993年发明并引入国际互联网&#xff08;US5606668&#xff08;A&#xff09;1993-12-15&#xff09;。 它是一种位于内部网络与外部网络之间的网络安全…

clickhouse数据库磁盘空间使用率过高问题排查

一、前言 clickhouse天天触发磁盘使用率过高告警&#xff0c;所以需要进行排查&#xff0c;故将排查记录一下。 二、排查过程 1、连接上进入clickhouse 2、执行语句查看各库表使用磁盘情况 SELECT database, table, formatReadableSize(sum(bytes_on_disk)) as disk_space F…

数据库加密产品都有哪些功能?

数据库加密产品的主要功能是保护数据库中的敏感数据&#xff0c;确保其机密性和完整性。以下是数据库加密产品可能具备的一些功能&#xff1a; 数据加密&#xff1a;对数据库中的敏感数据进行加密&#xff0c;使得未经授权的人员无法读取或篡改数据。加密算法可以包括对称加密、…

2024 年 20 款最佳免费视频转换器软件 [安全快速有效]

最佳视频转换器软件的功能和定价的回顾和比较。从顶级付费和免费在线视频转换器工具列表中选择&#xff0c;可以快速轻松地转换任何视频&#xff1a; 什么是视频转换器&#xff1f; 视频转换工具允许您将视频从一种格式转换为另一种格式。第一个商业上成功的视频格式是 Quad&…

面试官:说说webpack中常见的Loader?解决了什么问题?

面试官&#xff1a;说说webpack中常见的Loader&#xff1f;解决了什么问题&#xff1f; 一、是什么 loader 用于对模块的"源代码"进行转换&#xff0c;在 import 或"加载"模块时预处理文件 webpack做的事情&#xff0c;仅仅是分析出各种模块的依赖关系&a…

易宝OA 两处任意文件上传漏洞复现

0x01 产品简介 易宝OA系统是一种专门为企业和机构的日常办公工作提供服务的综合性软件平台,具有信息管理、 流程管理 、知识管理(档案和业务管理)、协同办公等多种功能。 0x02 漏洞概述 易宝OA系统UploadFile、BasicService.asmx等接口处存在文件上传漏洞,未授权的攻击者可…