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


    哈夫曼树

    哈夫曼树即最优二叉树,算法如下:
    (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("%[^
    ]", text);
    	int i = 0;
    	while (c = text[i++]) {
    		chars[c]++;//计数
    	}
    	for (i = 32; i <= 126; i++) {
    		if (chars[i]) {
    			printf("字符%c 出现%d次
    ", 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

    此时静态链表为

    下标 数据域 权重 父结点下标 左子树下标 右子树下标
    1 a 1 0 0 0
    2 f 2 0 0 0
    3 g 2 0 0 0
    4 h 3 0 0 0
    5 j 3 0 0 0

    建立哈夫曼树

    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;
    }
    

    此时的哈夫曼树为

    下标 数据域 权重 父结点下标 左子树下标 右子树下标
    1 a 1 6 0 0
    2 f 2 6 0 0
    3 g 2 7 0 0
    4 h 3 7 0 0
    5 j 3 8 0 0
    6 3 8 1 2
    7 5 9 3 4
    8 6 9 5 6
    9 11 0 7 8

    可以看出叶子结点左右孩子均为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("
    ");
    }
    

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

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

  • 相关阅读:
    ModbusRTU模式和结束符(转)
    modbus字符串的结束符介绍
    IAR平台移植TI OSAL到STC8A8K64S4A12单片机中
    实时系统概念
    单片机的存储区范例
    如何实现返回上一个页面,就像点击浏览器的返回按钮一般
    spring项目中的定时任务实现和问题解决
    context-param与init-param的区别与作用
    Chapter 1 First Sight——16
    一个好用简单的布局空间EasyUI
  • 原文地址:https://www.cnblogs.com/kafm/p/12721839.html
Copyright © 2020-2023  润新知