数据与结构——哈夫曼树

哈夫曼树的基本概念

哈夫曼树(Huffman Tree)是一种用于数据压缩的最优二叉树,广泛应用于哈夫曼编码中。其基本概念和构建方法如下:

基本概念

  1. 二叉树:哈夫曼树是一种特殊的二叉树。
  2. 权重:每个节点都有一个权重,通常是字符或数据出现的频率。
  3. 路径长度:从根节点到某一节点的路径长度称为该节点的路径长度。
  4. 带权路径长度:节点的路径长度乘以该节点的权重。
  5. 树的带权路径长度:树中所有叶子节点的带权路径长度之和。哈夫曼树的带权路径长度是最小的。

1.什么是路径?

在一棵树中,从一个节点往下可以达到的节点之间的通路,称为路径。

如图,从根节点7到叶子节点a的路径就是7->3->1->a

2.什么是路径长度 

某一路径所经过的“边”的数量,称为该路径的路径长度

对于字符'a',路径长度是指从根节点到字符'a'节点的边的数量。在这个示例中,路径长度为3,因为从根节点到字符'a'节点有三条边。

对于字符'b',路径长度也是指从根节点到字符'b'节点的边的数量。在这个示例中,路径长度为3,因为从根节点到字符'b'节点有三条边。

3.什么是节点的带权路径长度?

节点的权通常是指节点所代表的字符在数据中出现的频率或权重。从根结点到该结点之间的路径长度与该结点的节的乘积,称为该结点的带权路径长度。

假设树中的叶节点a代表字符'a',并且字符'a'在数据中出现了3次;叶节点b代表字符'b',字符'b'在数据中出现了3次;叶节点c代表字符'c',字符'c'在数据中出现了3次;叶节点d代表字符'd',字符'd'在数据中出现了3次;叶节点e代表字符'e',字符'e'在数据中出现了3次;叶节点f代表字符'f',字符'f'在数据中出现了3次;叶节点g代表字符'g',字符'g'在数据中出现了4次。

根据定义,每个节点的带权路径长度是从根节点到该节点的路径长度与该节点的权的乘积。

现在让我们计算加权路径长度:

  • 根节点的带权路径长度为 0(根节点)乘以 13(根节点权)= 0。
  • 节点 a 的带权路径长度为 3(根节点到节点a的路径长度)乘以 3(节点 a 的权重)= 9。
  • 节点 b 的带权路径长度为 3(根节点到节点b的路径长度)乘以 3(节点 b 的权重)= 9。
  • 节点 c 的带权路径长度为 3(根节点到节点c的路径长度)乘以 3(节点 c 的权重)= 9。
  • 节点 d 的带权路径长度为 3(根节点到节点d的路径长度)乘以 3(节点 d 的权重)= 9。
  • 节点 e 的带权路径长度为 3(根节点到节点e的路径长度)乘以 3(节点 e 的权重)= 9。
  • 节点 f 的带权路径长度为 3(根节点到节点f的路径长度)乘以 3(节点 f 的权重)= 9。
  • 节点 g 的带权路径长度为 3(根节点到节点g的路径长度)乘以 4(节点 g 的权重)= 12。

什么是树的带权路径长度?

树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。

 

假设树中的叶节点a代表字符'a',并且字符'a'在数据中出现了3次;叶节点b代表字符'b',字符'b'在数据中出现了3次;叶节点c代表字符'c',字符'c'在数据中出现了3次;叶节点d代表字符'd',字符'd'在数据中出现了3次;叶节点e代表字符'e',字符'e'在数据中出现了3次;叶节点f代表字符'f',字符'f'在数据中出现了3次;叶节点g代表字符'g',字符'g'在数据中出现了4次。 

如图,该二叉树的带权路径长度 WPL= 0 + 4 + 8 + 9 + 8 + 8 + 17 + 12 = 66。

 5、什么是哈夫曼树

给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,则称该二叉树为哈夫曼树,也被称为最优二叉树。

