使用C语言实现哈夫曼树的编码,压缩和解码过程_c语言哈夫曼树编码解码-程序员宅基地

技术标签: c语言  哈夫曼树  数据结构  

哈夫曼树的概念以及算法简述

1.相关名词概念:

  路径和路径长度:从树中的一个节点到达另外一个节点的之间的分支为路径,其长度为路径长度。树的路径长度定义为从根节点开始到达每一个节点的路径长度之和。

  权值:节点的权值为该节点在所有节点中占的比重,拿一个文本文件来说,某个字符的节点权重即为该字符出现的次数与该文本文件中所有字符出现的次数的比值。

带权路径长度:树中某个节点的带权路径长度为该节点的路径长度与该节点的权重的乘积。一颗树的带权路径长度(WPL)为该树中的所有叶节点的带权路径长度之和。

带权路径长度最小的树二叉树即为哈夫曼树(最优二叉树)!

哈夫曼树的算法描述:

1.根据给定的n个权值,构成n棵二叉树的集合:F = {T1,T2,T3,T4,…}。其中每棵二叉树中只有一个带权值为Wi的根节点,其左右孩子都为空。
2,在F中选取两棵权值最小的树,作为一个新节点的左右子树,构造出一课新的二叉树,且置新的树的权值为两棵左右子树的权值之和。
3,在F中删除这两棵左右子树,并将新的二叉树加入F中。
4,重复2,3过程,直到F只含有一棵二叉树。(这里即为树中根节点)

哈夫曼树的抽象数据类型的定义
/* 哈夫曼树节点 */
typedef struct _haffman {
	char data;							//用来存放节点字符的数据域
	int weight;							//权重
	struct _haffman *leftChild;			//左孩子节点
	struct _haffman *rightChild;		//右孩子节点

}HaffNode;

宏定义

#define MAX_SIZE 256					//编码数量
#define HALF_MAX 128					//一半的数量
#define ASCII_SIZE 128					//ASCII码的数量

定义一些需要用到的全局变量

/* 以顺序结构存储的树节点--编码解码的字符映射表 --即存储原数据*/
HaffNode node[MAX_SIZE];

/* 用来保存所有左孩子节点--为总节点数的一半 */
HaffNode left[HALF_MAX];

/* 用来保存所有右孩子节点 --为总节点数的一半*/
HaffNode right[HALF_MAX];

/** 创建一个二维数组,用来保存每一个叶节点的编码 */
char code[MAX_SIZE][HALF_MAX];
构造哈夫曼树的代码实现:
/* 构造哈夫曼树
	@param node 哈夫曼树的根节点
	@param length 节点数组的长度

*/
void CreatHaffmanTree(HaffNode * node, int length)
{
	if (length <= 0)
	{
		printf("长度为0,无法创建哈夫曼树!\n");
		return;
	}
	SortHaffmanNode(node, length);		//先进行节点权值的排序
	HaffNode parent;									//构建一个以node数组最后两个节点组成的父节点
	left[length - 1] = node[length - 1];				//权重最小的节点
	right[length - 1] = node[length - 2];				//权重第二小的节点
	parent.weight = left[length - 1].weight + right[length - 1].weight;	//累加权重
	parent.leftChild = &left[length - 1];				//左孩子指向相对小的值	
	parent.rightChild = &right[length - 1];				//右孩子指向相对更大的值
	node[length - 2] = parent;					//将倒数第二个替代为parent节点,数组长度 - 1,递归创建哈夫曼树
	CreatHaffmanTree(node, length - 1);			//递归,并且长度自动减一,每一次都会进行一次重新排序。

}

实现构造哈夫曼树前需要先将节点数组的权重进行排序,即为上述SorHaffmanNode();函数,这里我使用的是冒泡排序的方法进行排序。
以下是其代码实现


