数据结构和算法——Huffman树和Huffman编码

Huffman树是一种特殊结构的二叉树,由Huffman树设计的二进制前缀编码,也称为Huffman编码在通信领域有着广泛的应用。在word2vec模型中,在构建层次Softmax的过程中,也使用到了Huffman树的知识。

在通信中,需要将传输的文字转换成二进制的字符串,假设传输的报文为:“AFTERDATAEARAREARTAREA”,现在需要对该报文进行编码。

一、Huffman树的基本概念

在二叉树中有一些基本的概念,对于如下所示的二叉树:

这里写图片描述

  • 路径

路径是指在一棵树中,从一个节点到另一个节点之间的分支构成的通路,如从节点8到节点1的路径如下图所示:

这里写图片描述

  • 路径长度

路径长度指的是路径上分支的数目,在上图中,路径长度为2。

  • 节点的权

节点的权指的是为树中的每一个节点赋予的一个非负的值,如上图中每一个节点中的值。

  • 节点的带权路径长度

节点的带权路径长度指的是从根节点到该节点之间的路径长度与该节点权的乘积:如对于1节点的带权路径长度为:2。

  • 树的带权路径长度

树的带权路径长度指的是所有叶子节点的带权路径长度之和。

有了如上的概念,对于Huffman树,其定义为:

给定 n 权值作为n个叶子节点,构造一棵二叉树,若这棵二叉树的带权路径长度达到最小,则称这样的二叉树为最优二叉树,也称为Huffman树。

由以上的定义可以知道,Huffman树是带权路径长度最小的二叉树,对于上面的二叉树,其构造完成的Huffman树为:

这里写图片描述

二、Huffman树的构建

由上述的Huffman树可知:节点的权越小,其离树的根节点越远。那么应该如何构建Huffman树呢?以上述报文为例,首先需要统计出每个字符出现的次数作为节点的权:

这里写图片描述

接下来构建Huffman树:

  • 重复以下的步骤:
    • 按照权值对每一个节点排序:D-F-T-E-R-A
    • 选择权值最小的两个节点,此处为D和F生成新的节点,节点的权重为这两个节点的权重之和,为2
  • 直到只剩最后的根节点

按照上述的步骤,该报文的Huffman树的生成过程为:

这里写图片描述

这里写图片描述

对于树中节点的结构为:

#define LEN 512
struct huffman_node{
        char c;
        int weight;
        char huffman_code[LEN];
        huffman_node * left;
        huffman_node * right;
};

对于Huffman树的构建过程为:

int huffman_tree_create(huffman_node *&root, map<char, int> &word){
        char line[MAX_LINE];
        vector<huffman_node *> huffman_tree_node;

        map<char, int>::iterator it_t;
        for (it_t = word.begin(); it_t != word.end(); it_t++){
                // 为每一个节点申请空间
                huffman_node *node = (huffman_node *)malloc(sizeof(huffman_node));
                node->c = it_t->first;
                node->weight = it_t->second;
                node->left = NULL;
                node->right = NULL;
                huffman_tree_node.push_back(node);
        }


        // 开始从叶节点开始构建Huffman树
        while (huffman_tree_node.size() > 0){
                // 按照weight升序排序
                sort(huffman_tree_node.begin(), huffman_tree_node.end(), sort_by_weight);
                // 取出前两个节点
                if (huffman_tree_node.size() == 1){// 只有一个根结点
                        root = huffman_tree_node[0];
                        huffman_tree_node.erase(huffman_tree_node.begin());
                }else{
                        // 取出前两个
                        huffman_node *node_1 = huffman_tree_node[0];
                        huffman_node *node_2 = huffman_tree_node[1];
                        // 删除
                        huffman_tree_node.erase(huffman_tree_node.begin());
                        huffman_tree_node.erase(huffman_tree_node.begin());
                        // 生成新的节点
                        huffman_node *node = (huffman_node *)malloc(sizeof(huffman_node));
                        node->weight = node_1->weight + node_2->weight;
                        (node_1->weight < node_2->weight)?(node->left=node_1,node->right=node_2):(node->left=node_2,node->right=node_1);
                        huffman_tree_node.push_back(node);
                }
        }

        return 0;
}

其中,map结构的word为每一个字符出现的频率,是从文件中解析出来的,解析的代码为:

int read_file(FILE *fn, map<char, int> &word){
        if (fn == NULL) return 1;
        char line[MAX_LINE];
        while (fgets(line, 1024, fn)){
                fprintf(stderr, "%s\n", line);
                //解析,统计词频
                char *p = line;
                while (*p != '\0' && *p != '\n'){
                        map<char, int>::iterator it = word.find(*p);
                        if (it == word.end()){// 不存在,插入
                                word.insert(make_pair(*p, 1));
                        }else{
                                it->second ++;
                        }
                        p ++;
                }
        }
        return 0;
}

当构建好Huffman树后,我们分别利用先序遍历和中序遍历去遍历Huffman树,先序遍历的代码为:

void print_huffman_pre(huffman_node *node){
        if (node != NULL){
                fprintf(stderr, "%c\t%d\n", node->c, node->weight);
                print_huffman_pre(node->left);
                print_huffman_pre(node->right);
        }
}

中序遍历的代码为:

void print_huffman_in(huffman_node *node){
        if (node != NULL){
                print_huffman_in(node->left);
                fprintf(stderr, "%c\t%d\n", node->c, node->weight);
                print_huffman_in(node->right);
        }
}

得到的结构与上图中的结构一致。

三、由Huffman树生成Huffman编码

