哈夫曼树和哈夫曼编码

news2024/11/8 11:23:49

一.哈夫曼树

1.哈夫曼树

哈夫曼树是一种用于编码的树形结构。它是通过将频率最低的字符反复组合形成的二叉树,使得出现频率高的字符具有较短的二进制编码,而出现频率低的字符具有较长的编码。

在哈夫曼树中,每个叶子节点都代表一个字符,其权值表示该字符在文本中出现的次数。非叶子节点代表的权值为其两个子节点的权值之和。因此,从根节点到叶子节点的路径上的数字串即为该字符的哈夫曼编码。

在编码时,对于要编码的文本,把其中的每个字符用哈夫曼树的编码替换掉即可。解码时,从哈夫曼树的根节点开始,按照编码的数字串依次向下走,直到到达叶子节点,即可得到原来的字符。

哈夫曼树的应用广泛,常用于数据压缩、加密等领域。

2.构造哈夫曼树原理

给定n个权值分别为w1,w2… wn。的结点,构造哈夫曼树的算法描述如下:

  • 1)将这n个结点分别作为n棵仅含一个结点的二又树,构成森林F.
  • 2)构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
  • 3)从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
    4)重复步骤2)和3),直至F中只剩下一棵树为止。

从上述构造过程中可以看出哈夫曼树具有如下特点:

  • 1)每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大。
  • 2)构造过程中共新建了n-1个结点(双分支结点),因此哈夫曼树的结点总数为2n- 1
  • 3)每次构造都选择2棵树作为新结点的孩子,因此哈夫曼树中不存在度为1的结点。

二.哈夫曼编码

哈夫曼编码的原理是通过构建哈夫曼树来实现的。具体地,首先统计给定文本中每个字符出现的频率,并将它们作为叶子节点构建出一棵二叉树。接着,反复合并权值最小的两个节点,得到新节点,将它们加入哈夫曼树中,直至所有节点都被合并成为一个根节点。在此过程中,左分支标记为0,右分支标记为1。最终得到的哈夫曼树即为字符的哈夫曼编码。

举例来说,对于输入字符串"ABBCCCDDDDEEEEE",它包含了5种不同的字符 A、B、C、D 和 E,它们分别出现的次数为 1, 2, 3, 4 和 5。我们可以按照上述步骤构建出一个哈夫曼树,如下所示:

          +--------+
          |   15   |
          +--------+
                |
       +--------+--------+
       |         |        |
     +---+      +---+    +---+
     | B |      | C |    | D |
     +---+      +---+    +---+
      / \        / \      / \
    1/   \2    1/   \2  1/   \3
  +----+ +---+ +--+  +---+  +--+
  | A  | | E | |   |  |   |  |  |
  +----+ +---+ +--+  +---+  +--+

在这个哈夫曼树中,每个叶子节点代表一个字符,其权值表示该字符在文本中出现的次数。非叶子节点代表的权值为其两个子节点的权值之和。从根节点到叶子节点的路径上的数字串即为该字符的哈夫曼编码。例如,E 的编码为 0,B 的编码为 10,C 的编码为 11,D 的编码为 2(注意这里是二进制编码),A 的编码为 30。

对于给定的文本,我们可以将其中的每个字符用哈夫曼树的编码替换掉,得到压缩后的二进制串。解压时,从哈夫曼树的根节点开始,按照编码的数字串依次向下走,直到到达叶子节点,即可得到原来的字符。

三.补充知识

1.C语言排序函数

qsort() 函数是 C 语言中的标准库函数,用于对数组进行排序。它的语法如下所示:

void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void*, const void*));

该函数接收四个参数,分别是:

  • void *base:需要排序的数组的首地址;
  • size_t nmemb:数组中元素的个数;
  • size_t size:每个数组元素占用的字节数;
  • int (*compar)(const void*, const void*):指向比较函数的指针。

其中,比较函数必须返回一个整数值,表示两个元素之间的关系,具体规定如下:

  • 若返回值小于 0,则表示第一个元素应该排在第二个元素之前;
  • 若返回值等于 0,则表示两个元素相等,无需改变它们在数组中的位置;
  • 若返回值大于 0,则表示第一个元素应该排在第二个元素之后。

