import java.util.ArrayList;
import java.util.Collections;
/*
* 哈夫曼树(赫夫曼树)
* 1.给定n个权值作为n个叶子节点,构造一颗二叉树,若该树的带权路径长度(wpl)达到最小
* 这样的树称为最优二叉树,也称哈夫曼树
* 2.哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近
*
* 重要概念
* 1.路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径
* 通路中分支的数目称为路径长度
* 若规定根节点的层数为1,则从根节点到第L层结点的路径长度为 L-1
* 2.结点的权和带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值就称为该节点的权
* 从根节点到该结点之间的 路径长度 与该结点的 权 的 乘积 就为结点的带权路径长度
* 3.树的带权路径长度:即为所有叶子节点的带权路径长度之和,记为WPL(weighted path length)
* WPL最小就是哈夫曼树
*
* 构建哈夫曼树
* 一个数列[13,7,8,3,29,6,1],要求转成哈夫曼树
* 步骤
* 1.从小到大进行排序,将每一个数据就是一个结点,每个结点就是一颗最简单的二叉树
* 2.取出根节点权值最小的两颗二叉树,组成一颗新的二叉树
* 3.新的二叉树的权值是前面两颗二叉树根节点权值的和
* 4.再将这颗新的二叉树以根节点的权值大小进行再次排序,重复上述步骤,直到所有数据被处理
*
*/
public class HuffmanTree_ {
public static void main(String[] args) {
int[] arr = {13,7,8,3,29,6,1};
Node root = createHuffmanTree(arr);
//前序遍历测试构建的哈夫曼树
PreOrder(root);//67,29,38,15,7,8,23,10,4,1,3,6,13
}
//创建哈夫曼树的方法
public static Node createHuffmanTree(int[] arr) {
//遍历arr数组,将每个元素构成一个Node,将Node放入集合ArrayList中
ArrayList<Node> nodes = new ArrayList<Node>();
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);
System.out.println(nodes);
}
//返回哈夫曼树的root结点
return nodes.get(0);
}
//前序遍历的方法
public static void PreOrder(Node root) {
if (root != null) {
root.preOrder();
}else {
System.out.println("空树,无法遍历");
}
}
}
//创建结点
//为了让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();
}
}
}
/*
* 哈夫曼编码
* 1.哈夫曼编码是一种编码方式,属于一种程序算法
* 2.哈夫曼编码是哈夫曼树在电讯通信中的经典应用之一
* 3.哈夫曼编码广泛地用于数据文件压缩,压缩率通常在20%~90%之间
* 4.哈夫曼编码是可变字长编码(VLC)的一种,称之为最佳编码
*
* 原理剖析
* 通信领域中信息的处理方式1——定长编码
* I like like like java do you like a java//共40个字符(包含空格)
* 对应的ASCII码
* 105(I) 32(空格) 108 105 107 101 32 106 97 118 97....
* 对应的二进制
* 01101001 00100000 ...
* 按照二进制来传递信息,总的长度为359(包括空格)
*
* 处理方式2——变长编码
* I like like like java do you like a java//共40个字符(包含空格)
* d:1 y:1 u:1 j:2 v:2 l:4 k:4 e:4 i:5 a:5 :9//各个字符出现的次数
* 0= ,1=a,10=i,11=e,100=k,101=I,110=o,111=v,1000=j,1001=u,1010=y,1011=d
* 按照各个字符出现的次数进行编码,原则是出现次数越多的,编码越小,如空格出现最多,编码为0
* 在传递I like java对应的编码就是:
* 10010110100.....
* 问题:二义性
*
* 字符的编码都不能是其他字符编码的前缀,符合此要求的叫做前缀编码,即不能匹配到重复的编码
* 处理方式3——哈夫曼编码
* d:1 y:1 u:1 j:2 v:2 l:4 k:4 e:4 i:5 a:5 :9//各个字符出现的次数
* 按照上面字符出现的次数构建哈夫曼树,次数作为权值
* 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
*
* 按照上述哈夫曼编码,对应的编码为
* 1010100110111101.......长度为133
* 原长度为359,压缩了(359-133)/359=62.9%
* 哈夫曼编码是无损处理方案
*
* 注:根据排序方法不同,对应的哈夫曼编码可能不同,但wpl(树的带权路径长度)是一样的
*
* 最佳实践——数据压缩
* 思路
* 1.Node(data(存放数据),weight(权值),left和right)
* 2.得到I like like like java do you like java对应的byte[]数组
* 3.编写一个方法,构建哈夫曼树的Node结点放入List,形式为:[Node[data=97,weight=5],Node[data=32,weight=9]....]
* 4.通过List创建对应的哈夫曼树
*
* 将生成的哈夫曼树对应生成哈夫曼编码
*
* 使用哈夫曼编码生成哈夫曼编码数据
*
* 数据解压
* 思路
* 1.将哈夫曼编码压缩数组重新转成哈夫曼编码对应的二进制字符串
* 2.将二进制字符串 对照哈夫曼编码表 转成对应的字符串
*
* 最佳实践——文件压缩
* 具体:给定图片文件,要求对其无损压缩
* 读取文件->得到哈夫曼编码表->完成压缩
*
* 哈夫曼编码压缩文件注意事项
* 1.如果文件本身就是经过压缩处理的,那么使用哈夫曼编码再压缩效率不会有明显变化
* 2.哈夫曼编码是按字节来处理的,因此可以处理所有文件
* 3.如果一个文件的内容重复数据不多,压缩效果也不会
*/
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class HuffmanCoding_ {
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[] huffmanCodeBytes = huffmanZip(contentBytes);
System.out.println("压缩后=" + Arrays.toString(huffmanCodeBytes) + "\n长度为=" + huffmanCodeBytes.length);
byte[] sourceBytes = decode(huffmanCodes, huffmanCodeBytes);
System.out.println("原字符串=" + new String(sourceBytes));//I like like like java do you like a java
//测试文件压缩和解压
System.out.println("================================================================================");
String srcFile = "d://demo.png";
String dstFile = "d://dst.zip";
zipFile(srcFile, dstFile);
System.out.println("压缩成功");
String zipFile = "d://dst.zip";
String dstFile2 = "d://demo2.png";
unZipFile(zipFile, dstFile2);
System.out.println("解压成功");
/* 分步进行
List<Node> nodes = getNodes(contentBytes);
System.out.println(nodes);
//前序遍历测试构建的哈夫曼树
Node huffmanTreeRoot = createHuffmanTree(nodes);
huffmanTreeRoot.preOrder();//[null,40][null,17][null,8],[108,4]....
//测试是否生成了对应的哈夫曼编码
Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
System.out.println(huffmanCodes);
//测试生成的哈夫曼编码数据
byte[] huffmanCodeBytes = zip(contentBytes, huffmanCodes);
System.out.println(Arrays.toString(huffmanCodeBytes));
//发送huffmanCodeBytes 数组
*/
}
//编写方法,将文件解压
//zipFile:传入希望解缩的文件路径,dstFile:解缩后将文件放入哪个目录
public static void unZipFile(String zipFile,String dstFile) {
//创建文件输入流
InputStream is = null;
//创建对象输入流
ObjectInputStream ois = null;
//创建文件输出流
OutputStream os = null;
try {
//创建文件输入流
is = new FileInputStream(zipFile);
//创建和文件输入流关联的对象输入流
ois = new ObjectInputStream(is);
//读取byte数组
byte[] huffmanBytes =(byte[])ois.readObject();
//读取哈夫曼编码表
Map<Byte, String> huffmanCodes =(Map<Byte, String>)ois.readObject();
//解码
byte[] bytes = decode(huffmanCodes, huffmanBytes);
//将bytes数组写入到目标文件
os = new FileOutputStream(dstFile);
//写数据到dstFile中
os.write(bytes);
} catch (Exception e) {
System.out.println(e.getMessage());
}finally {
try {
os.close();
ois.close();
is.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
//编写方法,将文件进行压缩
//srcFile:传入希望压缩的文件路径,dstFile:压缩后将文件放入哪个目录
public static void zipFile(String srcFile,String dstFile) {
//创建文件输入流
FileInputStream is = null;
ObjectOutputStream oos = null;
//创建文件输出流
OutputStream os = null;
try {
is = new FileInputStream(srcFile);
//创建一个和源文件大小一样的byte[]
byte[] b = new byte[is.available()];
//读取文件
is.read(b);
//对源文件进行压缩
byte[] huffmanBytes = huffmanZip(b);
//输出流存放压缩文件
os = new FileOutputStream(dstFile);
//创建和文件输出流关联的ObjectOutputStream
oos = new ObjectOutputStream(os);
//把哈夫曼编码后的字节数组写入压缩文件
oos.write(huffmanBytes);
//以对象流的方式写入哈夫曼编码,为了以后恢复源文件使用
oos.writeObject(huffmanCodes);
} catch (Exception e) {
System.out.println(e.getMessage());
}finally {
try {
is.close();
oos.close();
os.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
//完成数据的解压
//编写方法,完成对压缩数据的解码
//huffmanCodes:哈夫曼编码表map,huffmanBytes:哈夫曼编码得到的字节数组,返回原来字符串对应的数组
private static byte[] decode(Map<Byte, String> huffmanCodes,byte[] huffmanBytes) {
//得到huffmanBytes对应的二进制字符串
StringBuilder stringBuilder = new StringBuilder();
//将byte数组转成二进制字符串
for(int i = 0;i < huffmanBytes.length;i++) {
byte b = huffmanBytes[i];
//判断是不是最后一个字节
boolean flag = (i == huffmanBytes.length - 1);
stringBuilder.append(byteToBitString(!flag, b));
}
//把字符串安装指定的哈夫曼编码进行解码
//把哈夫曼编码表进行调换(反向查询a->100 100->a)
Map<String,Byte> map = new HashMap<String,Byte>();
for(Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
//创建集合,存放byte
List<Byte> list = new ArrayList<>();
for(int i = 0;i < stringBuilder.length();){
int count = 1;//计数器
boolean flag = true;
Byte b = null;
while(flag) {
//取出key
String key = stringBuilder.substring(i,i+count);//i不动,count移动,直到匹配到一个字符
b = map.get(key);
if (b == null) {//还没匹配到
count++;
}else {//匹配到
flag = false;
}
}
list.add(b);
i += count;//让i移动到count位置
}
//当for循环结束后,list就存放了所有字符
//把list中的数据放入到byte[]并返回
byte[] b = new byte[list.size()];
for(int i = 0;i < b.length;i++) {
b[i] = list.get(i);
}
return b;
}
//编写方法,将byte转成一个二进制的字符串
//flag标志是否需要补高位,如果true,表示需要补高位,最后一个字节无需补高位,返回byte数组对应的二进制字符串(补码)
private static String byteToBitString(boolean flag,byte b) {
//使用变量保存b
int temp = b;//将b转成int
//如果是正数,存在补高位的问题
if (flag) {
temp |= 256;//按位或256 1 0000 0000 | 000 0001 =>1 0000 001
}
String str = Integer.toBinaryString(temp);//返回的是temp对应的二进制补码
if (flag) {
return str.substring(str.length() - 8);
}else {
return str;
}
}
//编写方法,将前面方法封装起来,便于调用
//bytes:原始字符串对应的字节数组,返回经过哈夫曼编码处理后的字节数组
private static byte[] huffmanZip(byte[] bytes) {
List<Node> nodes = getNodes(bytes);
//根据nodes创建哈夫曼树
Node huffmanTreeRoot = createHuffmanTree(nodes);
//根据哈夫曼树生成对应的哈夫曼编码
Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
//根据生成的哈夫曼编码,压缩后,得到哈夫曼编码字节数组
byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
return huffmanCodeBytes;
}
//编写方法,将字符串对应的byte[]数组,通过哈夫曼编码表,返回一个哈夫曼编码压缩后的byte[]
//bytes:字符串对应的byte[],huffmanCodes:生成的哈夫曼编码map
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));
}
//将"1010100..."转成byte[]
//统计返回byte[] huffmanCodeBytes长度
int len;
if (stringBuilder.length() % 8 == 0) {
len = stringBuilder.length() / 8;
}else {
len = stringBuilder.length() / 8 + 1;
}
//或者:int len = (stringBuilder.length()+7)/8;
//创建 存储压缩后的byte数组
byte[] huffmanCodeBytes = new byte[len];
int index = 0;//记录第几个byte
for(int i = 0;i < stringBuilder.length();i += 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<Byte,String> 形式为:32->01,97->100...
static Map<Byte, String> huffmanCodes = new HashMap<>();
//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);
//处理右子树
getCodes(root.right,"1",stringBuilder);
return huffmanCodes;
}
//将传入的node结点的所有叶子节点的哈夫曼编码得到,并放入到huffmanCodes集合
//code:传入结点,code:路径:左子节点为0,右子节点为1,stringBuilder:拼接路径
private static void getCodes(Node node,String code,StringBuilder stringBuilder) {
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
//将code加入到stringBuilder2
stringBuilder2.append(code);
if (code != null) {//code==null 不处理
//判断是叶子节点还是非叶子节点
if (node.data == null) {//非叶子节点
//递归处理
//向左递归
getCodes(node.left, "0", stringBuilder2);
//向右递归
getCodes(node.right, "1", stringBuilder2);
}else {//叶子节点
huffmanCodes.put(node.data, stringBuilder2.toString());
}
}
}
//接收字节数组,返回List,形式为:[Node[data=97,weight=5],Node[data=32,weight=9]....]
private static List<Node> getNodes(byte[] bytes){
//创建ArrayList
ArrayList<Node> nodes = new ArrayList<>();
//遍历bytes,统计 每一个byte出现的次数-->map[key,value]
Map<Byte, Integer> counts = new HashMap<>();
for(byte b : bytes) {
Integer count = counts.get(b);
if (count == null) {//Map还没有这个字符数据
counts.put(b, 1);
}else {
counts.put(b, count+1);
}
}
//把每个键值对转成一个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 parent = new Node(null,leftNode.weight + rightNode.weight);
parent.left = leftNode;
parent.right = rightNode;
//删除处理过的二叉树
nodes.remove(leftNode);
nodes.remove(rightNode);
//将parent加入到nodes
nodes.add(parent);
//重新排序
Collections.sort(nodes);
}
//返回哈夫曼树的root结点
return nodes.get(0);
}
//前序遍历的方法
public static void PreOrder(Node root) {
if (root != null) {
root.preOrder();
}else {
System.out.println("空树,无法遍历");
}
}
}
//创建Node
class Node implements Comparable<Node>{
Byte data;//存放数据本身,如'a'=>97,' '=>32
int weight;//权值(字符出现的次数)
Node left;
Node right;
public Node(Byte data, int weight) {
super();
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();
}
}
}