漫画:“哈夫曼编码” 是什么鬼?

简介: 哈夫曼编码(Huffman Coding),同样是由麻省理工学院的哈夫曼博所发明,这种编码方式实现了两个重要目标:1.任何一个字符编码,都不是其他字符编码的前缀。2.信息编码的总长度最小。

在上一期,我们介绍了一种特殊的数据结构 “哈夫曼树”,也被称为最优二叉树。没看过的小伙伴可以点击下方链接:漫画:什么是“哈夫曼树”

那么,这种数据结构究竟有什么用呢?我们今天就来揭晓答案。

640.png640.png640.png640.png

 

 

计算机系统是如何存储信息的呢?




计算机不是人,它不认识中文和英文,更不认识图片和视频,它唯一“认识”的就是0(低电平)和1(高电平)。

因此,我们在计算机上看到的一切文字、图像、音频、视频,底层都是用二进制来存储和传输的。

 640.png

从狭义上来讲,把人类能看懂的各种信息,转换成计算机能够识别的二进制形式,被称为编码。

编码的方式可以有很多种,我们大家最熟悉的编码方式就属ASCII码了。

ASCII码当中,把每一个字符表示成特定的8位二进制数,比如:

 640.png

显然,ASCII码是一种等长编码,也就是任何字符的编码长度都相等。

640.png640.png640.png640.png

 



为什么这么说呢?让我们来看一个例子:

假如一段信息当中,只有ABCDEF6个字符,如果使用等长编码,我们可以把每一个字符都设计成长度为3的二进制编码:

640.png



如此一来,给定一段信息 “ABEFCDAED”,就可以编码成二进制的 “000 001 100 101 010 011 000 100 011”,编码总长度是27

640.png

 

但是,这样的编码方式是最优的设计吗?如果我们让不同的字符对应不同长度的编码,结果会怎样呢?比如:

640.png

如此一来,给定的信息 “ABEFCDAED”,就可以编码成二进制的 “0 00 10 11 01 1 0 10 1”,编码的总长度只有14

640.png640.png640.png640.png

 

哈夫曼编码(Huffman Coding),同样是由麻省理工学院的哈夫曼博所发明,这种编码方式实现了两个重要目标:

1.任何一个字符编码,都不是其他字符编码的前缀。

2.信息编码的总长度最小。

640.png

640.png640.png

 

 

哈夫曼编码的生成过程是什么样子呢?


让我们看看下面的例子:

假如一段信息里只有ABCDEF6个字符,他们出现的次数依次是2次,3次,7次,9次,18次,25次,如何设计对应的编码呢?

我们不妨把这6个字符当做6个叶子结点,把字符出现次数当做结点的权重,以此来生成一颗哈夫曼树:

640.png


这样做的意义是什么呢?

哈夫曼树的每一个结点包括左、右两个分支,二进制的每一位有01两种状态,我们可以把这两者对应起来,结点的左分支当做0,结点的右分支当做1,会产生什么样的结果?

640.png

这样一来,从哈夫曼树的根结点到每一个叶子结点的路径,都可以等价为一段二进制编码:

640.png

上述过程借助哈夫曼树所生成的二进制编码,就是哈夫曼编码。

现在,我们面临两个关键的问题:

首先,这样生成的编码有没有前缀问题带来的歧义呢?答案是没有歧义。

因为每一个字符对应的都是哈夫曼树的叶子结点,从根结点到这些叶子结点的路径并没有包含关系,最终得到的二进制编码自然也不会是彼此的前缀。

其次,这样生成的编码能保证总长度最小吗?答案是可以保证。

哈夫曼树的重要特性,就是所有叶子结点的(权重 X 路径长度)之和最小。

640.png640.png640.png640.png640.png640.png640.png640.png

private
Node
 root
;
private
Node
[]
 nodes
;
//构建哈夫曼树
public
void
 createHuffmanTree
(
int
[]
 weights
)
{
//优先队列,用于辅助构建哈夫曼树
Queue
<
Node
>
 nodeQueue 
=
new
PriorityQueue
<>();
    nodes 
=
new
Node
[
weights
.
length
];
//构建森林,初始化nodes数组
for
(
int
 i
=
0
;
 i
<
weights
.
length
;
 i
++){
        nodes
[
i
]
=
new
Node
(
weights
[
i
]);
        nodeQueue
.
add
(
nodes
[
i
]);
}
//主循环,当结点队列只剩一个结点时结束
while
(
nodeQueue
.
size
()
>
1
)
{
//从结点队列选择权值最小的两个结点
Node
 left 
=
 nodeQueue
.
poll
();
Node
 right 
=
 nodeQueue
.
poll
();
//创建新结点作为两结点的父节点
Node
 parent 
=
new
Node
(
left
.
weight 
+
 right
.
weight
,
 left
,
 right
);
        nodeQueue
.
add
(
parent
);
}
    root 
=
 nodeQueue
.
poll
();
}
//输入字符下表,输出对应的哈夫曼编码
public
String
 convertHuffmanCode
(
int
 index
)
{
return
 nodes
[
index
].
code
;
}
//用递归的方式,填充各个结点的二进制编码
public
void
 encode
