赫夫曼树是一种带权路径最短的二叉树。带权路径:根节点到所有叶子结点所需路径*结点权值之和。通常路径即为结点所在层数之差,所以权值越大结点离根结点越近。
赫夫曼树构建思路:
1,将数据按照权值从小到大顺序排列,每个数据结点都看作一个二叉树;
2,分别取出权值最小的两个二叉树组成新树,其权值为前两者之和;
3,再将新树加入排列中,重复1-2步骤,直到所有结点都被处理。
注意:所有的数据都存在了叶子结点上。
现使用java实现赫夫曼树的创建。
package com.lsk.tree;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/*
* 赫夫曼数的构建思路:
* 定义:带有权值路劲之和最小的二叉树即为赫夫曼树
* 思路:1,将节点排序,选出权值最小的两个的节点;
* 2,将权值之和作为新的父节点,组成了一棵新的二叉树
* 3,再将新的二叉树加入权值排序,继续组件新的二叉树;
* 4,重复1、2、3;直到所有节点都被处理组建成一个根结点时*/
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("该树为空!");
}
}
//创建赫夫曼树
public static Node createhuffmanTree(int[] arr) {
// arr的每一个元素即为一个node节点
// 将node放入list中以便使用sort方法
List<Node> node = new ArrayList<>();
for (int weight : arr) {
node.add(new Node(weight));
}
while(node.size()>1)
{
Collections.sort(node);
Node leftNode = node.get(0); //较小的位于左子树
Node rightNode = node.get(1);
Node parent = new Node(leftNode.weight + rightNode.weight); //组建新树
parent.left = leftNode; //分别链接到左右子树
parent.right = rightNode;
node.add(parent); //加入节点中重新排序
node.remove(leftNode);
node.remove(rightNode);
}
//返回赫夫曼树
return node.get(0);
}
}
// 节点类 为了让node结点能实现排序功能,这里继承Comparable
class Node implements Comparable<Node> {
Node left; // 左子树
Node right;// 右子树
byte data; // 存放数据
int weight;// 权值
public Node(int weight) {
super();
this.weight = weight;
}
public Node(byte data, int weight) {
this.data = data;
this.weight = weight;
}
// 前序遍历结点
public void preOrder() {
System.out.println(this);
if(this.left!=null) {
this.left.preOrder();
}
if(this.right!=null)
{
this.right.preOrder();
}
}
@Override
public String toString() {
return "Node [data=" + data + ", weight=" + weight + "]";
}
@Override
public int compareTo(Node o) {
// 实现从小到大排序
return this.weight - o.weight;
}
}
赫夫曼树的重要应用就是通信领域中的赫夫曼编码了。
定长编码:比如在传输中,我们需要对字符串"i like like like java do you like a java"
进行编码,首先会根据ASCII编码表,将i转换为105,再转换为对应的二进制0110 1001
;然后依次进行类似的操作。
变长编码:对字符串中出现的字符进行次数统计,频次最高的使用最短的0进行编码,然后是1,10,11,...
;比如0=' ',1 = a,10='i',11='e'......
然后上面的字符串编码就是10010110...
,但这样简单的处理会出现多义性问题。
因此,我们需要做到前缀编码,即一个编码对应唯一的字符。
赫夫曼编码:
基于赫夫曼树,以左0右1遍历结点的结果组合,作为结点的字符编码。由于结点的位置不同,每一个字符的编码都将是唯一的。
下面用赫夫曼编码实现对数据的压缩和解压。
压缩思路:
1,统计待压缩字符串各字符出现的次数,把次数作为他们的权值,封装成node结点;
2,由node结点构造赫夫曼树。
3,然后以Map
的形式存储赫夫曼编码表,即每一个字符对应一个二进制的字符串; 4,然后把字符串拼接起来每8位一截取(用于发送),即是压缩后的字符形式了;
解压思路:
1,将压缩后的编码形式转换成二进制,
2,再逐一遍历二进制字符串和赫夫曼编码表匹配,并将匹配的的放入字节数组中。
public static void main(String[] args) {
//准备测试字符串数据
String content = "i like like like java do you like a java";
//将字符串转为字节数组形式
byte[] bytes = content.getBytes();
//1,将统计的数据和权值转换成对node
List<Node> nodes = getNodes(bytes);
//2,根据结点创建赫夫曼树,返回一个根结点
Node huffmanTreeRoot = createHuffmanTree(nodes);
//3,根据赫夫曼树创建赫夫曼编码表
huffmanCodes = getCodes(huffmanTreeRoot);
//4,压缩方法,返回压缩后的字节数组,长度17。可见本例中压缩率为23/40=57.5%
byte[] bytesCodes = huffmanZip(bytes);
//5,解压方法,返回原始字符串
byte[] originalBytes = decode(huffmanCodes, bytesCodes);
System.out.println(new String(originalBytes));
}
/**
* 遍历字节数组,将字符出现次数作为对应权值存入hashMap并转换为Node结点对象
*
* @param bytes 传入的字节数组
* @return 返回一个已经转换好的Node结点数组
*/
public static List<Node> getNodes(byte[] bytes) {
List<Node> nodes = new ArrayList<Node>();
Map<Byte, Integer> elementMap = new HashMap<>();
// 遍历字节数组拿到数据,统计每个字符出现的次数作为权值
for (byte b : bytes) {
// 实质上将遍历得到的每个字节当做key
Integer count = elementMap.get(b);
// 没有即存放,此时value为1
if (count == null) {
elementMap.put(b, 1);
} else {
elementMap.put(b, count + 1);
}
}
// 再将map转为node对象:key——>data;value——>weight
// map的遍历
for (Map.Entry<Byte, Integer> entry : elementMap.entrySet()) {
nodes.add(new Node(entry.getKey(), entry.getValue()));
}
return nodes;
}
/**
* 创建赫夫曼树
*
* @param nodes 传入的node数组
* @return 返回一个赫夫曼树根结点
*/
public static Node createHuffmanTree(List<Node> nodes) {
while (nodes.size() > 1) {
Collections.sort(nodes);
Node leftNode = nodes.get(0); // 较小的位于左子树
Node rightNode = nodes.get(1);
Node parent = new Node(null, leftNode.weight + rightNode.weight); // 组建新树
parent.left = leftNode; // 分别链接到左右子树
parent.right = rightNode;
nodes.add(parent); // 加入节点中重新排序
nodes.remove(leftNode);
nodes.remove(rightNode);
}
// 返回赫夫曼树根结点
return nodes.get(0);
}
/**
* 由赫夫曼树生成对应的赫夫曼编码表
* 思路:1->赫夫曼编码表存放在Map中
* 2->形如:{32(' ')=01, 97('a')=100, 100=11000, 117=11001......
* 3->左子结点是0,右子结点是1,string一个个拼接而成(使用stringBuilder)
*/
//1,定义一个静态map结构的huffmanCodes
static Map<Byte, String> huffmanCodes = new HashMap<>();
//2,定义一个stringBuilder,用于存放叶子结点的路径
static StringBuilder stringBuilder = new StringBuilder();
/**
* 根据赫夫曼树构建赫夫曼编码表的具体算法:
* 这是一个递归过程,将传入赫夫曼根节点的赫夫曼树生成对应的赫夫曼编码表
*
* @param node 待传入的赫夫曼树根节点
* @param code 拼接规则:左子结点是0,右子结点是1
* @param stringBuilder 拼接后的路径
*/
public static void getCodes(Node node, String code, StringBuilder stringBuilder) {
StringBuilder strBuilder = new StringBuilder(stringBuilder);
//将code拼接到strBuilder
strBuilder.append(code);
if (node != null) {
//非叶子结点
if (node.data == null) {
//向左递归
getCodes(node.left, "0", strBuilder);
//向右递归
getCodes(node.right, "1", strBuilder);
} else {
//遍历到了叶子结点,则存放入huffmanCodes中
huffmanCodes.put(node.data, strBuilder.toString());
}
}
}
/**
* 为了调用方便,对getCodes进行重载,直接返回huffmanCodes
*
* @param node 传入的赫夫曼树根结点
* @return 返回一个map结构的赫夫曼编码表
*/
public static Map<Byte, String> getCodes(Node node) {
if (node == null) {
return null;
} else {
//处理root的左子树
getCodes(node.left, "0", stringBuilder);
//处理root的右子树
getCodes(node.right, "1", stringBuilder);
return huffmanCodes;
}
}
/**
* 按照赫夫曼编码表规则将原始数据压缩成字节数组
*
* @param bytes
* @param huffmanCodes
* @return
*/
public static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
//将bytes数组按照赫夫曼编码规则拼接成一个二进制串
StringBuilder stringBuilder = new StringBuilder();
for (byte b : bytes) {
stringBuilder.append(huffmanCodes.get(b));
}
System.out.println(stringBuilder.toString());
//定义赫夫曼压缩的字节数组的长度
int len;
if (stringBuilder.length() % 8 == 0) {
len = stringBuilder.length() / 8;
} else {
len = stringBuilder.length() / 8 + 1;
}
//定义赫夫曼压缩的字节数组
byte[] huffmanCodeBytes = new byte[len];
int index = 0;
//对字符串8个一截取,并放入对应压缩后的字节数组中
for (int i = 0; i < stringBuilder.length(); i += 8) {
//用于装载每次截取的字符串
String str;
if (i + 8 > stringBuilder.length()) {
//某一个不够8位时
str = stringBuilder.substring(i); //此时截取起始位置到末尾
} else {
str = stringBuilder.substring(i, i + 8);
}
//将String字符数组转换为byte
/* 细节:以第一个8位字符串"10101000"为例(i l ' '->101 01 000)
将它强转为byte,溢出后转换成了负数求补码,所以输出是-88*/
huffmanCodeBytes[index] = (byte) Integer.parseInt(str, 2);
index++;
}
return huffmanCodeBytes;
}
/**
* 封装:将原始字节数组转换为赫夫曼编码的字节数组
*
* @param bytes 压缩前数组
* @return 返回压缩后数组
*/
public static byte[] huffmanZip(byte[] bytes) {
List<Node> nodes = getNodes(bytes);
Node huffmanTreeRoot = createHuffmanTree(nodes);
getCodes(huffmanTreeRoot);
//测试赫夫曼编码
for(Byte key : huffmanCodes.keySet()){
System.out.println("key:"+key+"--->"+huffmanCodes.get(key));
}
byte[] bytesCodes = zip(bytes, huffmanCodes);
return bytesCodes;
}
/**
* 将一个字节转换成二进制字符串形式。
* 截取:因为返回的int32位的补码形式,所以需要截取8位
* 补位:正数不足8位时截取会造成越界异常,需要补高位,比如1,补成1 0000 0001
* 判断:都是按8位处理的,但最后一位不需要截取和补位(可能就位数不足),比如28,1110,补成0000 1110,会造成多义性
* @param flag 判断是否需要补高位(对正数,对最后一个不需要)
* @param b 传入字节
* @return
*/
public static String byteToBitString(boolean flag,byte b){
int temp = b;
if(flag){
//不足8位时,和1 0000 0000 与运算,可以补齐前面的0
temp |= 256;
}
String str = Integer.toBinaryString(temp);
if(flag){
//只需要截取后面的8位即可
return str.substring(str.length()-8);
}else{
return str;
}
}
/**
* @param huffManCodes 赫夫曼编码表
* @param huffManBytes 赫夫曼编码的字节数组
* @return
*/
public static byte[] decode(Map<Byte,String> huffManCodes,byte[] huffManBytes){
//1,首先将编码的字节数组转换为二进制串
//2,遍历map,根据匹配转换成每个字节
//3,以字节数组的形式返回
StringBuilder stringBuilder = new StringBuilder();
for(int i =0;i<huffManBytes.length;i++){
byte b = huffManBytes[i];
//判断是否是最后一个字符,因为他可能位数不足
boolean flag = (i==huffManBytes.length-1);
stringBuilder.append(byteToBitString(!flag,b));
}
System.out.println(stringBuilder.toString());
//实现map翻转
Map<String,Byte> reverseCodeMap = new HashMap<>();
for(Map.Entry<Byte,String> entry : huffManCodes.entrySet()){
reverseCodeMap.put(entry.getValue(),entry.getKey());
}
//存放匹配的字节数组
List<Byte> byteList = new ArrayList<>();
for(int i = 0;i<stringBuilder.length();){
int count = 1; //第二指针,i不动,count移位
boolean flag = true; //是否匹配的标志
Byte b = null; //记录匹配对应的字节
while(flag){
String s = stringBuilder.substring(i, i + count);
b = reverseCodeMap.get(s);
if(b==null){
count++; //没有匹配的继续移动指针
}else {
flag = false;
}
}
byteList.add(b);
i += count; //每次匹配在上次结束的地方开始
}
//list数组转换为byte数组
byte[] originalBytes = new byte[byteList.size()];
for(int i = 0;i<byteList.size();i++){
originalBytes[i] = byteList.get(i);
}
return originalBytes;
}