/* 哈夫曼树的排序 --使用冒泡排序
 *@param node 节点数组
 * @param length 节点的数量
*/
void SortHaffmanNode(HaffNode * node, int length)
{
	HaffNode tempNode;
	for (int i = 0; i < length - 1; i++)
	{
		for (int j = 0; j < length - i - 1; j++)
		{
			if (node[j].weight < node[j + 1].weight)		//根据权重比较来排序--从大到小
			{
				tempNode = node[j];
				node[j] = node[j + 1];
				node[j + 1] = tempNode;
			}
		}
	}
}

哈夫曼树的编码:

过程:从根节点出发,左支的编码为0,右支的编码为1.直到叶节点结束,其编码即为该节点数据的编码。每一次遍历都重新从根节点出发重新遍历。
代码实现

/**
 * 创建一个编码函数
 * @param node 哈夫曼树节点数组
 * @param tempnode 编码后的字符数组
 * @param index 当前操作节点数组的下标
 */
void Coding(HaffNode * node, char * tempnode, int index)
{
	if (!node) return;
	if (node->leftChild == NULL || node->rightChild == NULL)
	{
		//使用字符数组,将编码形式保存
		tempnode[index] = '\0';			//字符串结束的标志
		strcpy(code[node->data - 0], tempnode);			//这里叶节点的值使用的是字母,可以使用ASCII码的形式确认存储的位置,也可以用强制类型转换
	}
	tempnode[index] = '0';			//左支路的编码为0
	Coding(node->leftChild, tempnode, index + 1);			//先递归调用左支路,
	tempnode[index] = '1';			//右支路的编码为1
	Coding(node->rightChild, tempnode, index + 1);			//再递归调用右支路
}

哈夫曼树的解码过程:

过程:对于一段由编码0/1构成的二进制文件,在同个树而言,重第一个开始,遇到0则访问左子树,遇到1则访问右子树。直到该节点没有了左右节点为止,即这段01字符即可解码为该叶子节点所对应的字符。
代码实现

/** 解码过程 -- flag 0/1 来表示往哪边走,左走0,右走1 */
HaffNode *Unziped(HaffNode *node, int flag)
{
	if (flag == 1)
		return node->rightChild;		//右子树
	else if(flag == 0)
		return node->leftChild;			//左子树
	return NULL;
}

下面是所有的代码实现:
main.c

#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include "HaffmanTree.h"
#include <string.h>

/**
	进行哈夫曼树编码的过程
	1,打开文件,进行文件的字符读取,并进行哈夫曼编码,将压缩的数据保存另外一个文件ziped.txt中
	2,解码文件,对二进制文件进行解码,将解码的结果保留在另外一个文件result.txt中

*/

