算法与数据结构(c语言)——赫夫曼树&赫夫曼编码

赫夫曼树:最优二叉树,带权路径长度最短的树,也称为赫夫曼树。

给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为赫夫曼树(Huffman Tree)。赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

先说几个基本的名词:

  • 路径:从树的一个结点到另一个结点之间的分支构成两个结点之间的路径
  • 路径长度:路径上的分支数目
  • 树的路径长度:从树根到每个结点的路径长度之和
  • 树的带权路径长度:树中所有叶子节点的带权路径长度之和
  • 赫夫曼树:带权路径长度WPL(Weighted Path Length)最小的二叉树。

构造一颗赫夫曼树

假设有n个权值,则构造出的赫夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则赫夫曼树的构造规则为:

(1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);

(2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;

(3)从森林中删除选取的两棵树,并将新树加入森林;

(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的赫夫曼树

赫夫曼编码

翠花说先上几个基本的名词:

  • 定长编码:像是ASCII编码就是定长的,都是8位表示一个字符。
  • 变长编码:单个字符的编码的长度不一致,可以根据整体出现的频率来进行调节。
  • 前缀码:给定一个序列的集合,若不存在一个序列是另一个序列的前缀,则该序列集合称为前缀码(就是没有任何码字是其他码字的前缀)。

C代码实现

定义的头文件:huffman.h

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


//错误
#define	ERROR		0
//成功
#define SUCCESS		1

/* 状态码识别类型 */
typedef int Status;

#define MAXSIZE 1024
typedef char Element;

typedef struct {
    // 节点值
    Element data;
    // 权重
    unsigned int weight;
    // 父结点,左孩子,右孩子
    unsigned int parent,lchild,rchild;
}HTNode;
typedef HTNode* HuffmanTree;
typedef Element* HCNode;

/*
用以存储根据huffman树来生成每个字符的huffman编码值
例如:
    data: , code: 10
    data:a, code: 111
    data:h, code: 110
    data:i, code: 001
    data:o, code: 0101
    data:s, code: 0100
    data:x, code: 0111
    data:y, code: 0110
    data:z, code: 000
*/
typedef HCNode* HuffmanCode;
/*
对于有n个叶子节点的赫夫曼树,共有2n-1个节点。
由二叉树性质:对于任何一个二叉树T,如果其终端节点数为n_0,度为2的节点数为n_2,则n_0 = n_2 +1。
赫夫曼树是个严格的二叉树,不存在度为1的节点。
so,赫夫曼树的节点 = 度为2的节点 + 叶子节点,即2n-1
*/

HuffmanTree buildTree(char *str);
void Create_Huffmantree(int n, int m, HuffmanTree HT);
Status HuffmanCoding(HuffmanTree HT, HuffmanCode *HC);
Status EnCoding(HuffmanCode HC, char *str, char **code);
Status DeCoding(HuffmanTree HT, char *code, char **translation);

 huffman.c

#include "huffman.h"

// 记录有效的字符个数
int valid_char_num = 0;
// 记录有效字符的指针
char *valid_char_pointer;

HuffmanTree buildTree(char *str) {
    // ASCII编码就只能表示256个字符
    valid_char_pointer = (char *)malloc(256 * sizeof(char));
    // 用来记录每个字符出现的次数
    int char_count_arr[256] = {0};

	//并计算每个符号出现的次数
	for(int i=0; str[i]!='\0'; i++) {
        // 以字符的ascill码作为索引,在数组中的对应位置记录次数。
        char_count_arr[(unsigned char) str[i]]++;
    }

    // 循环存放有效字符
    for(int i = 0; i < 256; i++) {
        if(char_count_arr[i] != 0) {
            valid_char_pointer[valid_char_num++] = (char)i;
        }
    }
    // 加上结束标志。
    valid_char_pointer[valid_char_num] = '\0';

    // 总共需要m个节点来存储整个树
    int m = valid_char_num * 2 - 1;
    // 申请一连串的存储空间
    HTNode *ht_nodes = (HTNode *)calloc(m, sizeof(HTNode));
    if(!ht_nodes) {
        exit(1);
    }

    // 节点进行初始化
    for(int i = 0; i < m; i++) {
        if(i < valid_char_num) {
            ht_nodes[i].data = valid_char_pointer[i];
            ht_nodes[i].weight = char_count_arr[(unsigned char) valid_char_pointer[i]];
        } else {
            ht_nodes[i].data = '\0';
            ht_nodes[i].weight = 0;
        }
        ht_nodes[i].lchild = ht_nodes[i].rchild = ht_nodes[i].parent = -1;
    }

    HuffmanTree ht = ht_nodes;
    Create_Huffmantree(valid_char_num, m, ht);
    return ht;
}

/*
寻找权值最小的两个数,然后合并放入n ~ m-1个结点之中。对叶子节点进行操作合并。
n是叶子结点的个数,m=2n-1,从下标0开始存储
*/
void Create_Huffmantree(int n, int m, HuffmanTree HT) {
	for (int i = n; i < m; i++) {
	    // 32767是整型在16位计算机中的最大值,w1记录最小权值,w2存储第二小的权值
		int w1 =32767, w2 = 32767;
		// p1记录最小权值下标,p2存储第二小的权值下标
		int p1 = -1, p2 = -1;

		// 必须是小于i,若等于i,那么最小的权值就是当前i节点的权值:0。
		// 同样不能是n,因为合并后的节点同样要参与比较。
		for (int j = 0; j < i; j++) {
            // 若为叶子节点
			if (HT[j].parent == -1) {
				if (HT[j].weight <= w1) {
					w2 = w1;
					w1 = HT[j].weight;
					p2 = p1;
					p1 = j;
				} else if (HT[j].weight < w2) {
					w2 = HT[j].weight;
					p2 = j;
				}
			}
		}
		if (p1 > -1 && p2 > -1) {
		    // 将最小的两个节点权值相加赋给合并的节点
			HT[i].weight = w1 + w2;
			// 左孩子为最小的,右孩子为第二小的
			HT[i].lchild = p1;
			HT[i].rchild = p2;
			HT[p1].parent = i;
			HT[p2].parent = i;
		}

        printf("\n------huffman tree Construction process------%d-th time------\n",i - valid_char_num + 1);
        for(int i = 0; i < valid_char_num * 2 - 1; i++) {
            printf("data:%c, weight:%d, parrent:%d, lchild: %d, rchild:%d\n", HT[i].data, HT[i].weight, HT[i].parent, HT[i].lchild, HT[i].rchild);
        }
        printf("\n");

	}
}

/* 从叶子节点开始逆向进行赫夫曼编码*/
/*HC用来储存huffman编码值*/
Status HuffmanCoding(HuffmanTree HT, HuffmanCode *HC) {
    int n = valid_char_num;

    if(!(*HC = (HuffmanCode)malloc(n * sizeof(HCNode)))) {
        return ERROR;
    }

    /* 声明一个动态字符数组code,用来临时存储赫夫曼编码,
    n个叶子节点的huffman tree,深度最多就为n(没验证过,只为理解),
    那一个字符的编码长度最大也就n-1个。所以有n个存储空间就够了,最后一个存放\0。
    */
    HCNode code;
    if(!(code = (HCNode)malloc(n * sizeof(char)))) {
        return ERROR;
    }
    /* 加上一个字符串结束符号*/
    code[n - 1] = '\0';

    for(int i = 0; i < n; i++) {
        // 记录当前元素的位置
        int current = i;
        int father = HT[i].parent;
        // 从后向前记录的索引位置
        int end = n - 1;

        // 这里的huffman tree根节点的parent域为-1
        while(father != -1) {
            // 若当前节点等于当前节点的父节点的左孩子,编码为0,右孩子编码为1
            if(current == HT[father].lchild) {
                code[--end] = '0';
            } else {
                code[--end] = '1';
            }

            // 父节点为当前元素节点。
            current = father;
            // 父节点指向父节点的父节点
            father = HT[father].parent;
        }

        /* HC[i]用于最终存储huffman码,是char类型的数组,有n个char类型的数据*/
        if(!(*(*HC + i) = (HCNode)malloc((n - 1 - end) * sizeof(char)))) {
            return ERROR;
        }

        /* 从临时空间中复制到HC[i]中*/
        strcpy(*(*HC + i), code + end);
    }
    /*释放临时存储空间*/
    free(code);
    code = NULL;

    printf("\n===============huffman code==================\n");
    for(int i = 0; i < n; i++) {
        printf("data:%c, code: %s\n", HT[i].data, *(*HC + i));
    }

    return SUCCESS;
}

Status EnCoding(HuffmanCode HC, char *str, char **code) {
    printf("\n===============Encode==================\n");
    if(!(*code = (char *)malloc(MAXSIZE * sizeof(char)))) {
        return ERROR;
    }

    **code = '\0';

    int len = strlen(str);
    for(int i = 0; i < len; i++){
        for(int j = 0; j < valid_char_num; j++) {
            if(*(str + i) == valid_char_pointer[j]) {
                //printf("%s", *(HC + i));
                strcpy(*code + strlen(*code), *(HC + j));
                break;
            }
        }
    }

    return SUCCESS;
}

Status DeCoding(HuffmanTree HT, char *code, char **translation) {
    printf("\n===============Decode==================\n");
    if(!(*translation = (char *)malloc(sizeof(char)))) {
        return ERROR;
    }

    // 编码的长度
    int n = strlen(code);
    int j = 0;

    for(int i = 0; i < n; ) {
        // 每次都从根节点开始
        HTNode t = HT[valid_char_num * 2 - 2];
        // 只要存在孩子节点就判断第i个编码是0还是1
        while((int)t.lchild > -1 || (int)t.rchild > -1) {
            if(code[i++] == '0') {
                // 编码为0将t的左孩子赋值给t
                t = HT[t.lchild];
            } else {
                // 编码为1将t的右孩子赋值给t
                t = HT[t.rchild];
            }
        }
        *(*translation + j++) = t.data;
    }

    // 加上一个字符串结束标志
    *(*translation + j) = '\0';

    return SUCCESS;
}

main:

#include "huffman.h"

int main(int argc, char *argv[]) {
    //char *str = "aabc  ebac fv afvvvv";

    char *str = "shi xiao zha zha ya";
    HuffmanTree ht = buildTree(str);
    HuffmanCode hc;
    HuffmanCoding(ht, &hc);

    char *code = NULL;
    EnCoding(hc, str, &code);
    printf("%s 得到的huffman编码为:%s\n", str,code);

    char *translation;
    char *cd = "010011000110011100111101011000011011110000110111100110111";
    DeCoding(ht, cd, &translation);
    printf("huffman编码 %s \n译文:%s\n", cd, translation);

    return 0;
}

运行结果:

代码有些是东拼西凑从别处摘的,参考的多篇博客,某些出处都忘了,所有就都不贴了,抱歉。

错误不足之处还请指正。3Q! 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值