根据树的带权路径长度的计算规则,我们不难理解:树的带权路径长度与其叶子结点的分布有关。
即便是两棵结构相同的二叉树,也会因为其叶子结点的分布不同,而导致两棵二叉树的带权路径长度不同。

那如何才能使一棵二叉树的带权路径长度达到最小呢?
 根据树的带权路径长度的计算规则,我们应该尽可能地让权值大的叶子结点靠近根结点,让权值小的叶子结点远离根结点,这样便能使得这棵二叉树的带权路径长度达到最小。

哈夫曼树的创建

构建思路

下面给出一个非常简洁易操作的算法,来构造一棵哈夫曼树:
 1、初始状态下共有n个结点,结点的权值分别是给定的n个数,将他们视作n棵只有根结点的树。
 2、合并其中根结点权值最小的两棵树,生成这两棵树的父结点,权值为这两个根结点的权值之和,这样树的数量就减少了一个。
 3、重复操作2,直到只剩下一棵树为止,这棵树就是哈夫曼树。

动图演示:

我们展现一个简单一点的哈夫曼树的形成过程

假设有以下字符及其频率

 

首先,我们将字符及其频率表示为叶子节点: 

然后,我们将这些叶子节点放入一个优先队列(按频率升序排列):

 

接下来,我们开始合并节点,每次合并都是取出频率最小的两个节点并创建一个新的节点,然后将新节点放回队列中:

第一步合并 (a,5) 和 (b,9),得到新节点 (ab,14):

第二步合并 (c,12) 和 (d,13),得到新节点 (cd,25): 

 第三步合并 (ab,14) 和 (e,16),得到新节点 (abe,30):

 第四步合并 (f,45) 和 (cd,25),得到新节点 (fcd,70):

最后,合并 (abe,30) 和 (fcd,70),得到根节点:

 这样,我们就构建了哈夫曼树。从根节点到叶子节点的路径就是字符的哈夫曼编码,频率越高的字符路径越短。

代码展示


// 定义节点结构体
typedef struct Node {
    int freq;          // 节点频率
    char data;         // 节点字符数据
    struct Node *left; // 左子节点指针
    struct Node *right;// 右子节点指针
} Node;

// 创建新节点
Node* createNode(int freq, char data) {
    Node* newNode = (Node*)malloc(sizeof(Node)); // 分配节点内存空间
    newNode->freq = freq;   // 设置节点频率
    newNode->data = data;   // 设置节点字符数据
    newNode->left = NULL;   // 左子节点初始化为空
    newNode->right = NULL;  // 右子节点初始化为空
    return newNode;         // 返回新节点指针
}

// 构建哈夫曼树
Node* buildHuffmanTree(char data[], int freq[], int size) {
    // 创建叶子节点数组
    Node* leafNodes[size];
    for (int i = 0; i < size; i++) {
        leafNodes[i] = createNode(freq[i], data[i]);
    }

    // 构建哈夫曼树
    while (size > 1) {
        // 找到频率最小的两个节点
        int min1 = 0, min2 = 1;
        if (leafNodes[min1]->freq > leafNodes[min2]->freq) {
            int temp = min1;
            min1 = min2;
            min2 = temp;
        }
        for (int i = 2; i < size; i++) {
            if (leafNodes[i]->freq < leafNodes[min1]->freq) {
                min2 = min1;
                min1 = i;
            } else if (leafNodes[i]->freq < leafNodes[min2]->freq) {
                min2 = i;
            }
        }

        // 创建新节点作为合并结果
        Node* newNode = createNode(leafNodes[min1]->freq + leafNodes[min2]->freq, '$');
        newNode->left = leafNodes[min1];
        newNode->right = leafNodes[min2];

        // 从叶子节点数组中移除被合并的节点,将新节点加入数组
        leafNodes[min1] = newNode;
        leafNodes[min2] = leafNodes[size - 1];
        size--;
    }

    // 返回根节点
    return leafNodes[0];
}

