什么是哈夫曼树?
哈夫曼树(Huffman Tree):给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度(WPL)达到最小,这样的二叉树也称为最优二叉树
一棵带权二叉树:
这里有几个概念:
-
路径:从一个结点到另一个结点经过的所有结点为两结点间的路径,如上图结点A到结点8的路径为:A-B-C-D-8
-
路径长度:树中一个结点到另一个结点的经过的边数,A到结点8的路径长度为4
-
权值:对于树的结点,可以定义一个有某种意义的数值为权重值
如结点为商品,我们就可以设置权值为商品价格,权值根据需求定义 -
结点的带权路径长度(WPL):对于某个结点,树的根结点到该结点的路径长度和该结点权重的乘积,即是该结点的带权路径长度,结点8的带权路径长度为32
哈夫曼树即是所有叶子结点的带权路径长度之和最小的树
上面的带权二叉树的带权路径长度为:
WPL = 413+48+329+21+26+33+3*7=215
很明显这带权路径长度不是最小的,即上面的带权二叉树不是哈夫曼树
如何实现带权路径长度最小?把权值较大的结点离根较近
带权路径长度最小
具体步骤如下:
- 将树叶子结点存储,并根据权值排序(以从小到大为例),把每个结点看成一个二叉树,权值即为该二叉树根节点的权值
- 取出权值最小的两棵树组成一棵新树,并将这两棵树的根节点权值相加作为新二叉树的根节点权值,把这棵新树加入数组,然后排序
- 后续就是重复上面的步骤,如下一步就是根节点为4的二叉树与根节点为6的二叉树组成新树
- 最终结果:数组仅有一个结点,即哈夫曼树的根结点
Java实现哈夫曼树
树结点类:
- 权值为value属性
- 继承
Comparable<Node>
接口,实现结点比较方法this.value - o.value
,从小到大排序 - 前序遍历方法:先打印当前结点,再循环遍历左子树,再循环遍历右子树
//树的结点,实现排序方法
class Node implements Comparable<Node>{
//结点权值
int value;
//左子结点
Node left;
//右子结点
Node right;
//前序遍历
public void preOrder(){
System.out.print(this.value+" ");
if (this.left != null){
this.left.preOrder();
}
if (this.right != null){
this.right.preOrder();
}
}
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
@Override
public int compareTo(Node o) {
//比较权值
//表示从小到大排序
return this.value - o.value;
}
}
哈夫曼树:用Java完成了上面的步骤
public class HuffmanTree {
public static void main(String[] args) {
int[] arr = {
13,7,8,3,29,6,1};
Node root = createHuffmanTree(arr);
preOrder(root);
}
/**
* 前序遍历哈夫曼树
* @param root 根结点
*/
public static void preOrder(Node root){
if (root != null){
System.out.println("=== 哈夫曼树前序遍历 ===");
root.preOrder();
}
else {
System.out.println("=== 哈夫曼树为空 ===");
}
}
/**
* 创建哈夫曼树
* @param arr 对应的数组
*/
public static Node createHuffmanTree(int[] arr){
//1.遍历arr数组
//2.将arr每个元素构成一个Node
//3.将Node放入List
List<Node> nodes = new ArrayList<>();
for (int value : arr){
nodes.add(new Node(value));
}
//循环处理以下步骤
while (nodes.size() > 1){
//从小到大排序
Collections.sort(nodes);
//取出根结点权值最小的两颗二叉树(结点)
//最小的结点(二叉树)
Node leftNode = nodes.get(0);
//第二下的结点(二叉树)
Node rightNode = nodes.get(1);
//构建新的二叉树,根结点的权值为左右子结点的权值和
Node parent = new Node(leftNode.value + rightNode.value);
parent.left = leftNode;
parent.right = rightNode;
//删除取出的两个结点
nodes.remove(leftNode);
nodes.remove(rightNode);
//将parent加入nodes,构成新的二叉树
nodes.add(parent);
Collections.sort(nodes);
}
//最终nodes仅一个结点,为哈夫曼树的根结点
return nodes.get(0);
}
}
测试结果,哈夫曼树的前序遍历:
哈夫曼树有什么用?哈夫曼编码
哈夫曼编码是一种编码方式,属于一种算法,是电讯通信的经典应用之一,广泛用于数据文件的压缩,其压缩率通常在20%~90%
关于编码方式
定长编码
我们知道通信是二进制通信,要传输字符串Hello World Hello Hello World
需要以下步骤:
- 字符串分割为字符数组,所有的字符解析为ASCII码
根据ASCII码:H-72,e-101,l-108,o-111,W-87,r-114,d-100,空格-32
Hello World Hello Hello World
=>
(Hello)72 101 108 108 111 32
(World)87 111 114 108 100 32
(Hello)72 101 108 108 111 32
(Hello)72 101 108 108 111 32
(World)87 111 114 108 100
- 将ASCII码转为二进制数:也就是十进制转二进制
01001000 01100101 01101100 01101100 01101111 00100000
01010111 01101111 01110010 01101100 01100100 00100000
01001000 01100101 01101100 01101100 01101111 00100000
01001000 01100101 01101100 01101100 01101111 00100000
01010111 01101111 01110010 01101100 01100100
虽然这样传输是可以达成目标的,但是太浪费了,为了传输29个字符,需要传输29*8=232个二进制数
变长编码
对于字符串Hello World Hello Hello World
,各个字符出现次数 H:3,e:3,l:8,o:5,W:2,r:2,d:2,空格:4
根据出现次数编码,出现次数越多编码越小:
可以自定义编码规则为:
l=0,o=1,空格=10,H=11,e=100,r=101,d=110,W=111
字符串对应的编码为:1110000110111…
这样编码是简便了,但是存在一个问题:无法解码,11可以解码为H,也可以解码为ll
前缀编码:符号字符的编码不能是其他字符编码的前缀要求的编码
哈夫曼编码
对于字符串Hello World Hello Hello World
,各个字符出现次数 H:3,e:3,l:8,o:5,W:2,r:2,d:2,空格:4
根据这些字符构建一颗哈夫曼树,以字符出现的次数为权值
关于构建哈夫曼树的步骤上面解释了,这里直接编程:
树结点实体类:新增data、weight属性
//哈夫曼树结点
class Node implements Comparable<Node>{
//存放数据本身,'a' => 97
Byte data;
//权值,表示字符出现的次数
int weight;
//左右结点
Node left;
Node right;
public Node(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
@Override
public String toString