文章目录
一、基本概念:
赫夫曼树几个重要概念和举例说明:
- 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为 L-1。
- 结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积 。
- 树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为 WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。
- WPL 最小的就是赫夫曼树。
二、创建赫夫曼树:
1.思路:
给你一个数列 {13, 7, 8, 3, 29, 6, 1},要求转成一颗赫夫曼树。
构成赫夫曼树的步骤:
- 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
- 取出根节点权值最小的两颗二叉树
- 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
- 再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数 据都被处理,就得到一颗赫夫曼树
2.代码实现:
package huffmantree;
/**
* 创建赫夫曼树
*/
import java.util.ArrayList;
import java.util.Collections;
public class HuffmanTree {
public static void main(String[] args) {
int [] arr = {13,7,8,3,29,6,1};
Node nodeRoot = createHuffmanTree(arr);
preOrder(nodeRoot);
}
/**
*
* @param arr 需要创建成赫夫曼树的数组
* @return 创建好后的赫夫曼树的root节点
*/
public static Node createHuffmanTree(int[] arr){
// 第一步为了操作方便
// 1. 遍历 arr 数组
// 2. 将 arr 的每个元素构成成一个 Node
// 3. 将 Node 放入到 ArrayList 中
ArrayList<Node> nodesList = new ArrayList<>();
for(int value : arr){
nodesList.add(new Node(value));
}
// System.out.println("node=" + nodesList);
while (nodesList.size() > 1){
Collections.sort(nodesList);
Node leftNode = nodesList.get(0);
Node rightNode = nodesList.get(1);
Node parentNode = new Node(leftNode.value + rightNode.value);
parentNode.left = leftNode;
parentNode.right = rightNode;
nodesList.remove(leftNode);
nodesList.remove(rightNode);
nodesList.add(parentNode);
}
return nodesList.get(0);
}
public static void preOrder(Node root){
if (root != null){
root.preOrder();
}else{
System.out.println("the tree is empty! can't list!");
}
}
}
// 创建结点类
// 为了让 Node 对象实现排序, Collections 集合排序 .让 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.基本原理:
- 赫夫曼编码也翻译为 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式, 属于一种程序算法
- 赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。
- 赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在 20%~90%之间
- 赫夫曼码是可变字长编码(VLC)的一种。Huffman 于 1952 年提出一种编码方法,称之为最佳编码
此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性,赫夫曼编码是无损处理方案。
注意:这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是 wpl 是 一样的,都是最小的, 最后生成的赫夫曼编码的长度是一样。
2.案例:数据压缩
(1)创建赫夫曼树:
package huffmancode;
import java.util.*;
import static java.util.Collections.*;
/**
* 赫夫曼编码
*/
public class HuffmanEncode {
public static void main(String[] args) {
String str = "i like like like java do you like a java";
byte[] bytes = str.getBytes();
List<Node> nodesList = getNode(bytes); //获得每个字符及其对应的权值
System.out.println(nodesList); //查看每个字符及其所对应的权值
Node nodeRoot = createHuffmanEncode(nodesList);//创建赫夫曼树,并返回根节点
preOrder(nodeRoot); //遍历验证
}
/**
* 就是要拿到所给字符串中的每个字符及其出现的次数,次数作为权值,进而构建赫夫曼树
* @param bytes 接收字节数组
* @return 返回的是list形式 【Ndoe[data= , weight= ]】
*/
public static List<Node> getNode(byte[] bytes){
ArrayList<Node> nodeList = new ArrayList<Node>();
HashMap<Byte, Integer> map = new HashMap<>();
for (byte b : bytes){
Integer count = map.get(b);
if (count == null){//表示b还没有被添加进来map中,进行添加
map.put(b,1);
}else{
map.put(b,count+1);
}
}
//遍历map,把每一个键值对转换成一个Node对象,并加入到nodeList
for (Map.Entry<Byte,Integer> entry : map.entrySet()) {
nodeList.add(new Node(entry.getKey(),entry.getValue()));
}
return nodeList;
}
//通过nodeList创建对应的赫夫曼树
public static Node createHuffmanEncode(List<Node> list){
while (list.size() > 1){
Collections.sort(list);
Node leftNode = list.get(0);
Node rightNode = list.get(1);
Node parentNode = new Node(null, leftNode.weight + rightNode.weight);
parentNode.left = leftNode;
parentNode.right = rightNode;
list.remove(leftNode);
list.remove(rightNode);
list.add(parentNode);
}
return list.get(0);
}
public static void preOrder(Node root){
if (root != null){
root.preOrder();
}else {
System.out.println("tree is empty! can't list!");
}
}
}
class Node implements Comparable<Node>{
int weight;
Node left;
Node right;
Byte data; //存放数据(字符)本身
public Node(Byte data,int weight) {
this.weight = weight;
this.data = data;
}
@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)生成赫夫曼编码和赫夫曼编码后的数据:
//**************************************************************
//将赫夫曼编码存放在map中
static Map<Byte, String> huffmanCodes = new HashMap<>();
//生成赫夫曼编码表示,需要去拼接路径,使用StringBuilder 去存储某个叶子结点的路径
static StringBuilder stringBuilder = new StringBuilder();
/**
* 将传入的node节点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合中
* @param node 传入节点
* @param code 路径:向左子节点定义为0, 向右子节点定义为1
* @param stringBuilder 用于拼接路径
*/
public static void getCodes(Node node, String code, StringBuilder stringBuilder){
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
stringBuilder2.append(code);//将code加入到stringBuilder2中
if (node != null){
//判断当前节点是叶子结点还是非叶子节点
if (node.data == null){//表示是非叶子节点,递归处理
getCodes(node.left,"0",stringBuilder2);
getCodes(node.right,"1",stringBuilder2);
}else {//说明是叶子结点,即找到了最后
huffmanCodes.put(node.data,stringBuilder2.toString());
}
}
}
//重载getCodes方法,只传入根节点即可,然后将huffmanCodes集合返回
public static Map<Byte,String> getCodes(Node nodeRoot){
if (nodeRoot != null){
getCodes(nodeRoot.left,"0",stringBuilder);
getCodes(nodeRoot.right,"1",stringBuilder);
}else{
return null;
}
return huffmanCodes;
}
//************************************************************
//**********************************************************
//将整个过程封装为一个方法
public static byte[] huffmanEncode(String str){
byte[] bytes = str.getBytes();
List<Node> nodesList = getNode(bytes); //获得每个字符及其对应的权值
Node nodeRoot = createHuffmanEncode(nodesList);//创建赫夫曼树,并返回根节点
Map<Byte, String> mapCodes = getCodes(nodeRoot); //得到赫夫曼编码集
byte[] huffmanEncodeBytes = encode(bytes, mapCodes); //返回赫夫曼编码的结果
return huffmanEncodeBytes;
}
//**********************************************************
/**
* 进行编码, 返回字节数组
* @param bytes
* @param huffmanCodes
* @return
*/
public static byte[] encode(byte[] bytes, Map huffmanCodes){
StringBuilder stringBuilder = new StringBuilder();
for (byte b : bytes) {
stringBuilder.append(huffmanCodes.get(b));
}
//将stringBuilder存储的编码字符串转换为每8位一组的byte数组
int length = (stringBuilder.length() + 7) / 8;
byte[] huffmanCodeBytes = new byte[length];
int index = 0;
for (int i = 0; i < stringBuilder.length(); i+=8){
String substring;
if (i+8 > stringBuilder.length()){//表示不够8位的情况
substring = stringBuilder.substring(i);
}else {
substring = stringBuilder.substring(i, i+8);
}
huffmanCodeBytes[index] = (byte) Integer.parseInt(substring,2);
index++;
}
return huffmanCodeBytes;
}
(3)解码:
解码过程存在一定问题,解码png图片会出现数组越界的错误。
//*********************************************************
//解码过程:
public static String decode(Map<Byte,String> huffmanCodes, byte[] huffmanCodeBytes){
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < huffmanCodeBytes.length; i++){
byte b = huffmanCodeBytes[i];
//判断是不是最后一个字节
boolean flag = ( i == huffmanCodeBytes.length -1);
stringBuilder.append(byteToString(!flag,b));
}
//将赫夫曼编码表进行调换,因为是反向查询
HashMap<String, Byte> map = new HashMap<>();
for (Map.Entry<Byte,String> entry: huffmanCodes.entrySet()) {
map.put(entry.getValue(),entry.getKey());
}
//创建集合,存放byte
ArrayList<Byte> list = new ArrayList<>();
for (int i = 0; i < stringBuilder.length();){
int count = 1;
boolean flag = true;
Byte b = null;
while (flag){
String key = stringBuilder.substring(i, i + count);
b = map.get(key);
if (b == null){
count++;
}else {
flag = false;
}
}
list.add(b);
i+=count;//移动i到count的位置
}
byte[] b = new byte[list.size()];
for (int i = 0; i < b.length; i++){
b[i] = list.get(i);
}
String decodeString = new String(b);
return decodeString;
}
/**
*将一个 byte 转成一个二进制的字符串。
* @param flag 标志是否需要补高位如果是 true ,表示需要补高位,如果是 false 表示不补, 如果是最后一个 字节,无需补高位
* @param b b 传入的 byte
* @return 是该 b 对应的二进制的字符串,(注意是按补码返回)
*/
public static String byteToString(boolean flag, byte b){
int temp = b;
if (flag){
temp |= 256;
}
String str = Integer.toBinaryString(temp);
if (flag){
return str.substring(str.length()-8);
}else {
return str;
}
}
(4)编码压缩文件:
//**************************************************************
//编写一个方法进行文件压缩
/**
*
* @param srcFile 要压缩文件的路径
* @param destFile 压缩文件存放的目录
*/
public static void zipFile(String srcFile, String destFile) {
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
ObjectOutputStream os = null;
try {
inputStream = new FileInputStream(srcFile);
//创建一个和源文件一样大小的byte[]
byte [] b = new byte[inputStream.available()];
inputStream.read(b);
//对源文件进行压缩
byte[] encodeBytes = huffmanEncode(b);
//创建输出流,存放压缩文件
outputStream = new FileOutputStream(destFile);
//创建一个和输出流关联的ObjectOutputStream
os = new ObjectOutputStream(outputStream);
//以对象流的方式写入 编码结果(编码后的字节数组),为了以后恢复文件时使用
os.writeObject(encodeBytes);
//注意:一定要将赫夫曼编码 写入压缩文件,不然无法恢复
os.writeObject(huffmanCodes);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (os != null){
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outputStream != null){
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//文件解压缩
/**
*
* @param zipFilePath 要解压缩文件的目录
* @param destFilePath 解压缩完存放的目录
*/
public static void unZipFile(String zipFilePath, String destFilePath) {
FileInputStream is = null;
ObjectInputStream ois = null;
FileOutputStream os = null;
try {
is = new FileInputStream(zipFilePath);
ois = new ObjectInputStream(is);
//读取byte数组,编码后的huffmanBytes
byte[] huffmanBytes = (byte[])ois.readObject();
Map<Byte,String> huffmanCodes = (Map<Byte,String>)ois.readObject();
//解码
byte[] decodeBytes = decode(huffmanCodes, huffmanBytes);
//将decodeBytes 写入到 目标文件中
os = new FileOutputStream(destFilePath);
os.write(decodeBytes);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (os != null){
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (ois != null){
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (is != null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}