因为 qsort() 函数会直接操作原数组,因此在使用时需要保证比较函数和数据类型的匹配、越界访问等问题,以避免出现未定义行为。

2.占位符

在计算机科学中,占位符是一个表示数据的特殊字符或值。它通常用于代替实际数据或者标识出不存在的数据。

一些常见的占位符包括:

  1. 空格:在文本中,空格常被用作占位符。例如,在编程语言中,空格可以用于分隔单词或语句。
  2. 通配符:在搜索引擎、文件管理器和数据库查询等应用程序中,通配符(如 * 和 ?)可以用来匹配任意字符或字符串。它们可以帮助用户快速查找文件或记录。
  3. 十六进制码:在编程领域,十六进制码可以用作占位符表示某个未知的数值。它们也可以用于调试和修复代码中的错误。
  4. 填充字符:在格式化输出时,填充字符(如 0 或空格)可以用于在数字前面添加零或空格,以满足格式要求。
  5. 标记:在自然语言处理中,标记可以用于表示某个词汇或短语的类型。例如,POS(Part-of-Speech)标记可以用于表示一个单词是名词、动词还是形容词。
  Node* parent = newNode(nodes[0]->weight + nodes[1]->weight, '-');

在这行代码中,'-'是一个占位符,用于表示新节点不代表任何字符或叶子节点。当构造哈夫曼树时,每个非叶子节点都需要有两个子节点,因此在将两个权重最小的节点合并成一个新节点时,需要创建一个没有对应字符的节点,以便作为它们的父节点。在这种情况下,可以使用一个占位符来表示该节点不代表任何字符。

四.核心功能实现

1.构造哈夫曼树

//构造哈夫曼树
HTree *createHTree(int n,char values[],int weights[]){
	HTnode *nodes = (HTnode*)malloc(sizeof(HTnode)*n);          //建立一个结点数组
	for (int i = 0; i < n; ++i) {
		nodes[i] = creatennode(weights[i], values[i]);          //给每一个结点赋值
	}
	while (n > 1) {
		// 对节点按照权重从小到大排序
		qsort(nodes, n, sizeof(HTnode), cmp);             //C语言的排序函数
		
		// 取出两个权重最小的节点构造一个新的父节点
		HTnode parent = creatennode(nodes[0]->weight + nodes[1]->weight, '-');  //构造它们的和结点,修改左右指针
		parent->lchild = nodes[0];
		parent->rchild = nodes[1];
		
		// 将新节点插入到原来的节点集合中,并将原来的两个子节点从集合中删除
		nodes[0] = parent;             
		for (int i = 2; i < n; ++i) {            //其实这并不是严格的删除操作,相当于数组0号元素重载,然后从2号元素开始向前移动一个位置
			nodes[i - 1] = nodes[i];
		}
		n--;
	}
	HTnode root = nodes[0];              //当剩下最后一个结点时,就是我们的根结点
	free(nodes);
	return root;
}

2.哈夫曼编码

//根据哈夫曼树输出哈夫曼编码
void print_code(HTnode &node, int* code, int len) {
	// 如果是叶子节点,打印它的字符和对应编码
	if (node->lchild == NULL && node->rchild == NULL) {
		printf("%c的哈夫曼编码: ", node->val);
		for(int i=0;i<len;i++){
			printf("%d ",code[i]);
		}
		printf("\n");
	}
	else {
		// 递归左子树,在编码末尾加上0
		code[len] = 0;
		print_code(node->lchild, code, len+1);
		
		// 递归右子树,在编码末尾加上1
		code[len] = 1;
		print_code(node->rchild, code, len+1);
	}
}

3.遍历哈夫曼树

这里应该算全文最简单的了,因为上一篇博客我已经写了二叉树的相关操作,;里面有二叉树的遍历,欢迎点击同专栏的二叉树的相关操作查看。

void visit1(HTnode &T){
	printf("%d\t",T->weight);             //这里遍历本来应该是输出value的,但是在构造二叉树的时候我们创建了新结点,其值是占位符,所以这里选择打印权值
}

//这里我们采用先序遍历
void Preorder(HTnode &T){
	if(T!=NULL){
		visit1(T);        //在访问函数里面定义我们想要的可视化输出
		Preorder(T->lchild);
		Preorder(T->rchild);
	}
}

