哈夫曼树的概念以及算法简述
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
#include
#include
#include "haffmantree.h"
#include
/**
进行哈夫曼树编码的过程
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
#include
#include
#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