1.介绍
①给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)到达最小,称这样的二叉树为最优二叉树,也称为哈夫曼树,还有的书翻译为霍夫曼树。
②赫夫曼树 是带权路径最短的树,权值较大的结点离根结点较近。
③路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称之为路径。通路中的分支的数目称为路径长度,若规定根结点的层数为1,则从根节点到第L层结点到路径长度为L-1。
④结点的权及带权路径长度:若将结点赋给一个有某中含义的数值,这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与结点的权的乘积。
⑤树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL,权值越大的结点离根结点越近的二叉树才是最优二叉树。
2.构建步骤
①从小到大进行排序,将每个数据,每一个数据都是一个结点,每个结点可以看成是一棵最简单的二叉树。
②取出根节点权值最小的两颗二叉树。
③组成一棵新的二叉树,该二叉树的根节点的权值是前面两颗二叉树根节点权值的和。
④再将这颗新的二叉树,以根结点的权值大小再排序,不断重复1-2-3-4的步骤,直到数列中,所有的数据被处理,就得到一棵赫夫曼树。
3.代码实现:
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> lists=new ArrayList<Node>();
for (int item:arr){
lists.add(new Node(item));
}
while (lists.size()>1){
// 将数据进行排序
Collections.sort(lists);
// 取出根节点权值最小的两颗二叉树
// (1) 取出权值最小的结点(二叉树)
Node node1=lists.get(0);
// (2) 取出权值第二小的结点(二叉树)
Node node2=lists.get(1);
//(3)构建一颗新的二叉树
Node parent=new Node(node1.value+node2.value);
parent.left=node1;
parent.right=node2;
lists.remove(node1);
lists.remove(node2);
lists.add(parent);
}
return lists.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) {
return this.value-o.value;
}
}
4.赫夫曼编码
4.1基本介绍
①赫夫曼编码翻译为哈夫曼编码,霍夫曼编码,是一种编码方式,属于程序算法。
②赫夫曼编码是赫夫曼树在电讯通信中的经典的应用之一。
③赫夫曼编码广泛地用于数据文件的压缩。其压缩率通常在20%~90%之间。
④赫夫曼码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,称之为最佳编码。
4.1压缩案例
传输的 字符串
1) i like like like java do you like a java
2) d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数
3) 按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值
步骤:
构成赫夫曼树的步骤:
1) 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
2) 取出根节点权值最小的两颗二叉树
3) 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
4) 再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 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。
代码实现:
import java.util.*;
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();
System.out.println(contentBytes.length); //40
byte[] huffmanCodesBytes= huffmanZip(contentBytes);
System.out.println("压缩后的结果是:" + Arrays.toString(huffmanCodesBytes) + " 长度= " + huffmanCodesBytes.length);
}
//使用一个方法,将前面的方法封装起来,便于我们的调用.
/**
*
* @param bytes 原始的字符串对应的字节数组
* @return 是经过 赫夫曼编码处理后的字节数组(压缩后的数组)
*/
public static byte[] huffmanZip(byte[] bytes){
List<Node>list=getCode(bytes);
//根据 nodes 创建的赫夫曼树
Node huffmanTree = createHuffmanTree(list);
//对应的赫夫曼编码(根据 赫夫曼树)
Map<Byte, String> codes = getCodes(huffmanTree);
//根据生成的赫夫曼编码,压缩得到压缩后的赫夫曼编码字节数组
byte[] zip = zip(bytes, codes);
return zip;
}
//编写一个方法,将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte[]
/**
*
* @param bytes 这时原始的字符串对应的 byte[]
* @param huffmanCodes 生成的赫夫曼编码map
* @return 返回赫夫曼编码处理后的 byte[]
* 举例: String content = "i like like like java do you like a java"; =》 byte[] contentBytes = content.getBytes();
* 返回的是 字符串 "1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
* => 对应的 byte[] huffmanCodeBytes ,即 8位对应一个 byte,放入到 huffmanCodeBytes
* huffmanCodeBytes[0] = 10101000(补码) => byte [推导 10101000=> 10101000 - 1 => 10100111(反码)=> 11011000= -88 ]
* huffmanCodeBytes[1] = -88
*/
public static byte[] zip(byte[]bytes,Map<Byte,String> huffmanCodes){
//1.利用 huffmanCodes 将 bytes 转成 赫夫曼编码对应的字符串
StringBuilder stringBuilder = new StringBuilder();
for (byte b:bytes){
stringBuilder.append(huffmanCodes.get(b));
}
System.out.println(stringBuilder);
int len=0;
if(stringBuilder.length()%8==0){
len=stringBuilder.length()/8;
}else {
len=stringBuilder.length()/8+1;
}
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()){
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<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
public static Map<Byte, String> getCodes(Node node){
if(node==null){
return null;
}
// 处理root的左子树
getCodes(node.left,"0",stringBuilder);
//处理root的右子树
getCodes(node.right,"1",stringBuilder);
return huffmanCodes;
}
/**
* 功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合
* @param node 传入结点
* @param code 路径: 左子结点是 0, 右子结点 1
* @param stringBuilder 用于拼接路径
*/
public static void getCodes(Node node,String code,StringBuilder stringBuilder){
StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);
stringBuilder1.append(code);
if(node!=null){
if(node.data==null){
//递归处理
//向左递归
getCodes(node.left,"0",stringBuilder1);
// 向右递归
getCodes(node.right,"1",stringBuilder1);
}else {
huffmanCodes.put(node.data,stringBuilder1.toString());
}
}
}
//前序遍历的方法
public static void preOrder(Node root){
if(root!=null){
root.preOrder();
}else {
System.out.println("该树为空~~");
}
}
/**
*
* @param bytes 接收字节数组
* @return 返回的就是 List 形式 [Node[date=97 ,weight = 5], Node[]date=32,weight = 9]......],
*/
public static List<Node>getCode(byte[] bytes){
//1创建一个ArrayList
List<Node>lists=new ArrayList<>();
//遍历 bytes , 统计 每一个byte出现的次数->map[key,value]
Map<Byte,Integer> map=new HashMap<>();
for(byte b:bytes){
Integer count=map.get(b);
if(count==null){// Map还没有这个字符数据,第一次
map.put(b,1);
}else {
map.put(b,count+1);
}
}
//把每一个键值对转成一个Node 对象,并加入到nodes集合
//遍历map
for(Map.Entry<Byte,Integer> entry:map.entrySet()){
lists.add(new Node(entry.getKey(),entry.getValue()));
}
return lists;
}
//可以通过List 创建对应的赫夫曼树
public static Node createHuffmanTree(List<Node> lists){
while (lists.size()>1){
// 将数据进行排序
Collections.sort(lists);
// 取出根节点权值最小的两颗二叉树
// (1) 取出权值最小的结点(二叉树)
Node node1=lists.get(0);
// (2) 取出权值第二小的结点(二叉树)
Node node2=lists.get(1);
//(3)构建一颗新的二叉树
Node parent=new Node(null,node1.weight+node2.weight);
parent.left=node1;
parent.right=node2;
lists.remove(node1);
lists.remove(node2);
lists.add(parent);
}
return lists.get(0);
}
}
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();
}
}
}
总结:上面实现的使用对字符串进行压缩,压缩成一个字节数组下面是结果显示。原来长度为40,压缩后长度为17.