五.完整代码

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

#define N 50         //叶子结点总数 
#define M 2*N-1      //树中结点总数

//哈夫曼树中的结点
typedef struct HTNode
{
	char val;
	int weight;
	struct HTNode * parent;       //双亲结点
	struct HTNode *lchild;       //左孩子结点
	struct HTNode *rchild;       //右孩子结点
}HTree,*HTnode;
/*如果为了简单,我们可以把结点值存储为int型,这样可以直接代替权重*/

//创建一个结点
HTree *creatennode(int weight,char value){
	HTnode node=(HTnode)malloc(sizeof(HTree));
	node->val=value;
	node->weight=weight;
	node->lchild = NULL;
	node->rchild = NULL;
	node->parent = NULL;
	return node;
}

// 比较函数,用于对节点按照权重从小到大排序
int cmp(const void* a, const void* b) {
	HTnode node1 = *(HTnode*)a;
	HTnode node2 = *(HTnode*)b;
	return node1->weight - node2->weight;
}

//构造哈夫曼树
HTree *createHTree(int n,char values[],int weights[]){
	HTnode *nodes = (HTnode*)malloc(sizeof(HTnode)*n);          //建立一个结点数组
	for (int i = 0; i < n; ++i) {
		nodes[i] = creatennode(weights[i], values[i]);          //给每一个结点赋值
	}
	while (n > 1) {
		// 对节点按照权重从小到大排序
		qsort(nodes, n, sizeof(HTnode), cmp);             //C语言的排序函数
		
		// 取出两个权重最小的节点构造一个新的父节点
		HTnode parent = creatennode(nodes[0]->weight + nodes[1]->weight, '-');  //构造它们的和结点,修改左右指针
		parent->lchild = nodes[0];
		parent->rchild = nodes[1];
		
		// 将新节点插入到原来的节点集合中,并将原来的两个子节点从集合中删除
		nodes[0] = parent;             
		for (int i = 2; i < n; ++i) {            //其实这并不是严格的删除操作,相当于数组0号元素重载,然后从2号元素开始向前移动一个位置
			nodes[i - 1] = nodes[i];
		}
		n--;
	}
	HTnode root = nodes[0];              //当剩下最后一个结点时,就是我们的根结点
	free(nodes);
	return root;
}
/*这里为了检验我们的哈夫曼树是否正确,就把它当作二叉树遍历出来,和我们的手算作比较*/
//遍历就不要多说了,参考我上一篇博客,二叉树的相关操作

void visit1(HTnode &T){
	printf("%d\t",T->weight);             //这里遍历本来应该是输出value的,但是在构造二叉树的时候我们创建了新结点,其值是占位符,所以这里选择打印权值
}

//这里我们采用先序遍历
void Preorder(HTnode &T){
	if(T!=NULL){
		visit1(T);        //在访问函数里面定义我们想要的可视化输出
		Preorder(T->lchild);
		Preorder(T->rchild);
	}
}

//根据哈夫曼树输出哈夫曼编码
void print_code(HTnode &node, int* code, int len) {
	// 如果是叶子节点,打印它的字符和对应编码
	if (node->lchild == NULL && node->rchild == NULL) {
		printf("%c的哈夫曼编码: ", node->val);
		for(int i=0;i<len;i++){
			printf("%d ",code[i]);
		}
		printf("\n");
	}
	else {
		// 递归左子树,在编码末尾加上'0'
		code[len] = 0;
		print_code(node->lchild, code, len+1);
		
		// 递归右子树,在编码末尾加上'1'
		code[len] = 1;
		print_code(node->rchild, code, len+1);
	}
}

int main(){
	int n;
	HTnode p;
	printf("请输入要编写的结点总数:");
	scanf("%d",&n);
	char values[n];
	int weights[n];
	for(int i=0;i<n;i++){
		printf("请输入第%d个结点的值:\n",i);
		scanf(" %c",&values[i]);        //这里%c前面一定要留一个空格抵消前面的换行,否则会出bug
	}
	for(int j=0;j<n;j++){
		printf("请输入第%d个元素的权重:\n",j);
		scanf("%d",&weights[j]);
	}
	p=createHTree(n,values,weights);
	Preorder(p);
	printf("\n");
	//到这一步,我已经测试了一遍,没有问题,下面开始思考哈夫曼编码问题
	int code[n]; // 初始化为空串
	print_code(p, code, 0);  // htree为哈夫曼树的根节点
	return 0;
}