有了上述的Huffman树的结构,现在我们需要利用Huffman树对每一个字符编码,该编码又称为Huffman编码,Huffman编码是一种前缀编码,即一个字符的编码不是另一个字符编码的前缀。在这里约定:

  • 将权值小的最为左节点,权值大的作为右节点
  • 左孩子编码为0,右孩子编码为1

因此,上述的编码形式如下图所示:

这里写图片描述

从上图中,E节点的编码为:00,同理,D节点的编码为1001

Huffman编码的实现过程为:

int get_huffman_code(huffman_node *&node){
        if (node == NULL) return 1;
        // 利用层次遍历,构造每一个节点
        huffman_node *p = node;
        queue<huffman_node *> q;
        q.push(p);
        while(q.size() > 0){
                p = q.front();
                q.pop();
                if (p->left != NULL){
                        q.push(p->left);
                        strcpy((p->left)->huffman_code, p->huffman_code);
                        char *ptr = (p->left)->huffman_code;
                        while (*ptr != '\0'){
                                ptr ++;
                        }
                        *ptr = '0';
                }
                if (p->right != NULL){
                        q.push(p->right);
                        strcpy((p->right)->huffman_code, p->huffman_code);
                        char *ptr = (p->right)->huffman_code;
                        while (*ptr != '\0'){
                                ptr ++;
                        }
                        *ptr = '1';
                }
        }


        return 0;
}

利用上述的代码,测试的主函数为:

int main(){
        // 读文件
        FILE *fn = fopen("huffman", "r");
        huffman_node *root = NULL;
        map<char, int> word;
        read_file(fn, word);
        huffman_tree_create(root, word);
        fclose(fn);
        fprintf(stderr, "pre-order:\n");
        print_huffman_pre(root);
        fprintf(stderr, "in-order:\n");
        print_huffman_in(root);

        get_huffman_code(root);
        fprintf(stderr, "the final result:\n");
        print_leaf(root);
        destory_huffman_tree(root);
        return 0;
}

print_leaf函数用于打印出每个叶节点的Huffman编码,其具体实现为:

void print_leaf(huffman_node *node){
        if (node != NULL){
                print_leaf(node->left);
                if (node->left == NULL && node->right == NULL) fprintf(stderr, "%c\t%s\n", node->c, node->huffman_code);
                print_leaf(node->right);
        }
}

destory_huffman_tree函数用于销毁Huffman树,其具体实现为:

void destory_huffman_tree(huffman_node *node){
        if (node != NULL){
                destory_huffman_tree(node->left);
                destory_huffman_tree(node->right);
                free(node);
                node = NULL;
        }
}

其最终的结果为:

这里写图片描述

参考文献

  • 《大话数据结构》
  • 《数据结构》(C语言版)
### Huffman 编码算法原理 Huffman编码是一种基于字符出现频率的数据压缩技术。该方法的核心在于构建一颗特殊的二叉结构—即所谓的Huffman,在此上,每个叶节点代表输入数据集中的一种符号,并附带有一个权重值表示该符号的发生概率或频次[^1]。 对于每一个待编码的对象而言,如果它在整个序列里频繁显现,则会被赋予更短的位串作为其对应的编码形式;相反地,那些较少见的对象将会被分配相对较长一些的比特组合来进行表达。这种策略有效地减少了整个消息所需传输的信息量,实现了高效的数据压缩效果[^3]。 另外值得注意的是,为了防止解码时可能出现歧义的情况发生,所设计出来的各个代码之间不会存在任何一个是他者的前缀关系,这一特性也被称为“即时可解析性”,确保了解压过程能够顺利进行而不受干扰[^2]。 ### 实现方式概述 以下是关于如何创建并运用上述提到的Huffman编码方案的一个简化版描述: #### 构建优先队列(最小堆) 首先统计给定文本中各字符的数量并将它们存入到一个列表当中,随后依据这些数值初始化一个小根堆(Min Heap),其中每项元素都是形如`[frequency, character]`这样的数组片段。 #### 创建霍夫曼 接着不断地从未处理完毕的小顶堆里面取出两个具有最低频率计数的项目AB,然后合成一个新的内部结点C,它的子节点分别是之前选出的那一对成员AB,同时更新这个新加入者C自身的累计次数等于两者之。重复执行以上操作直到最后只剩下唯一的超级父级为止,此时就得到了完整的霍夫曼形态。 #### 生成编码映射表 遍历整棵已建立好的霍夫曼,沿着路径从根部向叶子方向移动的过程中记录下沿途经过分支的方向信息(左转记作'0',右转视为'1'),最终到达末端位置处便可以获得相应于特定字符的一系列连续数字组成的字符串,也就是所说的霍夫曼编码。 ```python import heapq from collections import defaultdict, Counter def huffman_encoding(data): frequency = dict(Counter(data)) heap = [[weight, [symbol, ""]] for symbol, weight in frequency.items()] heapq.heapify(heap) while len(heap) > 1: lo = heapq.heappop(heap) hi = heapq.heappop(heap) for pair in lo[1:]: pair[-1] = '0' + pair[-1] for pair in hi[1:]: pair[-1] = '1' + pair[-1] heapq.heappush(heap, [lo[0]+hi[0]] + lo[1:] + hi[1:]) return sorted(heapq.heappop(heap)[1:], key=lambda p: (len(p[-1]),p)) data = "this is an example of a huffman tree" huff_code = huffman_encoding(data) print("Symbol".ljust(10) + "Frequency".ljust(10)+ "Huffman Code") for i in huff_code: print(i[0].ljust(10), str(data.count(i[0])).ljust(10),i[1]) ```
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值