int main() {
    
	//保存到二进制文件中的无符号字符,即解码过后对应的字符,这里即为00000000
	unsigned char saveChar = 0;
	//中间变量
	unsigned char tempchar = 0;	
	
	printf("使用哈夫曼树实现文件的压缩,(只支持ASCII字符)\n");
	//以只读的形式打开文本文件,该文件就是要进行压缩的源文件
	FILE * inputFile = fopen("input.txt", "r");			
	//以二进制的形式写入该文本文件,相当于压缩包	
	FILE * zipedFile = fopen("ziped.txt","wb");					
	
	//文件中保存的字符个数,源文件
	int fileLength = 0;		
	//定义一个 数组,用来保存每个ASCII码的权重									
	int asciicount[128] = {
    0};	
	//保留读取到的字符								
	char readChar;												

	while ((readChar = fgetc(inputFile)) != EOF) {
    
		//逐个读取字符,并累加
		fileLength++;
		//将读取到的字符作为下标,统计每个字符出现的次数										
		asciicount[readChar - 0]++;								
	}
	//字符种类计数器					
	int cnt = 0;												
	for (int i = 0; i < 128; i++) {
    
		if(asciicount[i] != 0) {
     //统计节点的数量,即文件中出现多少种字符 
			node[cnt].data = i;
			// 将字符出现的次数当成权重 
			node[cnt].weight = asciicount[i];
			//节点数量累加
			cnt++;								
		}
	}
	CreatHaffmanTree(node, cnt);	//创建哈夫曼树
	char tempcode[10];				//编码的值 0/1
	Coding(node, tempcode, 0);		//编码,从零开始
	cnt = 0;						//计数器置零
	fseek(inputFile, 0L, 0);		//将文件定位于文件头,偏移0个字节长度
	int zipedLength = 0;
	while ((readChar = fgetc(inputFile)) != EOF) {
    		//逐位将编码保存到文件中
		for (int i = 0; i < strlen(code[(int)readChar]); i++) {
    
			saveChar |= (code[(int)readChar][i] - '0');			//将saveChar的最后一位赋值为0/1
			cnt++;												//累加计数器
			if (cnt == 8) {
    	//如果计数累加到8,8位一个字符,则写入文件
				//在文件中写入一个unsigned char 大小的字符
				fwrite(&saveChar, sizeof(unsigned char), 1, zipedFile);		
				cnt = 0;
				saveChar = 0;
				zipedLength++;					//压缩文件字符大小累加,方便算出压缩比
			} else {
    
				//saveChar的值左移一位,为下次给最低位赋值做好准备
				saveChar <<= 1;										
			}

		}

	}
	if (cnt < 8) {
    			//处理不足8个字符的情况
		saveChar <<= (8 - cnt);
		cnt = 0;
		saveChar = 0;
		fwrite(&saveChar, sizeof(unsigned char), 1, zipedFile);
		zipedLength++;
	}
	fclose(inputFile);
	fclose(zipedFile);
	printf("文件压缩成功,请在文件夹中查看以压缩的文件\n");
	printf("源文件的字符个数:%d,压缩后文件的个数:%d  %.2lf\n", fileLength, zipedLength, (double)zipedLength / fileLength);

	//哈夫曼树的解码过程
	zipedFile = fopen("ziped.txt", "rb");
	FILE * resultFile = fopen("result.txt", "w");
	cnt = 0;	//计数器清零
	HaffNode * currNode = &node[0];
	while (fread(&readChar, sizeof(unsigned char), 1, zipedFile)) {
    
		if (fileLength == 0)
			break;
		//遍历readChar中的每个二进制
		for (int i = 0; i < 8; i++) {
    
			tempchar = readChar & 128;				//取readChar的最高128为10000000
			tempchar >>= 7;							//每一次右移7位
			readChar <<= 1;							//最高位已经被取了,所以在进行移动
			currNode = Unziped(currNode, tempchar - 0);
			//判断叶节点
			if (currNode->leftChild == NULL || currNode->rightChild == NULL) {
    
				fprintf(resultFile, "%c", currNode->data);
				cnt++;
				currNode = &node[0];			//每一次遍历都需从根节点开始
			}
		}
	}
	fclose(inputFile);
	fclose(resultFile);
	printf("解码成功,请查看文件;result.txt");

	system("pause");

	return 0;
}

Haffman.h

#pragma warning(suppress : 4996)

/*
  哈夫曼树的顺序存储
  哈夫曼树编码的压缩文件的主要思路
  1,读取某个文本文件,统计各个字符出现的个数,作为权重
  2,构建哈夫曼树,生成每个字符对应的编码,将编码保存到压缩文件中
  3,文件解压缩,实际就是将压缩文件翻译过来,然后在保存在解压缩文件中的过程
  
  @autor 小机

*/

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

#define MAX_SIZE 256					//编码数量
#define HALF_MAX 128					//一半的数量
#define ASCII_SIZE 128					//ASCII码的数量

/* 哈夫曼树节点 */
typedef struct _haffman {
    
	char data;							//用来存放节点字符的数据域
	int weight;							//权重
	struct _haffman *leftChild;			//左孩子节点
	struct _haffman *rightChild;		//右孩子节点

}HaffNode;

