基本介绍
(1)给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)(weightt path length)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree), 还有的书翻译为霍夫曼树。
(2)赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
赫夫曼树几个重要概念和举例说明
路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1
结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
赫夫曼树创建步骤图解
构成赫夫曼树的步骤:
从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树(左右节点为空)
取出根节点权值最小的两颗二叉树
组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树
给你一个数列 {13, 7, 8, 3, 29, 6, 1},要求转成一颗赫夫曼树.
排序
1, 3, 6, 7, 8, 13, 29
于是数列变成 4,6, 7, 8, 13, 29
于是数列变成 , 7, 8, 10,13, 29
10,13,15,29
15 23 29
29,38
赫夫曼树创建代码实现
前序遍历
package com.atguigu.huffmantree;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class HuffmanTree {
public static void main(String[] args) {
int arr[] = { 13, 7, 8, 3, 29, 6, 1 };
Node root = createHuffmanTree(arr);
//测试一把
preOrder(root); //
}
//编写一个前序遍历的方法
public static void preOrder(Node root) {
if(root != null) {
root.preOrder();
}else{
System.out.println("是空树,不能遍历~~");
}
}
// 创建赫夫曼树的方法
/**
*
* @param arr 需要创建成哈夫曼树的数组
* @return 创建好后的赫夫曼树的root结点
*/
public static Node createHuffmanTree(int[] arr) {
// 第一步为了操作方便
// 1. 遍历 arr 数组
// 2. 将arr的每个元素构成成一个Node
// 3. 将Node 放入到ArrayList中
List<Node> nodes = new ArrayList<Node>();
for (int value : arr) {
nodes.add(new Node(value));
}
//我们处理的过程是一个循环的过程
while(nodes.size() > 1) {//最后只剩下一个节点
//排序 从小到大
Collections.sort(nodes);
System.out.println("nodes =" + nodes);
//取出根节点权值最小的两颗二叉树
//(1) 取出权值最小的结点(二叉树)
Node leftNode = nodes.get(0);
//(2) 取出权值第二小的结点(二叉树)
Node rightNode = nodes.get(1);
//(3)构建一颗新的二叉树
Node parent = new Node(leftNode.value + rightNode.value);
parent.left = leftNode;
parent.right = rightNode;
//(4)从ArrayList删除处理过的二叉树
nodes.remove(leftNode);
nodes.remove(rightNode);
//(5)将parent加入到nodes
nodes.add(parent);
}
//返回哈夫曼树的root结点
return nodes.get(0);
}
}
// 创建结点类
// 为了让Node 对象支持排序Collections集合排序
// 让Node 实现Comparable接口
class Node implements Comparable<Node> {
int value; // 结点权值
char c; //字符
Node left; // 指向左子结点
Node right; // 指向右子结点
//写一个前序遍历
public void preOrder() {
System.out.println(this);
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) {
// TODO Auto-generated method stub
// 表示从小到大排序
return this.value - o.value;
}
}
nodes=[Node{value=1}, Node{value=3}, Node{value=6}, Node{value=7}, Node{value=8}, Node{value=13}, Node{value=29}]
nodes=[Node{value=4}, Node{value=6}, Node{value=7}, Node{value=8}, Node{value=13}, Node{value=29}]
nodes=[Node{value=7}, Node{value=8}, Node{value=10}, Node{value=13}, Node{value=29}]
nodes=[Node{value=10}, Node{value=13}, Node{value=15}, Node{value=29}]
nodes=[Node{value=15}, Node{value=23}, Node{value=29}]
nodes=[Node{value=29}, Node{value=38}]
Node{value=67}
Node{value=29}
Node{value=38}
Node{value=15}
Node{value=7}
Node{value=8}
Node{value=23}
Node{value=10}
Node{value=4}
Node{value=1}
Node{value=3}
Node{value=6}
Node{value=13}
之所以可以前序遍历是因为下面两句每一次都压入了底层节点
parent.left = leftNode;
parent.right = rightNode;
赫夫曼编码
赫夫曼编码也翻译为 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法
赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。
赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间
赫夫曼码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,称之为最佳编码
变长编码举例说明
在线转码 工具 :https://www.mokuge.com/tool/asciito16/
但是这样总长度太长了,于是出现如下存储方式
但是这种编码会有二义性,(解码不好匹配)
赫夫曼编码原理剖析
步骤:
构成赫夫曼树的步骤:
- 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
- 取出根节点权值最小的两颗二叉树
- 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
- 再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树
-
根据赫夫曼树,给各个字符,规定编码 (前缀编码:每个字符的编码都不会是另一个字符编码的前缀,匹配的时候就不会出现多义性), 向左的路径为0 向右的路径为1 , 编码如下:
o: 1000 u: 10010 d: 100110 y: 100111 i: 101
a : 110 k: 1110 e: 1111 j: 0000 v: 0001
l: 001 : 01 -
按照上面的赫夫曼编码,我们的"i like like like java do you like a java" 字符串对应的编码为 (注意这里我们使用的无损压缩)
1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110 通过赫夫曼编码处理 长度为 133
6) 长度为 : 133
说明:
原来长度是 359 , 压缩了 (359-133) / 359 = 62.9%
此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性
赫夫曼编码是无损处理方案,即恢复的时候不会出现问题
注意, 这个赫夫曼树根据排序方法不同,也可能不太一样(比如某些字符出现的次数一样),这样对应的赫夫曼编码也不完全一样,但是wpl 是一样的,都是最小的, 比如: 如果我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为:
数据压缩,创建赫夫曼树思路
功能: 根据赫夫曼编码压缩数据的原理,需要创建 “i like like like java do you like a java” 对应的赫夫曼树
思路:
(1) Node { data (存放数据), weight (权值), left 和 right }
(2) 得到 “i like like like java do you like a java” 对应的 byte[] 数组
(3) 编写一个方法,将准备构建赫夫曼树的Node 节点放到 List , 形式 [Node[date=97 ,weight = 5], Node[]date=32,weight = 9]…], 体现 d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9
(4) 可以通过List 创建对应的赫夫曼树
数据压缩,创建赫夫曼树实现
(1) Node { data (存放数据), weight (权值), left 和 right }
//创建Node,带数据和权值
class Node implements Comparable<Node>{
Byte data;//存放数据本身,比如'a'=97 ''=32
int weight;//权值,表示字符出现的次数
Node left;
Node right;
public Node(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
@Override
public int compareTo(Node o) {
return this.weight-o.weight;//从小到大排序
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
//前序遍历
public void preOrder(){
System.out.println(this);
if(this.left!=null)
{
this.left.preOrder();
}
if(this.right!=null)
{
this.right.preOrder();
}
}
}
(2) 得到 “i like like like java do you like a java” 对应的 byte[] 数组
String content="i like like like java do you like a java";
byte [] contentBytes = content.getBytes();
(3) 编写一个方法,将准备构建赫夫曼树的Node 节点放到 List , 形式 [Node[date=97 ,weight = 5], Node[]date=32,weight = 9]…], 体现 d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9
private static List<Node> getBytes(byte[] bytes)
{
//1创建一个ArrayList
ArrayList<Node> nodes = new ArrayList<Node>();
//遍历bytes,用map存储每一个byte出现的次数
Map<Byte,Integer> counts=new HashMap<>();
for(byte b:bytes)
{
// System.out.print(b+" ");//105 32 108 105 107 101 32 108 105 107 101 32 108 105 107 101 32 106 97 118 97 32 100 111 32 121 111 117 32 108 105 107 101 32 97 32 106 97 118 97
Integer count=counts.get(b);
if(count==null)
{
counts.put(b,1);
}
else
{
counts.put(b,count+1);
}
}
//把每个键值对转成一个Node对象,并加入到nodes集合
for(Map.Entry<Byte,Integer>entry:counts.entrySet())
{
nodes.add(new Node(entry.getKey(),entry.getValue()));
}
return nodes;
}
测试
List<Node>nodes=getBytes( contentBytes);
System.out.println(nodes);
[Node{data=32, weight=9}, Node{data=97, weight=5}, Node{data=100, weight=1},
Node{data=101, weight=4}, Node{data=117, weight=1}, Node{data=118, weight=2},
Node{data=105, weight=5}, Node{data=121, weight=1}, Node{data=106, weight=2},
Node{data=107, weight=4}, Node{data=108, weight=4}, Node{data=111, weight=2}]
(4) 可以通过List 创建对应的赫夫曼树
private static Node creatHuffmanTree(List<Node> nodes)
{
while(nodes.size()>1)
{
//先从小到大排序
Collections.sort(nodes);
//取出第一棵最小的二叉树(一个节点就是一个二叉树)
Node leftNode = nodes.get(0);
//取出第一棵最小的二叉树
Node rightNode = nodes.get(1);
//创建一棵新的二叉树,它的根节点没有data,只有权值
Node parent=new Node(null,leftNode.weight+rightNode.weight);
parent.left=leftNode;
parent.right=rightNode;
//将处理过的节点删除
nodes.remove(leftNode);
nodes.remove(rightNode);
//加入新的节点
nodes.add(parent);
}
//nodes最后的节点就是哈夫曼数的根节点
return nodes.get(0);
}
前序遍历赫夫曼树
//前序遍历
private static void preOrder(Node root)
{
if(root!=null)
{
root.preOrder();
}
else{
System.out.println("赫夫曼树为空");
}
}
测试代码
List<Node>nodes=getBytes( contentBytes);
System.out.println("哈夫曼树");
Node huffmanTreeRoot= creatHuffmanTree( nodes);
System.out.println("前序遍历");
huffmanTreeRoot.preOrder();
哈夫曼树
前序遍历
Node{data=null, weight=40}
Node{data=null, weight=17}
Node{data=null, weight=8}
Node{data=108, weight=4}
Node{data=null, weight=4}
Node{data=106, weight=2}
Node{data=111, weight=2}
Node{data=32, weight=9}
Node{data=null, weight=23}
Node{data=null, weight=10}
Node{data=97, weight=5}
Node{data=105, weight=5}
Node{data=null, weight=13}
Node{data=null, weight=5}
Node{data=null, weight=2}
Node{data=100, weight=1}
Node{data=117, weight=1}
Node{data=null, weight=3}
Node{data=121, weight=1}
Node{data=118, weight=2}
Node{data=null, weight=8}
Node{data=101, weight=4}
Node{data=107, weight=4}
数据压缩-生成赫夫曼编码表
生成赫夫曼树对应的赫夫曼编码 , 如下表:空格=01 a=100 d=11000 u=11001 e=1110 v=11011 i=101 y=11010 j=0010 k=1111 l=000 o=0011
使用赫夫曼编码来生成赫夫曼编码数据 ,即按照上面的赫夫曼编码,将"i like like like java do you like a java" 字符串生成对应的编码数据, 形式如下.1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
//生成赫夫曼树对应的赫夫曼编码
//思路:
//1. 将赫夫曼编码表存放在 Map<Byte,String> 形式
// 生成的赫夫曼编码表{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
static Map<Byte, String> huffmanCodes = new HashMap<Byte,String>();
//2. 在生成赫夫曼编码表示,需要去拼接路径, 定义一个StringBuilder 存储某个叶子结点的路径
static StringBuilder stringBuilder = new StringBuilder();
//为了调用方便,我们重载 getCodes
private static Map<Byte, String> getCodes(Node root) {
if(root == null) {
return null;
}
//处理root的左子树
getCodes(root.left, "0", stringBuilder);
//处理root的右子树
getCodes(root.right, "1", stringBuilder);
return huffmanCodes;
}
/**
* 功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合
* @param node 传入结点
* @param code 路径: 左子结点是 0, 右子结点 1
* @param stringBuilder 用于拼接路径
*/
private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
//将code 加入到 stringBuilder2
stringBuilder2.append(code);
if(node != null) { //如果node == null不处理
//判断当前node 是叶子结点还是非叶子结点
if(node.data == null) { //非叶子结点
//递归处理
//向左递归
getCodes(node.left, "0", stringBuilder2);
//向右递归
getCodes(node.right, "1", stringBuilder2);
} else { //说明是一个叶子结点
//就表示找到某个叶子结点的最后
huffmanCodes.put(node.data, stringBuilder2.toString());
}
}
}
测试
//测试一把是否生成了对应的赫夫曼编码
Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
System.out.println("~生成的赫夫曼编码表= " + huffmanCodes);
~生成的赫夫曼编码表= {32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101,
121=11010, 106=0010, 107=1111, 108=000, 111=0011}
包含数据压缩数据解压部分的代码见下面链接
链接:https://pan.baidu.com/s/1FzCLZiC5xxkcfY11YUhgyA
提取码:flq7