文章参考自:程序员小灰:漫画:“哈夫曼编码” 是什么鬼?
哈夫曼编码是一种高效的编码方式,在信息存储和传输过程中,用于对信息进行压缩。
计算机系统是如何存储信息的呢?
计算机不是人,它不认识中文和英文,更不认识图片和视频,它唯一“认识”的就是0(低电平)和1(高电平)。
因此,我们在计算机上看到的一切文字、图像、音频、视频,底层都是用二进制来存储和传输的。
从狭义上来讲,把人类能看懂的各种信息,转换成计算机能够识别的二进制形式,被称为编码。
编码的方式可以有很多种,我们大家最熟悉的编码方式就属ASCII码了。
在ASCII码当中,把每一个字符表示成特定的8位二进制数,比如:
显然,ASCII码是一种等长编码,也就是任何字符的编码长度都相等。
等长编码的优点很明显,因为每个字符对应的二进制编码长度相等,所以很容易设计,也很方便读写。
但计算机的存储空间以及网络传输的带宽是有限的,等长编码最大的缺点就是编码结果太长,会占用过多资源。
举个例子:
假如一段信息当中,只有A,B,C,D,E,F这6个字符,如果使用等长编码,我们可以把每一个字符都设计成长度为3的二进制编码:
如此一来,给定一段信息 “ABEFCDAED”,就可以编码成二进制的 “000 001 100 101 010 011 000 100 011”,编码总长度是27。
但是,这样的编码方式是最优的设计吗?如果我们让不同的字符对应不同长度的编码,结果会怎样呢?比如:
如此一来,给定的信息 “ABEFCDAED”,就可以编码成二进制的 “0 00 10 11 01 1 0 10 1”,编码的总长度只有14。
但是,上面这样的设计会带来歧义,A的编码是0,B的编码是00,那么000即是AB也是AAA。
所以不定长编码是不能随意设计的,如果一个字符串的编码恰好是另一个字符串编码的前缀,就会产生上面的歧义。
这个时候就要用到哈夫曼编码了哈夫曼编码是一种不定长的编码方式。
哈夫曼编码(Huffman Coding),同样是由麻省理工学院的哈夫曼博所发明,这种编码方式实现了两个重要目标:
1.任何一个字符编码,都不是其他字符编码的前缀。
2.信息编码的总长度最小。
哈夫曼编码并非一套固定的编码,而是根据给定信息中各个字符出现的频次,动态生成最优的编码。
哈夫曼编码的生成过程就用到了我们上次说的哈夫曼树。
哈夫曼编码的生成过程是什么样子呢?让我们看看下面的例子:
假如一段信息里只有A,B,C,D,E,F这6个字符,他们出现的次数依次是2次,3次,7次,9次,18次,25次,如何设计对应的编码呢?
我们不妨把这6个字符当做6个叶子结点,把字符出现次数当做结点的权重,以此来生成一颗哈夫曼树:
这样做的意义是什么呢?
哈夫曼树的每一个结点包括左、右两个分支,二进制的每一位有0、1两种状态,我们可以把这两者对应起来,结点的左分支当做0,结点的右分支当做1,会产生什么样的结果?
这样一来,从哈夫曼树的根结点到每一个叶子结点的路径,都可以等价为一段二进制编码:
上述过程借助哈夫曼树所生成的二进制编码,就是哈夫曼编码。
现在,我们面临两个关键的问题:
首先,这样生成的编码有没有前缀问题带来的歧义呢?答案是没有歧义。
因为每一个字符对应的都是哈夫曼树的叶子结点,从根结点到这些叶子结点的路径并没有包含关系,最终得到的二进制编码自然也不会是彼此的前缀。
其次,这样生成的编码能保证总长度最小吗?答案是可以保证。
哈夫曼树的重要特性,就是所有叶子结点的(权重 X 路径长度)之和最小。
放在信息编码的场景下,叶子结点的权重对应字符出现的频次,结点的路径长度对应字符的编码长度。
所有字符的(频次 X 编码长度)之和最小,自然就说明总的编码长度最小。
总结起来,哈夫曼树就是利用特殊的二叉树来生成二进制编码。
在这个例子中,哈夫曼编码的总长度是141,比定长编码短了百分之二十多。
哈夫曼编码(HaffmanCoding)代码实现:
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Algorithm
{
public class TreeNode : IComparable
{
public int weight; // 权重
public string code; // 节点对应的二进制编码
public TreeNode lChild;
public TreeNode rChild;
public TreeNode(int weight)
{
this.weight = weight;
}
public TreeNode(int weight, TreeNode lChild, TreeNode rChild)
{
this.weight = weight;
this.lChild = lChild;
this.rChild = rChild;
}
// 实现比较接口,用于排序
public int CompareTo(object obj)
{
int result = 0;
TreeNode tmp = obj as TreeNode;
if (tmp.weight > this.weight)
{
result = 1;
}
else if (tmp.weight < this.weight)
{
result = -1;
}
return result;
}
}
// 哈夫曼树
public class HuffmanCodingTree
{
public TreeNode root;
private TreeNode[] nodes;
/// <summary>
/// 构建哈夫曼树
/// </summary>
/// <param name="weights">权重数组</param>
public void CreateHuffmanCodingTree(int[] weights)
{
// 优先队列,用于辅助构建哈夫曼树
Queue<TreeNode> nodeQueue = new Queue<TreeNode>();
nodes = new TreeNode[weights.Length];
// 构建森林,初始化nodes数组
for (int i = 0; i < weights.Length; i++)
{
nodes[i] = new TreeNode(weights[i]);
nodeQueue.Enqueue(nodes[i]);
}
// 主循环,当节点队列只剩下一个节点结束
while (nodeQueue.Count > 1)
{
// 从节点队列选择权值最小的两个节点
TreeNode left = nodeQueue.Dequeue();
TreeNode right = nodeQueue.Dequeue();
// 创建新节点作为两节点的父节点
TreeNode parent = new TreeNode(left.weight + right.weight, left, right);
nodeQueue.Enqueue(parent);
}
root = nodeQueue.Dequeue();
}
// 输入下表,输出对应的哈夫曼编码
public string ConvertHuffmanCode(int index)
{
return nodes[index].code;
}
// 用递归的方式,填充各个节点
public void Encode(TreeNode node, string code)
{
if (node == null)
{
return;
}
node.code = code;
Encode(node.lChild, node.code + "0");
Encode(node.rChild, node.code + "1");
}
public void Output(TreeNode head)
{
if (head == null)
{
return;
}
Debug.Log(head.weight);
Output(head.lChild);
Output(head.rChild);
}
}
}
这段代码中,Node类增加了一个新字段code,用于记录结点所对应的二进制编码。
当哈夫曼树构建之后,就可以通过递归的方式,从根结点向下,填充每一个结点的code值。