/* 以顺序结构存储的树节点--编码解码的字符映射表 --即存储原数据*/
HaffNode node[MAX_SIZE];

/* 用来保存所有左孩子节点--为总节点数的一半 */
HaffNode left[HALF_MAX];

/* 用来保存所有右孩子节点 --为总节点数的一半*/
HaffNode right[HALF_MAX];

/** 创建一个二维数组,用来保存每一个叶节点的编码 */
char code[MAX_SIZE][HALF_MAX];

/* 哈夫曼树的排序 --使用冒泡排序的方法*/
void SortHaffmanNode(HaffNode * node, int length);

/* 构造哈夫曼树
	@param node 哈夫曼树的节点数组
	@param length 节点数组的长
*/
void CreatHaffmanTree(HaffNode * node, int length);

/** 
 * 创建一个编码函数
 * @param node 哈夫曼树节点数组
 * @param tempnode 编码后的字符数组
 * @param index 当前操作节点数组的下标
 */
void Coding(HaffNode * node, char * tempnode, int index);

/** 解码过程 -- flag 0/1 来表示往哪边边的树枝走,左走0,右走1 */
HaffNode *Unziped(HaffNode *node, int flag);

Haffman.c

#include "HaffmanTree.h"


/* 哈夫曼树的排序 --使用冒泡排序*/
void SortHaffmanNode(HaffNode * node, int length) {
    
	HaffNode tempNode;
	for (int i = 0; i < length - 1; i++) {
    
		for (int j = 0; j < length - i - 1; j++) {
    
			if (node[j].weight < node[j + 1].weight) {
    	//根据权重比较来排序--从大到小
				tempNode = node[j];
				node[j] = node[j + 1];
				node[j + 1] = tempNode;
			}
		}
	}
}
/* 构造哈夫曼树
	@param node 哈夫曼树的根节点
	@param length 节点数组的长度

*/
void CreatHaffmanTree(HaffNode * node, int length) {
    
	if (length <= 0) {
    
		printf("长度为0,无法创建哈夫曼树!\n");
		return;
	}
	// 先排序
	SortHaffmanNode(node, length);
	//构建一个以node数组最后两个节点组成的父节点
	HaffNode parent;
	//权重最小的节点
	left[length - 1] = node[length - 1];
	//权重第二小的节点
	right[length - 1] = node[length - 2];
	parent.weight = left[length - 1].weight + right[length - 1].weight;	//累加权重
	//左孩子指向相对小的值
	parent.leftChild = &left[length - 1];
	//右孩子指向相对更大的值
	parent.rightChild = &right[length - 1];
	//将倒数第二个替代为parent节点,数组长度 - 1,递归创建哈夫曼树
	node[length - 2] = parent;
	//递归,并且长度自动减一
	CreatHaffmanTree(node, length - 1);

}

/**
 * 创建一个编码函数
 * @param node 哈夫曼树节点数组
 * @param tempnode 编码后的字符数组
 * @param index 当前操作节点数组的下标
 */
void Coding(HaffNode * node, char * tempnode, int index) {
    
	if (!node) return;
	if (node->leftChild == NULL || node->rightChild == NULL) {
    
		//使用字符数组,将编码形式保存
		tempnode[index] = '\0';			//字符串结束的标志
		//这里叶节点的值使用的是字母,可以使用ASCII码的形式确认存储的位置,也可以用强制类型转换
		strcpy(code[node->data - 0], tempnode);			
	}
	tempnode[index] = '0';			//左支路的编码为0
	Coding(node->leftChild, tempnode, index + 1);			//先递归调用左支路,
	tempnode[index] = '1';			//右支路的编码为1
	Coding(node->rightChild, tempnode, index + 1);			//再递归调用右支路
}