哈夫曼编码的生成

编码生成思路

对于任意一棵二叉树来说,把二叉树上的所有分支都进行编号,将所有左分支都标记为0,所有右分支都标记为1。
我们就以7、5、4、2构建的哈夫曼树为例。 

那么对于树上的任何一个结点,都可以根据从根结点到该结点的路径唯一确定一个编号。

对于哈夫曼树上的叶子结点,根据从根结点到该叶子结点的路径所确定的一个编号,就是该叶子结点的哈夫曼编码。

代码实现 

我们首先需要清楚这样一个问题:
用n个数据所构建出来的哈夫曼树,生成的哈夫曼编码的长度最长是多少?
 因为哈夫曼编码是根据从根结点到该叶子结点的路径所确定的一个编号,所以生成的哈夫曼编码最长的叶子结点一定是离根结点最远的结点。要使叶子结点里根结点达到最远,那么生成的哈夫曼树应该是斜二叉树。

 

该斜二叉树中最后一层的叶子结点所生成的哈夫曼编码就是最长的,其哈夫曼编码的长度就是从根结点到达该叶子结点的路径长度,即n − 1 n-1n−1。

一个字符串若是想要容纳下“用n个数据生成的哈夫曼编码”中的任意一个编码,那么这个字符串的长度应该为n nn,因为我们还需要用一个字节的位置用于存放字符串的结束标志’\0’。

我们就以数字7、5、4、2构建的哈夫曼树为例,哈夫曼编码生成的基本实现步骤如下:

第一阶段
因为数据个数为4,所以我们开辟一个大小为4的辅助空间,并将最后一个位置赋值为’\0’,用于暂时存放正在生成的哈夫曼编码。

 

为了存放这4个数据哈夫曼编码,我们开辟一个字符指针数组,该数组中有5个元素,每个元素的类型为char**,该字符指针数组的基本布局如下:

 

注意:这里为了与“构建哈夫曼树时所生成的数组”中的下标相对应,所以该字符指针数组中下标为0的元素也不存储有效数据。

第二阶段:
利用已经构建好的哈夫曼树,生成这4个数据的哈夫曼编码。单个数据生成哈夫曼编码的过程如下:
 1、判断该数据结点与其父结点之间的关系,若该数据结点是其父结点的左孩子,则将start指针前移,并将0填入start指向的位置,若是右孩子,则在该位置填1。
 2、接着用同样的方法判断其父结点与其父结点的父结点之间的关系,直到待判断的结点为哈夫曼树的根结点为止,该结点的哈夫曼编码生成完毕。
 3、将字符串中从start的位置开始的数据拷贝到字符指针数组中的相应位置。

注意:在每次生成数据的哈夫曼编码之前,先将start指针指向’\0’。

按照此方式,依次生成7、5、4、2的哈夫曼编码后,字符指针数组的基本布局如下:

哈夫曼编码生成完毕。

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef double DataType; //结点权值的数据类型

typedef struct HTNode //单个结点的信息
{
	DataType weight; //权值
	int parent; //父节点
	int lc, rc; //左右孩子
}*HuffmanTree;

typedef char **HuffmanCode; //字符指针数组中存储的元素类型

//在下标为1到i-1的范围找到权值最小的两个值的下标,其中s1的权值小于s2的权值
void Select(HuffmanTree& HT, int n, int& s1, int& s2)
{
	int min;
	//找第一个最小值
	for (int i = 1; i <= n; i++)
	{
		if (HT[i].parent == 0)
		{
			min = i;
			break;
		}
	}
	for (int i = min + 1; i <= n; i++)
	{
		if (HT[i].parent == 0 && HT[i].weight < HT[min].weight)
			min = i;
	}
	s1 = min; //第一个最小值给s1
	//找第二个最小值
	for (int i = 1; i <= n; i++)
	{
		if (HT[i].parent == 0 && i != s1)
		{
			min = i;
			break;
		}
	}
	for (int i = min + 1; i <= n; i++)
	{
		if (HT[i].parent == 0 && HT[i].weight < HT[min].weight&&i != s1)
			min = i;
	}
	s2 = min; //第二个最小值给s2
}

