赫夫曼树
基本介绍
-
给定n个权值作为n个叶子节点,构造一颗二叉树,若该数的带权路径长度达到最小,称这样的二叉树为最优二叉树,也叫哈夫曼树,还有的书翻译霍夫曼树。
-
赫夫曼树是带权路径长度最短的树,权值较大的带你离根较近。
重要概念
- 路径和路径长度:在一颗树中,从一个节点往下可到达的孩子或孙子节点之间的通路,称为路径。通路中分支的树木称为==路径长度。==若规定根节点的层数为1,则从根节点到第L层节点的路径长度为L-1。
- 节点的权:若将树中的节点赋给一个有着某种含义的数值,则这个数值称为该节点的权。
- 节点的带权路径长度:从根节点到该节点之间的路径长度与该节点的权的乘积。
- 树的带权路径长度:所有叶子节点的带权路径长度之和,记为WPL(weighted path length),权值越大的结点距离根节点越近的二叉树才是最优二叉树
- WPL最小的就是赫夫曼树
数组转换成赫夫曼树
思路
{13,7,8,29,6,1}
构成赫夫曼树的步骤
- 从小到大进行排序,将每一个数据,每个数据都是一个节点,每个节点可以看成是一颗最简单的二叉树
- 取出根节点权值最小的两颗二叉树(也就是按照从小到大的前两个元素)
- 取出一颗新的二叉树,该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
- 再将这颗新的二叉树,以根节点的权值大小再次排序,不断重复1-2-3-4的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树
例子
如何构建一颗赫夫曼树?
13,3,7,8,3,29,6,1
排序
1,3,6 , 7 , 8 , 13 29
取出根节点权值最小的两颗二叉树
新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
代码
package J树的提高;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @Author Zhou jian
* @Date 2020 ${month} 2020/1/9 0009 19:30
*/
public class HufmanTree {
public static void main(String[] args) {
int[] arr = {13,7,8,3,29,6,1};
Node head = createHuffmanTree(arr);
head.preOrder();// 67 29 38 15 7 8 23 10 4 1 3 6 13
}
//创建赫夫曼树的方法
/**
*
* @param arr 需要创建爱你成赫夫曼树的数组
* @return 创建好后赫夫曼的root节点
*/
public static Node createHuffmanTree(int[] arr){
//第一步为了操作方便
//1、遍历arr数组
//2、将arr的每个元素构成一个Node
//3、将Node放入到ArrayList中
List<Node> nodes = new ArrayList<>();
for(int value:arr){
nodes.add(new Node(value));
}
//处理的过程是一个循环的过程,直到数列中,所有的数据都被处理,就得到一颗霍夫曼树
while(nodes.size()>1) {
//排序,从小到大 取决于Node中的比较方法
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对象支持排序
//让Node实现Comparable接口
class Node implements Comparable<Node>{
int value;//节点权值
Node left;//指向左子节点
Node right;//指向右子节点
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;
}
//前序遍历
public void preOrder(){
System.out.println(this);
if(this.left!=null){
this.left.preOrder();
}
if(this.right!=null){
this.right.preOrder();
}
}
}
赫夫曼编码
基本介绍
- 是一种编码方式,属于一种程序算法
- 赫夫曼编码时是哈夫曼树在电讯通信中的经典应用
- 赫夫曼树编码广泛应用于数据文件压缩,其压缩率通常在20%-90%
- 赫夫曼码是可变字长编码(VLC)的一种,Huffman于1952年提出一种编码方法,称为最佳编码
通信原理剖析
赫夫曼原理
- 传输的字符串: i like like like java do you like a java // 共40个字符(包括空格)
- 统计各个字符串出现的次数 d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数
- 按照上面字符出现的次数构建一颗赫夫曼树,次数作为权值
步骤:
- 从小到大进行排序,将每一个数据,每个数据都是一个节点,每个节点可以看成是一颗最简单的二叉树
- 取出根节点权值最小的两颗二叉树(也就是按照从小到大的前两个元素)
- 取出一颗新的二叉树,该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
- 再将这颗新的二叉树,以根节点的权值大小再次排序,不断重复1-2-3-4的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树
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
5.按照上面的赫夫曼编码,我们的"i like like like java do you like a java" 字符串对应的编码为 (注意这里我们使用的无损压缩)
1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
长度为 : 133
说明:
1)原来长度是 359 , 压缩了 (359-133) / 359 = 62.9%
2)此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性
3)这个赫夫曼树根据排序方法的不同,也可能不太一样,==这样对应的赫夫曼编码也不完全一样,==但是wpl是一样的 ,都是最小的比如: 如果我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为:
代码
思路:
- Node{data,weight(权值),left和right}
- 得到”I like like java do you like a java"对应的byte[] 数组
- 编写一个方法,将准备构建的赫夫曼树的节点放到list中,形式[Node[data=97,weight=5],Node[data=32,weight=9],…]
- 通过List创建对应的赫夫曼树
- 根据生成的赫夫曼树生成对应的赫夫曼编码表(根据赫夫曼树,给各个字符规定编码,向左的路径为0,向右的路径为1)
- 将字符串对应的byte数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte
package J树的提高;
import java.util.*;
/**
* @Author Zhou jian
* @Date 2020 ${month} 2020/1/10 0010 11:26
* 赫夫曼编码
*/
public class HuffmanCode {
public static void main(String[] args) {
String content= "I like like like java do you like a java";
byte[] contentBytes = content.getBytes();
// List<Node> nodes = getNodes(contentBytes); //[Node{data=32, weight=9}, Node{data=97, weight=5}, Node{data=100, weight=1}, Node{data=101, weight=4}, Node{data=73, weight=1}, Node{data=105, weight=4}, Node{data=106, weight=2}, Node{data=107, weight=4}, Node{data=108, weight=4}, Node{data=111, weight=2}, Node{data=117, weight=1}, Node{data=118, weight=2}, Node{data=121, weight=1}]
//
// //测试一把,创建的赫夫曼二叉树
// System.out.println("hefumanshu");
// Node hufmanTreeRoot = createHuffmanTree(nodes);
//
// //前序遍历创建的赫夫曼二叉树
// hufmanTreeRoot.preOrder();
//
//
// //测试是否生成了对应的哈夫曼编码
// getCodes(hufmanTreeRoot,"",stringBuilder);
// System.out.println("生成的哈夫曼编码表");
// System.out.println(huffmanCodes);
//
// //根据哈夫曼编码表生成对应的byte数组
// byte[] huffmanCodeBytes= zip( contentBytes,huffmanCodes);
// System.out.println(Arrays.toString(huffmanCodeBytes));
byte[] huffmanByteCodes = huffmanZip(contentBytes);
System.out.println(Arrays.toString(huffmanByteCodes));
}
//使用一个方法:将前面的方法封装起来,便于我们的调用
/**
*
* @param bytes 原始的字符串对应的字节数组
* @return 经过赫夫曼编码处理后的数组
*/
private static byte[] huffmanZip(byte[] bytes){
//1、
List<Node> nodes = getNodes(bytes); //[Node{data=32, weight=9}, Node{data=97, weight=5}, Node{data=100, weight=1}, Node{data=101, weight=4}, Node{data=73, weight=1}, Node{data=105, weight=4}, Node{data=106, weight=2}, Node{data=107, weight=4}, Node{data=108, weight=4}, Node{data=111, weight=2}, Node{data=117, weight=1}, Node{data=118, weight=2}, Node{data=121, weight=1}]
//2、测试一把,创建的赫夫曼二叉树
System.out.println("hefumanshu");
Node hufmanTreeRoot = createHuffmanTree(nodes);
//3、//测试是否生成了对应的哈夫曼编码
getCodes(hufmanTreeRoot,"",stringBuilder);
//4、根据哈夫曼编码表生成对应的byte数组
byte[] huffmanCodeBytes= zip( bytes,huffmanCodes);
return huffmanCodeBytes;
}
// 将字符串对应的byte数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte
/**
*
* @param bytes 这时原始的字符串对应的byte[[]
* @param huffmanCodes 生成的赫夫曼编码 map
*
* @return 返回赫夫曼编码处理后的byte[]
* 距离:
* String content ="i like java "
* 返回的字符串“1010100010111111
*
*/
private static byte[] zip(byte[] bytes,Map<Byte,String> huffmanCodes){
//1、利用huffmanCodes将bytes转成赫夫曼编码对应的字符串
StringBuilder stringBuilder = new StringBuilder();
//遍历bytes数组
for(byte b:bytes){
stringBuilder.append(huffmanCodes.get(b));
}
// System.out.println(stringBuilder);
//将StringbUILDER转成byte[],统计返回byte[] huffmanCodeBytes长度
int len;
if(stringBuilder.length()%8==0){
len = stringBuilder.length()/8;
}else{
len = stringBuilder.length()/8+1;
}
//创建HuffmanCodeBytes
//创建存储压缩后的byte数组
byte[] huffmanCodeBytes = new byte[len];
int index=0;//记录是第几个byte
for(int i=0;i<stringBuilder.length();i+=8){//因为每8位对应一个byte,所以步长+8
String strByte;
if(i+8>stringBuilder.length()){//不够8位
strByte = stringBuilder.substring(i);
}else{
strByte = stringBuilder.substring(i,i+8);
}
//将strByte转成一个byte,放入到huffmanCodeBytes
huffmanCodeBytes[index]=(byte)Integer.parseInt(strByte,2);
index++;
}
return huffmanCodeBytes;
}
//生成赫夫曼树对应的赫夫曼编码
//思路:
//1、将赫夫曼编码表存放在map中比较合适 Map<Byte,String> 形式如: o->1000 u->10010 d->100110 y->100111 i->10 a->110 k->1110 e->1111 j: 0000 v: 0001 l: 001 : 01
//2、在生成赫夫曼编码表需要拼接路径 ,定义一个StringBuilder,村出某个叶子节点的路径
static Map<Byte,String > huffmanCodes = new HashMap<>();
static StringBuilder stringBuilder = new StringBuilder();
/**
* 将传入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());
}
}
}
/**
*
* @param bytes 接受字节数组
* @return 返回的就是形式 [Node[data=97,weight=5],Node[data=32,weight=9],.....]
*/
private static List<Node> getNodes(byte[] bytes){
//1、创建ArrayList
ArrayList<Node> nodes = new ArrayList<>();
//2、遍历bytes 存储每个byte出现的次数->
Map<Byte,Integer> counts= new HashMap<Byte,Integer>();
for(byte b:bytes){
Integer count = counts.get(b);
if(count==null){//map还没有这个数据
counts.put(b,1);
}else{
counts.put(b,count+1);
}
}
//3、把每个键值对转成Node对象,并加入到nodes集合中
//遍历map
for(Map.Entry<Byte,Integer> entry:counts.entrySet()){
nodes.add(new Node(entry.getKey(),entry.getValue()));
}
return nodes;
}
//通过list创建对应的赫夫曼树
private static Node createHuffmanTree(List<Node> nodes){
while(nodes.size()>1){
//排序,从小到大
Collections.sort(nodes);
//取出第一课最小的二叉树
Node leftNode= nodes.get(0);
//取出第二课最小的二叉树
Node rightNode = nodes.get(1);
//创建一颗新的二叉树,它的节点没有data只有权值
Node parentNode = new Node(null,leftNode.weight+rightNode.weight);
parentNode.left=leftNode;
parentNode.right=rightNode;
//将处理过的二叉树从nodes移除
nodes.remove(leftNode);
nodes.remove(rightNode);
//将新的二叉树加入到nodes中
nodes.add(parentNode);
}
//nodes最后的节点就是哈夫曼树的根节点
return nodes.get(0);
}
}
//创建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();
}
}
}