/** 解码过程 -- flag 0/1 来表示往哪边走,左走0,右走1 */
HaffNode *Unziped(HaffNode *node, int flag) {
    
	if (flag == 1)
		return node->rightChild;		//右子树
	else if(flag == 0)
		return node->leftChild;			//左子树
	return NULL;
}

注意上面程序只能编码ASCII码的文件,如果文件中有中文字符的存在会存在乱码的情况,默认输入文件为input.txt,压缩文件为ziped.txt,解压文件为result.txt

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_44184990/article/details/100695436

智能推荐

TTL、CMOS、RS232、RS485、RS422、CAN、USB电平说明 与通讯芯片_单片机引脚一般是ttl-程序员宅基地

文章浏览阅读2.8k次,点赞4次,收藏40次。什么是TTL电平、CMOS电平、RS232电平?它们有什么区别呢?一般说来,CMOS电平比TTL电平有着更高的噪声容限。单片机串口输出的是TTL电平,电脑串口输出的是RS232电平,需要芯片转换。(一)、TTL电平标准输出 L: &lt;0.8V ; H:&gt;2.4V。输入 L: &lt;1.2V ; H:&gt;2.0VTTL器件输出低电平要小于0.8V,高电平要大于2.4V..._单片机引脚一般是ttl

Flutter原理:三棵重要的树(渲染过程、布局约束,flutterlistview原理复用_flutter focus原理-程序员宅基地

文章浏览阅读2.7k次。每次,当控件挂载到控件树上时,Flutter 调用其 createElement() 方法,创建其对应的 Element。Flutter 再将这个 Element 放到元素树上,并持有创建它控件的引用,如下图:控件会有它的子树:子控件也会创建相应 Element 被放在元素树上:4Element 中的状态我们上文提到了 Widget 的不可变性,相应的 Element 就有其可变性,正如我们前文所说的它被标记为 dirty Element 便是作为需要更新的状态,另外一个我们需要格外注意的是,_flutter focus原理

zepto.js选择器_zepto.js 属性选择-程序员宅基地

文章浏览阅读2.1k次。1.$是一个函数构造器_zepto.js 属性选择

tornado 异步执行shell命令并返回执行结果_486cpu tornado shell命令-程序员宅基地

文章浏览阅读1k次。思路:通过tornado框架构建web服务器,通过执行后台命令程序获取监控目标状态或监控结果,根据状态或结果,通过websocket发送信息到前端进行相应的展现。问题:tornadoweb框架是异步处理的,其核心是将事务都放入到ioloop异步循环中。但通常使用python调用shell脚本或者执行的shell命令,以及python打开文件的操作都是同步阻塞模式,无法加入到ioloop中。在下面的连接中提到了如何将shell命令通过异步方式执行并获取执行结果。https://www.cn..._486cpu tornado shell命令

设计类似于抖音、小红书、微博等方式的主题点赞与评论的数据库表_小红书 评论 表结构-程序员宅基地

文章浏览阅读2.8k次。转载请注明Garcia主题设计: 主题ID、用户ID、主题标题、主题城市、主题位置名称、主题位置详细地址、地理经度、地理纬度、 主题展示内容(100)、主题展示媒体文件路径(List图片名称)、点赞数、评论数、收藏数,转发数、 是否为转发主题、被转发主题ID、被转发主题发行人ID、被转发主题发行人名称、创建时间、更新时间主题内容设计: 主题ID,主题完整内容主题点赞表: 主题ID、用户ID、状态(1有效,0取消)主题主评论表(根据点赞数排序)..._小红书 评论 表结构

牛客练习赛76_牛客练习赛76b-程序员宅基地

文章浏览阅读141次。牛客练习赛76B zzugzx (vs) Kurisu是一个博弈游戏注意到(m+1)^n<=5000那么我们是可以直接考虑爆搜的总共N个回合,那么两个人就是2*N次操作定义f[a][b]代表当 ,zzugzx 选了a的数,Kurisu选了b的数,zzugzx赢的概率a和b分别是n位m+1进制的数,代表n回合他抽到1-m的数放在1~n哪个位置#include<bits/stdc++.h>using namespace std;int ok[5000][5000];dou_牛客练习赛76b

随便推点

异构计算 — CPU+GPU_异构计算 cpu gpu-程序员宅基地

文章浏览阅读5.9k次。目录文章目录目录CPU-GPU 异构计算系统分离式架构CPU-GPU 异构计算系统在现代的异构计算系统中,GPU 是以 PCIe 卡的形式作为 CPU 的辅助计算设备。根据 CPU 和 GPU 是否共享了内存,可分为两种类型的 CPU-GPU 异构计算架构:分离式架构:CPU 和 GPU 拥有各自独立的缓存和内存,两者之间通过 PCIe 总线通信。目前主要做计算机、智能手机中使用。耦合式架构:CPU 和 GPU 共享内存和缓存。AMD 的 APU 采用的就是这种结构,目前主要使用在游戏主机中。_异构计算 cpu gpu

小米10pro卡刷教程 卡刷升级官方系统方法_小米10如何刷澎湃系统-程序员宅基地

文章浏览阅读1.3w次。来源:智能手机网小米10pro卡刷升级官方系统图文步骤1、确保手机电量充足,己经下载好了官方卡刷包,还没有下载的请下载小米10pro官方完整卡刷包。2、将手机连接电脑,打开存储模式,将下载后的zip格式的压缩包不要解压,直接拷贝至内置存储 /downloaded_rom 文件夹下,或仅包含"英文或数字"路径的文件夹下。3、然后进入小米10pro手机中“设置-我的设备,如下图所示:..._小米10如何刷澎湃系统

浅析J2EE,J2SE,J2ME_简述j2se.j2ee和j2me的特点及使用方向-程序员宅基地

文章浏览阅读1k次。J2EE,J2SE,J2ME三者有什么不同?J2EE,J2SE,J2ME是Sun 公司的Java多个版本,就像Windows XP还有专业版和家庭版是一样的。J2EE:Java 2 Platform Enterprise Edition 企业版,用于企业应用,支持分布式部署。J2SE:Java 2 Platform Standard Edition 标准版,用于桌面应用,也是J2EE的_简述j2se.j2ee和j2me的特点及使用方向

clickhouse SummingMergeTree表引擎-程序员宅基地

文章浏览阅读1.3k次。该引擎继承了MergeTree引擎,当合并 SummingMergeTree 表的数据片段时,ClickHouse 会把所有具有相同主键的行合并为一行,该行包含了被合并的行中具有数值数据类型的列的汇总值,即如果存在重复的数据,会对对这些重复的数据进行合并成一条数据,类似于group by的效果。推荐将该引擎和 MergeTree 一起使用。例如,将完整的数据存储在 MergeTree 表中,并且使用 SummingMergeTree 来存储聚合数据。这种方法可以避免因为使用不正确的主键组合方式而丢失数据。_clickhouse summingmergetree

CUDA c programming guide_cude c program model-程序员宅基地

文章浏览阅读3.1k次。http://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#axzz4FIp5fBgMCUDA C Programming GuideChanges from Version 7.0Updated C/C++ Language Support to:Added new_cude c program model

Redis(十八)-Redis的数据结构之整数集合_redis int64 9.2233720368548e+18-程序员宅基地

文章浏览阅读2.4k次。本文简单介绍了整数集合这种数据结构,整数集合是集合键的底层实现之一,是专门用来存储整数的,整数集合的底层实现是数组,这个数组以有序,无重复的方式保存集合元素,在有需要时,程序为会根据新添加元素的类型,改变这个数组的类型,升级操作为整数集合带来了操作上的灵活性,并且尽可能节约了内存。_redis int64 9.2233720368548e+18

推荐文章

热门文章

相关标签