//构建哈夫曼树
void CreateHuff(HuffmanTree& HT, DataType* w, int n)
{
	int m = 2 * n - 1; //哈夫曼树总结点数
	HT = (HuffmanTree)calloc(m + 1, sizeof(HTNode)); //开m+1个HTNode,因为下标为0的HTNode不存储数据
	for (int i = 1; i <= n; i++)
	{
		HT[i].weight = w[i - 1]; //赋权值给n个叶子结点
	}
	for (int i = n + 1; i <= m; i++) //构建哈夫曼树
	{
		//选择权值最小的s1和s2,生成它们的父结点
		int s1, s2;
		Select(HT, i - 1, s1, s2); //在下标为1到i-1的范围找到权值最小的两个值的下标,其中s1的权值小于s2的权值
		HT[i].weight = HT[s1].weight + HT[s2].weight; //i的权重是s1和s2的权重之和
		HT[s1].parent = i; //s1的父亲是i
		HT[s2].parent = i; //s2的父亲是i
		HT[i].lc = s1; //左孩子是s1
		HT[i].rc = s2; //右孩子是s2
	}
	//打印哈夫曼树中各结点之间的关系
	printf("哈夫曼树为:>\n");
	printf("下标   权值     父结点   左孩子   右孩子\n");
	printf("0                                  \n");
	for (int i = 1; i <= m; i++)
	{
		printf("%-4d   %-6.2lf   %-6d   %-6d   %-6d\n", i, HT[i].weight, HT[i].parent, HT[i].lc, HT[i].rc);
	}
	printf("\n");
}

//生成哈夫曼编码
void HuffCoding(HuffmanTree& HT, HuffmanCode& HC, int n)
{
	HC = (HuffmanCode)malloc(sizeof(char*)*(n + 1)); //开n+1个空间,因为下标为0的空间不用
	char* code = (char*)malloc(sizeof(char)*n); //辅助空间,编码最长为n(最长时,前n-1个用于存储数据,最后1个用于存放'\0')
	code[n - 1] = '\0'; //辅助空间最后一个位置为'\0'
	for (int i = 1; i <= n; i++)
	{
		int start = n - 1; //每次生成数据的哈夫曼编码之前,先将start指针指向'\0'
		int c = i; //正在进行的第i个数据的编码
		int p = HT[c].parent; //找到该数据的父结点
		while (p) //直到父结点为0,即父结点为根结点时,停止
		{
			if (HT[p].lc == c) //如果该结点是其父结点的左孩子,则编码为0,否则为1
				code[--start] = '0';
			else
				code[--start] = '1';
			c = p; //继续往上进行编码
			p = HT[c].parent; //c的父结点
		}
		HC[i] = (char*)malloc(sizeof(char)*(n - start)); //开辟用于存储编码的内存空间
		strcpy(HC[i], &code[start]); //将编码拷贝到字符指针数组中的相应位置
	}
	free(code); //释放辅助空间
}