代码虽然少,主要是写的过程。

六.运行结果

9VTf.jpg

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

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

相关文章

chatgpt赋能python:Python图形填充颜色教程

Python图形填充颜色教程 Python是一种简单易学、高效的编程语言&#xff0c;广泛应用于数据分析、机器学习、Web开发等领域。其中&#xff0c;图形处理是Python编程领域的一个重要方面。在很多情况下&#xff0c;我们需要填充图形颜色来增加图形的美观程度和可读性。本文将介绍…

使用OpenCV和MediaPipe实现姿态识别!

大家好&#xff0c;我是小F&#xff5e; MediaPipe是一款由Google开发并开源的数据流处理机器学习应用开发框架。 它是一个基于图的数据处理管线&#xff0c;用于构建使用了多种形式的数据源&#xff0c;如视频、音频、传感器数据以及任何时间序列数据。 MediaPipe通过将各个感…

表示学习(Representation Learning) Part1--Pretext Text

文章目录 Representation LearningInferring structure&#xff08;推断结构&#xff09; Transformation predictionRotation predictionRelative transformation prediction ReconstructionDenoising AutoencodersContext encodersColorizationSplit-brain encoders Instance…

屏幕录像视频录制编辑软件TechSmith Camtasia 2023 for Mac 简体中文版

TechSmith Camtasia for Mac 中文版 是一款专业的屏幕录像视频录制编辑软件&#xff0c;非常容易就可以获得精彩的截屏视频。创建引人注目的培训&#xff0c;演示和演示视频。Camtasia 屏幕录制软件简化&#xff0c;直观&#xff0c;让您看起来像专业人士。利用Camtasia&#x…

SpringMVC 学习总结

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔&#x1f93a;&#x1f93a;&#x1f93a; 目录 1. 什么是 Spring MVC 1.1 Spring、Spring MV…

Dockerfile创建镜像

一、Docker镜像的创建 创建镜像有三种方法&#xff0c;分别为【基于已有镜像创建】、【基于本地模板创建】以及【基于Dockerfile创建】。 1.1 基于现有镜像创建 &#xff08;1&#xff09;首先启动一个镜像&#xff0c;在容器里做修改docker run -it centos:7 /bin/bash …

旧手机卖掉之前我们需要做这几个操作

随着科技的不断进步&#xff0c;人们使用的电子产品也在不断地迭代更新。当我们不再使用旧手机时&#xff0c;卖掉它可以省下一笔开支&#xff0c;但也需要注意保护个人隐私数据。因此&#xff0c;在售卖二手手机之前&#xff0c;正确清除旧手机中的历史数据变得至关重要。 首先…

Java网络开发(Tomcat)——从同步到异步 从jsp 到 js + axios + vue 实现 数据分页显示 数据增删改查

目录 引出一些固定的东西1.固定的响应格式2.name 变成 v-model 进行双向绑定3.下拉框选中--:value"type.id"4.vue导包固定写法5.script固定写法6.axios的get请求7.axios的post请求8.前端美化&#xff1a; 数据分页显示1.后端改成resp响应2.前端的修改要点&#xff08…

揭秘报表新玩法!标配插件不再单调,如何用柱形图插件让你的报表瞬间高大上!

摘要&#xff1a;本文由葡萄城技术团队于CSDN原创并首发。转载请注明出处&#xff1a;葡萄城官网&#xff0c;葡萄城为开发者提供专业的开发工具、解决方案和服务&#xff0c;赋能开发者。 前言 图表作为一款用于可视化数据的工具&#xff0c;可以帮助我们更好的分析和理解数…

flutter自定义系列之简单的K线图绘制

上篇文章讲了flutter自定义的相关流程&#xff0c; 今天继续练习下flutter的自定义K线&#xff1a; 我们可以通过自定义Painter来实现一个简单的K线图界面&#xff1a; 创建一个自定义的Painter&#xff0c;用于绘制K线图&#xff1a; import dart:ui;import package:flutte…

