树:哈夫曼编码解码及压缩存储

哈夫曼树

哈夫曼树即最优二叉树,算法如下:
(1)在当前未使用结点中找出两个权重最小的作为左右孩子,计算出新的根结点
(2)新的根结点继续参与过程(1),直至所有结点连接为一棵树

如下图,symbol为具体字符,Frequency为出现频率(权重)
频率表

Huffman树构建过程

特点:只有度数为0和2的结点


C语言静态链表实现哈夫曼树

实现功能:输入一段英文文本,统计各字符出现次数作为权重,以当前字符集生成哈夫曼树,给出所有字符及指定编码与文本编码,再将编码后的文本解码为原文

数据结构定义

下标数据域父结点下标左子树下标右子树下标
typedef char ElementType;
typedef struct {
	ElementTree data;	结点数据
	int weight;	结点权重
	int parent;		双亲下标
	int left, right;		左右子树下标
}HuffmanTree;
  • 统计字符及初始化静态链表
    用下标从10 ~ 126的数组记录ASCII码10 ~ 126的字符,包含了英文文本的绝大多数字符
int num = 1;当前静态链表有效长度 [1,num)
char text[2005];//文本源

char *textAnalyze() {//返回字符集中字符个数的数组
	char *chars = (char *)malloc(sizeof(char)*127);//32~126
	char c = 0;
	memset(chars, 0, sizeof(char)*127);
	scanf("%[^\n]", text);
	int i = 0;
	while (c = text[i++]) {
		chars[c]++;//计数
	}
	for (i = 32; i <= 126; i++) {
		if (chars[i]) {
			printf("字符%c 出现%d次\n", i, chars[i]);
		}
	}
	return chars;
}
void initElement(HuffmanTree *nodes, char *chars) {
	memset(nodes, 0, sizeof(char) * sizeof(HuffmanTree) * 200);
	for (int i = 32; i <= 126; i++) {
		if (chars[i]) {
			nodes[num].data = i;
			nodes[num].weight = chars[i];
			nodes[num].parent = nodes[num].left = nodes[num].right = 0;
			num++;//全局变量记录当前结点总数
		}
	}
	free(chars);
}///初始化静态链表完毕

eg:
输入为affgghhhjjj

此时静态链表为

下标数据域权重父结点下标左子树下标右子树下标
1a1000
2f2000
3g2000
4h3000
5j3000

建立哈夫曼树
void createHuffmanTree(HuffmanTree *nodes) {
	每次连接两个结点,生成一个新结点,连接完成应该生成n-1个结点
	故	n个结点建立的哈夫曼树应当有2n-1个结点
	int end = num + num - 3;//计算总结点数
	int *min = NULL;
	while (num != end+1) {
		min = searchOrder(nodes);
		//制作新结点
		nodes[num].weight = nodes[min[0]].weight + nodes[min[1]].weight;
		nodes[num].left = min[0];
		nodes[num].right = min[1];
		//填补原结点
		nodes[min[0]].parent = num;
		nodes[min[1]].parent = num;
		num++;
		free(min);
	}
}

其中searchOrder( )返回当前权重最小值与次小值的下标

int *searchOrder(HuffmanTree *nodes) {// num>=2
	int *nums = (int *)malloc(sizeof(int) * 2);
	int i = 1;
	for (; i < num&&nodes[i].parent != 0; i++);//nodes[i].parent == 0 可用
1*-	nums[0] = i;//        0  pre      1 later
	for (i++; i < num&&nodes[i].parent != 0; i++);
	nums[1] = i;//找到初始两下标
	for (i++; i < num; i++) {
		if (nodes[i].parent == 0) {//未使用
			if (nodes[i].weight < nodes[nums[1]].weight) {//  <min
				nums[1] = i;
			}
			else if (nodes[i].weight < nodes[nums[0]].weight) {
				nums[0]=nums[1],nums[1] = i;
			}
		}//按出现顺序生成最优二叉树
	}
	return nums;
}

此时的哈夫曼树为

下标数据域权重父结点下标左子树下标右子树下标
1a1600
2f2600
3g2700
4h3700
5j3800
63812
75934
86956
911078

可以看出叶子结点左右孩子均为0,根结点父结点域为0


哈夫曼编码

前缀编码:每个字符的编码都不为其余编码的前缀
非前缀编码:存在某字符的编码是其余某编码的前缀
(没错就是这么扭曲)

所有参与编码的字符都在叶子结点上,因此保证编码为前缀编码

哈夫曼编码:走左子树为0,走右子树为1。从树根走到叶子结点组成的01序列
哈夫曼编码是前缀编码

  • 遍历哈夫曼树得到每个叶子结点的编码

typedef struct {
	ElementTree data;字符
	char hfCode[115];该字符对应的编码序列
}HfCode;
HfCode codes[111];存储每个字符的哈夫曼编码
int charNum = 0;//字符集中的字符个数 [0,num)
char encodedText[4005];//编码后的文本

void encodeAll(HuffmanTree *nodes, int index, char *order, int cnt) {
	if (nodes[index].left == nodes[index].right) {
		printf("%c : ", nodes[index].data);
		order[cnt] = 0;
		puts(order);
		codes[charNum].data = nodes[index].data;
		strcpy(codes[charNum++].hfCode,order);
	}
	if (nodes[index].left) {
		order[cnt] = '0';
		encodeAll(nodes, nodes[index].left, order, cnt+1);
		order[cnt] = '1';
		encodeAll(nodes, nodes[index].right, order, cnt+1);
	}
}
  • 从叶子结点走到根得到该叶子的编码