//主函数
int main()
{
	int n = 0;
	printf("请输入数据个数:>");
	scanf("%d", &n);
	DataType* w = (DataType*)malloc(sizeof(DataType)*n);
	if (w == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	printf("请输入数据:>");
	for (int i = 0; i < n; i++)
	{
		scanf("%lf", &w[i]);
	}
	HuffmanTree HT;
	CreateHuff(HT, w, n); //构建哈夫曼树

	HuffmanCode HC;
	HuffCoding(HT, HC, n); //构建哈夫曼编码

	for (int i = 1; i <= n; i++) //打印哈夫曼编码
	{
		printf("数据%.2lf的编码为:%s\n", HT[i].weight, HC[i]);
	}
	free(w);
	return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【vue3 + Echarts 】中国地图省市区下钻,并返回上级

实现效果如果&#xff1a; echarts版本&#xff1a; 地图数据来源&#xff1a;阿里云数据可视化平台 代码 <template><div class"mapWrapper"><a-button type"primary" click"goBack">返回上级</a-button><div…

python编程:实现对数据库中图片文件的查看及比对

当谈到图像查看和管理时,我们往往会使用一些工具软件,比如Windows自带的照片查看器或者第三方工具。那如果你想要一个更加强大和定制化的图像查看器呢?这时候就需要自己动手写一个程序了。 C:\pythoncode\new\ShowSqliteImage.py 这里我们将介绍一个使用Python和wxPython编写…

赛轮集团受邀出席2024国际新能源智能网联汽车创新生态大会

赛轮集团受邀出席2024国际新能源智能网联汽车创新生态大会 5月22日-24日&#xff0c;以“汽车供应链的创新与重构”为主题的2024国际新能源智能网联汽车创新生态大会&#xff08;以下简称CIEV2024&#xff09;在温州瑞安隆重召开。会议期间&#xff0c;CIEV2024高端对话成功召…

Docker(Centos7+)

先确定是否 Centos 7 及以上的版本 查看是否 ping 通外网 linux centos7运行下面的代码&#xff0c;基本上都可以正常安装 # 删除之前的docker残留 yum -y remove docker*yum install -y yum-utilsyum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/…

Linux、Windows安装python环境(最新版及历史版本指定版本)-python

目录 一、Linux环境二、windows环境最新版本下载指定版本下载配置VScode开发环境 python 官网地址&#xff1a; https://www.python.org/ 一、Linux环境 以openEuler/CentOS为例 查看可安装python源版本 dnf provides python*默认安装新版本 dnf install -y python3. 进入p…

拼多多商品信息一键抓取:深度解析商品详情接口,Python实战代码来袭!

拼多多的商品详情接口允许开发者通过指定的商品ID获取商品的详细信息&#xff0c;如商品标题、价格、描述、图片等。接口采用HTTP请求方式&#xff0c;支持GET方法&#xff0c;返回格式为JSON。 三、接口调用 要调用拼多多的商品详情接口&#xff0c;你需要遵循以下步骤&…

C++ vector的使用和简单模拟实现(超级详细!!!)

目录 前言 1.STL是什么 2.vector使用 2.1 vector简介 2.2 常用接口函数 1. 构造函数 2.operator[ ]和size&#xff0c;push_back 3. 用迭代器进行访问和修改 4. 范围for遍历 5.修改类型函数 pop_back find insert erase 6. 容量相关函数capacity resize reserve 3.…

【Node】Assertion testing 模块的使用

简言 node:assert 模块提供了一组用于验证不变式的断言函数。 node版本&#xff1a;20.14.0 Assertion testing 测试断言模块 node:assert 模块是一个测试相关的模块。 严格模式和非严格模式 感觉该模块的严格模式和js的严格模式相匹配&#xff0c;非严格模式也是这样的。…

掘金AI 商战 宝典 进阶班:如何用AI绘画设计(实战实操 现学现用 玩赚超值)

课程内容 10-第十讲用AI做网站设计 11-第十一讲用AI做艺术字 12-第十二讲用AI做室内设计(上) 13-第十三讲用AI做室内设计(下) 14-第十四讲用AI抠图与修图 15-第十五讲用AI修复模糊照片 16-第十六讲用AI自动做PPT(上) 17-第十七讲用AI自动做PPT(下) 18-第十八讲用AI做文…

atcoder350,351,352,353,354,355期部分题解

声明&#xff1a;有些题感觉已经说到很明白了&#xff0c;就先不写代码了&#xff0c;有空会补上 目录 350D: new friend 350E: toward 0 351D:Grid and Magnet 352D:permutation subsequence 353C: sigma problem 353D: another sigma problem 354C: atcoder magics …

一文读懂存内计算与近存计算的分类与应用

存内计算与近存计算-基础理论及分类 技术基础知识和分类 "近存计算"与"存内计算"易混淆&#xff0c;本章明晰其分类&#xff0c;并比较各内存驱动方法的独特优势。可计算存储器设备可作分立加速器或替代现有存储模块。我们深入剖析每种方法的利弊&#xf…

像艺术家一样工作

接下来开始翻译这本小册子 豆瓣评分还是挺高的&#xff0c;目前在国内没有看到有在售的翻译版本 书名直译的话是&#xff1a;像艺术家一样去偷 作者可能是为了制造营销话题&#xff0c;所以起了这么一个名字 但是偷这个词总归不太体面&#xff0c;所以我把书名翻译为&#…

Qos令牌桶算法:笔记0601

令牌桶 令牌&#xff1a;目前看到2种表述&#xff0c;csdn表示一个令牌代表一个字节&#xff0c;51cto是一个令牌代表一个bit。51cto上关于cisco qos算法描述多表达为一个令牌一个bit (不知道rfc上咋表达的懒得去查了&#xff0c;主打一个好读书不求甚解&#xff0c;感觉应该是…

c++学习----初识类和对象(上)

1.面向过程和面向对象初步认识 C语言是面向过程的&#xff0c;关注的是过程&#xff0c;分析出求解问题的步骤&#xff0c;通过函数调用逐步解决问题。 C是基于面向对象的&#xff0c;关注的是对象&#xff0c;将一件事情拆分成不同的对象&#xff0c;靠对象之间的交互完 成。…

rtl8723DU移植 android4.4 4418

一、 linux 的移植。 首先编译一遍确保没有问题。 将驱动拷贝到 driver/net/wireless 目录下。 使用的是&#xff1a; 改写 makefile Kconfig 去改写 8723 的makefile 设置menuconfig 使能固有的 库。 使能USB部分 ieee 部分 编译一遍 有报错。 解决&#xff1a; …

基于深度学习YOLOv8\YOLOv5的花卉识别鲜花识别检测分类系统设计

本文将介绍基于深度学习YOLOv8\YOLOv5PySide6SQLite的花卉检测与识别系统&#xff0c;该系统基于YOLOv8算法&#xff0c;并与YOLOv5版本进行比较&#xff0c;该系统不仅实现了对花卉的精准识别和分类&#xff0c;还提供了包括用户认证管理、模型快速切换及界面个性化定制在内的…

ssm汉服文化平台网站

博主介绍&#xff1a;✌程序员徐师兄、8年大厂程序员经历。全网粉丝15w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

【TB作品】msp430f5529单片机墨水屏,口袋板,tmp421温度,温控风扇

文章目录 一、扬声器模块介绍二、驱动介绍三、程序介绍四、全部代码下载 msp430f5529d单片机墨水屏&#xff0c;口袋板&#xff0c;tmp421温度&#xff0c;温控风扇 基本要求&#xff1a;高于20度开转&#xff0c;温度越高转速越快&#xff0c;高于40度风扇停转&#xff0c;温…

Day45 动态规划part05

LC1049最后一块石头重量II(未掌握) 未掌握分析&#xff1a;其实本题跟LC416分割等和子集类似&#xff0c;本质上题目的要求是尽量让石头分成重量相同的两堆&#xff0c;相撞之后剩下的石头最小&#xff0c;也就是01背包问题weight和value都是stones数组&#xff0c;题目可以看…

Java的JDK环境变量配置(Windows)

只写了需要配置的环境变量 注&#xff1a;从JDK1.5开始&#xff0c;配置Java环境变量时&#xff0c;不再需要配置CLASSPATH&#xff0c;只需要配置JAVA_HOME和Path 1、配置JAVA_HOME 找到自己的JDK位置&#xff0c;我这里是 C:\dev\java\jdk-17.0.119在环境变量-系统变量中&…