(
Node
 node
,
String
 code
){
if
(
node 
==
null
){
return
;
}
    node
.
code 
=
 code
;
    encode
(
node
.
lChild
,
 node
.
code
+
"0"
);
    encode
(
node
.
rChild
,
 node
.
code
+
"1"
);
}
public
static
class
Node
implements
Comparable
<
Node
>{
int
 weight
;
//结点对应的二进制编码
String
 code
;
Node
 lChild
;
Node
 rChild
;
public
Node
(
int
 weight
)
{
this
.
weight 
=
 weight
;
}
public
Node
(
int
 weight
,
Node
 lChild
,
Node
 rChild
)
{
this
.
weight 
=
 weight
;
this
.
lChild 
=
 lChild
;
this
.
rChild 
=
 rChild
;
}
@Override
public
int
 compareTo
(
Node
 o
)
{
return
new
Integer
(
this
.
weight
).
compareTo
(
new
Integer
(
o
.
weight
));
}
}
public
static
void
 main
(
String
[]
 args
)
{
char
[]
 chars 
=
{
'A'
,
'B'
,
'C'
,
'D'
,
'E'
,
'F'
};
int
[]
 weights 
=
{
2
,
3
,
7
,
9
,
18
,
25
};
HuffmanCode
 huffmanCode 
=
new
HuffmanCode
();
    huffmanCode
.
createHuffmanTree
(
weights
);
    huffmanCode
.
encode
(
huffmanCode
.
root
,
""
);
for
(
int
 i
=
0
;
 i
<
chars
.
length
;
 i
++){
System
.
out
.
println
(
chars
[
i
]
+
":"
+
 huffmanCode
.
convertHuffmanCode
(
i
));
}
}


放在信息编码的场景下,叶子结点的权重对应字符出现的频次,结点的路径长度对应字符的编码长度。

所有字符的(频次 X 编码长度)之和最小,自然就说明总的编码长度最小。

这段代码中,Node类增加了一个新字段code,用于记录结点所对应的二进制编码。

当哈夫曼树构建之后,就可以通过递归的方式,从根结点向下,填充每一个结点的code值。

640.png

相关文章
|
人工智能 BI
【寒假每日一题】AcWing 4699. 如此编码
目录 一、题目 1、原题链接 2、题目描述 二、解题报告 1、思路分析 2、时间复杂度 3、代码详解
53 0
|
存储 算法
漫画:什么是 “哈夫曼树” ?
哈夫曼树是由麻省理工学院的哈夫曼博士于1952年发明,这到底是一颗什么样的树呢? 刚才我们学习了树的带权路径长度(WPL),而哈夫曼树(Huffman Tree)是在叶子结点和权重确定的情况下,带权路径长度最小的二叉树,也被称为最优二叉树。 举个例子,给定权重分别为1,3,4,6,8的叶子结点,我们应当构建怎样的二叉树,才能保证其带权路径长度最小? 原则上,我们应该让权重小的叶子结点远离树根,权重大的叶子结点靠近树根。
346 0
漫画:什么是 “哈夫曼树” ?
|
算法
漫画:什么是二分查找?(修订版)
如果我们把场景转换成最初的面试问题:在包含1000个整型元素的有序数组中查找某个特定整数,又该如何去做呢?
134 0
漫画:什么是二分查找?(修订版)
|
算法 搜索推荐
漫画:什么是基数排序?
数组每一个下标位置的值,代表了数列中对应整数出现的次数。 有了这个“统计结果”,排序就很简单了。直接遍历数组,输出数组元素的下标值,元素的值是几,就输出几次: 0,1,1,2,3,3,3,4,4,5,5,6,7,7,8,9,9,9,9,10 显然,这个输出的数列已经是有序的了。 这就是计数排序的朴素版本。
155 0
漫画:什么是基数排序?
|
算法 搜索推荐 Java
漫画:什么是桶排序?
让我们先来回顾一下计数排序: 计数排序需要根据原始数列的取值范围,创建一个统计数组,用来统计原始数列中每一个可能的整数值所出现的次数。
178 0
漫画:什么是桶排序?
|
存储
漫画:什么是计数排序?
如何给无序的随机整数排序呢? 非常简单,让我们遍历这个无序的随机数列,每一个整数按照其值对号入座,对应数组下标的元素进行加1操作。 比如第一个整数是9,那么数组下标为9的元素加1
130 0
漫画:什么是计数排序?
|
算法
漫画:什么是八皇后问题?
国际象棋中的皇后,可以横向、纵向、斜向移动。如何在一个8X8的棋盘上放置8个皇后,使得任意两个皇后都不在同一条横线、竖线、斜线方向上?让我们来举个栗子,下图的绿色格子是一个皇后在棋盘上的“封锁范围”,其他皇后不得放置在这些格子: 下图的绿色格子是两个皇后在棋盘上的“封锁范围”,其他皇后不得放置在这些格子: 那么,如何遵循规则,同时放置这8个皇后呢?让我们来看看小灰的回答。
283 0
漫画:什么是八皇后问题?
|
算法
漫画:什么是鸡尾酒排序?(修订版)
昨天小灰发布的漫画中存在一些勘误,所以今天重新发布一篇修订版,修正了代码当中的一些细节问题。在上一篇漫画中,小灰介绍了冒泡排序的思路和几种变化:漫画:什么是冒泡排序?那么,鸡尾酒排序又是何方神圣呢?我们这一期将会详细讲述。
149 0
漫画:什么是鸡尾酒排序?(修订版)
漫画:什么是插入排序?
人们如何进行扑克牌的排序呢? 举个例子,比如我手中有红桃6,7,9,10这四张牌,已经处于升序排列:这时候,我又抓到了一张红桃8,如何让手中的五张牌重新变成升序呢?用冒泡排序,选择排序,亦或是快速排序?
174 0
漫画:什么是插入排序?