void getCodeByChar(HuffmanTree *nodes, char leaf) {//得到某个叶子节点的编码
	int index;
	int end = num / 2;
	for (index = 0; index <= end; index++) {
		if (nodes[index].data == leaf)
			break;
	}
	if (index > end) {
		printf("输入有误!");
		return;
	}
	char order[115];
	int cnt = 0;
	while (nodes[index].parent) {不为根
		order[cnt++] = nodes[nodes[index].parent].left == index ? '0' : '1';
		index = nodes[index].parent;
	}
	printf("%c : ", leaf);
	for (cnt--; cnt >= 0; cnt--) {
		printf("%c", order[cnt]);
	}
	printf("\n");
}

在建立哈夫曼树并得到各字符编码的基础上对整个文本进行编码/解码就十分容易了

void encodeText(HuffmanTree *nodes) {//编码
	int i, j, len = strlen(text);
	printf("该信息为:\n%s\n", text);
	printf("该信息的哈夫曼编码为:\n");
	for (i = 0; i < len; i++) {
		for (j = 0; j < charNum&&codes[j].data != text[i]; j++);
			strcat(encodedText, codes[j].hfCode);
	}
	printf("%s\n",encodedText);
}

void decodeText(HuffmanTree *nodes, char *unknown) {//解码
	int len = strlen(unknown);
	int root = num - 1;
	int i, index;
	for (i = 0, index = root; i < len; i++) {
		index = unknown[i] == '0' ? nodes[index].left : nodes[index].right;
		if (nodes[index].left == 0) {
			printf("%c", nodes[index].data);
			index = root;
		}
	}
}

解码的主要思路是从哈夫曼树的根开始,遍历整个01序列,按照编码的方式,0向左走,1向右走,走到叶子结点输出,即译出一个字符,循环变量重新回到根结点继续解译下一个字符。因为前缀编码的前提保证,不会有歧义。


2019/11/19

编码的压缩存储

编码后的文件通常比原文件要大,因为每个字符以多个字符的01编码形式存储。既然是01编码,就有更为高效的存储方式。关键操作是将文本编码后得到的01串中的每个0或1以位的方式存储。

那么如何将01串的每个01按顺序填到 bit 上呢?
有两种解决方案:

  • 利用位运算将编码后的01串按字节填到每一个 bit 上,再将得到的 char[ ] 写入文件
  • 编码后的01串每八位一组,计算数值(char类型),再将得到的 char[ ] 写入文件

下面是位运算的方式将01串填成 char[ ]

将地址destination后第bits位为置0
void setZero(void *destination, int bits) {//将第bits位置0
	char *des = (char *)destination;
	char zero[8] = { 0b01111111,0b10111111,0b11011111,0b11101111,0b11110111,0b11111011,
		0b11111101,0b11111110, };
	int bit = bits / 8;//des前进字节数
	bits = bits % 8;//目标位数
	des += bit;
	*des = *des&zero[bits];
}

将地址destination后第bits位为置1
void setOne(void *destinaton, int bits) {//将第bits为置1
	char *des = (char *)destinaton;
	char one[8] = { 0b10000000,0b01000000,0b00100000,0b00010000, 0b00001000,
		0b00000100, 0b00000010, 0b00000001, };
	int bit = bits / 8;//des前进字节数
	bits = bits % 8;//目标位数
	des += bit;
	*des = *des|one[bits];
}

但是01串并不一定能刚好被8整除,因此多写入一个字节,表示最后一个字节的剩余量
比如说编码后的01串为
0000 1111 001(11位)
那么将写入 11/8+1=2 个字节 如果刚好16字节则不需要加1
再多写入一个数表示最后一个字节的补足位数 8-11%8=5 (需要补五位)
那么写入文件的两个表示数据的字节是
0x0F,0x20
表示余量的数是0x05

示例程序如下:

int main() {
	char num[100]= {0};
	char str[] = "000011110000100000100010";
	int len = strlen(str);
	for(int i = 0; i<len; i++) {
		str[i]=='1'?setOne(num,i):setZero(num,i);
	}
	
	//写入 
	int left = 8-len%8;
	int bits = len/8+(len/8?1:0);
	FILE *fp = fopen("test.xx","wb");
	fwrite(&left,1,1,fp);
	int cnt = 0;
	while(1){
		fwrite(num+cnt,1,1,fp);
		cnt++;
		if(cnt==bits)break;
	}
	fclose(fp);
	
	//读出 
	int get_left; 
	int get_bits = 0;//记录读取的字节数 
	char get_num[1000];//足够大小 
	fp = fopen("test.xx","rb");
	fread(&get_left,1,1,fp);//读出补足字节数 
	while(fread(get_num+get_bits,1,1,fp)==1){
		get_bits++;
	} 
	fclose(fp);
	
	//还原为01串
	char get_str[100]={0};
	cnt = 0;//为get_str赋值 
	char judge[] = { 0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01 };
	for (int i = 0; i < get_bits-1; i++) {//先不读取最后一个有补足位数的byte 
		for (int bit = 0; bit < 8; bit++) {//取出每一位  
			get_str[cnt++] = ((get_num[i] & judge[bit]) == judge[bit]) ? '1' : '0';
		}
	}
	for (int bit = 0; bit < 8-left; bit++) {//按补足位数读取最后一个字节
			get_str[cnt++] = ((get_num[get_bits-1] & judge[bit]) == judge[bit]) ? '1' : '0';
	}
	
//	get_str[strlen(get_str)-left] = 0;//或使用left进行截断 
	puts(get_str);
	return 0;
}

在这里插入图片描述
在这里插入图片描述

计算数值再写入文件的方法不再赘述。但注意可能遇到的大小端问题。


2019/6/21 更新

  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值