聊聊多线程

摘要 开发过程中&#xff0c;总会遇到一些并发安全问题。本文总结出常用的数据结构哪些是安全的&#xff0c;哪些是不安全的以及他们为什么是不安全。 java中sychronize锁的原理&#xff1a; 常见的数据结构 类型 数据结构是否安全ArrayList数组 不安全HashMap数…

Mocha Pro:AdjustTrack 模块

跟踪时由于缺乏细节或有障碍物阻挡&#xff0c;跟踪点发生了漂移&#xff0c;或者一个或多个跟踪点可能会离开画面&#xff0c;此时可考虑使用 AdjustTrack &#xff08;调整跟踪&#xff09;模块手动设置关键帧来获得更精准的跟踪数据。 尤其是当要利用表面 Surface区域进行插…

随机数组归并问题

1 问题 生成两个任意的随机数组&#xff0c;并将这两个数组按照数字大小按顺序归并到一个新数组中。 2 方法 思路&#xff1a;定义三个数组&#xff0c;两个数组自己输入值&#xff0c;第三个数组用来作归并后的数组&#xff0c;先将两个数组的值全部赋给第三个数组&#xff0c…

极简主义的远程文件浏览器Mikochi

什么是 Mikochi &#xff1f; Mikochi 是一个远程文件浏览器&#xff0c;用于自托管服务器 / NAS。它允许您浏览远程文件夹、上传文件、删除、重命名、下载和流式传输文件到 VLC/mpv。它带有一个由 JavaScript/Preact 提供支持的 Web 界面&#xff0c;以及一个内置于 Go/Gin 中…

ChatGPT 教我用 200 行代码写一个简版 Vue 框架 - OpenTiny

AI 是未来最好的老师 最近&#xff0c;我正在准备一份关于 Vue 基础的学习材料。期间我突发奇想&#xff1a;能否利用现在热门的 ChatGPT 帮我创建学习内容&#xff1f;其实 Vue 本身不难学&#xff0c;特别是基础用法&#xff0c;但是&#xff0c;如果你想深入掌握 Vue&#…

数据挖掘(7.1)--数据仓库

目录 引言 一、数据库 1.简介 2.数据库管理系统(DBMS) 二、数据仓库 数据仓库特征 数据仓库作用 数据仓库和DBMS对比 分离数据仓库和数据库 引言 数据仓库的历史可以追溯到20世纪60年代&#xff0c;当时计算机领域的主要工作是创建运行在主文件上的单个应用&#xff0…

LaravelPHP笔记-响应头去掉(隐藏)X-Powered-By

最近想搞个小项目&#xff0c;后端先用PHP&#xff0c;框架是Laravel但http响应头如下&#xff1a; 头带有X-Powered-By: PHP/7.3.33&#xff0c;这样很不安全&#xff0c;应该要隐藏&#xff0c;查了下百度。都是一个抄一个。 在代码中添加&#xff1a; header_remove(x-pow…

【几分醉意赠书活动 - 02期】 | 《前端系列丛书》

个人主页&#xff1a; 几分醉意的CSDN博客主页_传送门 个人主页&#xff1a; 陈老板的CSDN博客主页_传送门 赠书活动 | 第二期 本期好书推荐&#xff1a;《前端系列丛书》 粉丝福利&#xff1a;书籍赠送&#xff1a;共计送出30本 参与方式&#xff1a;关注公众号&#xff1a;码…

Flutter控件封装之轮播图Banner

Flutter中实现轮播图的方式有很多种&#xff0c;比如使用三方flutter_swiper&#xff0c;card_swiper等等&#xff0c;使用这些三方&#xff0c;可以很快很方便的实现一个轮播图展示&#xff0c;基本上也能满足我们日常的开发需求&#xff0c;如果说&#xff0c;想要一些定制化…

CloudFlare系列--使用第三方来自定义CDN的IP(笨牛简洁版)

原文网址&#xff1a;CloudFlare系列--使用第三方来自定义CDN的IP(笨牛简洁版)_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍CloudFlare的CDN如何自定义第三方IP。 概述 CloudFlare官网接入域名的方式只能是 NS 接入&#xff0c;这样默认DNS服务器只能改为CloudFlare的D…