文章目录
1、赫夫曼树及相关概念介绍
2、赫夫曼树的创建
1、创建赫夫曼树图解
问题描述:
思路分析:
依次取出权值最小结点和次小结点,将其和作为新的结点并删除原来的结点,直至整个数列中只留下一个元素
2、代码实现
Node.class
/**
* @author zhihua.li
* @date 2021/2/22 - 22:33
**/
/**
*@program: DataStructure
*@description: 赫夫曼树单个节点
*@author: zhihua li
*@create: 2021-02-22 22:33
*/
public class Node implements Comparable<Node> {
private Byte data; //存放字符本身
private int weight; //存放某字符的权重,表示字符出现的频率
private Node left;
private Node right;
public Node(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
// 先序遍历结点
public void preList(){
System.out.println(this);
if(this.left!=null){
this.left.preList();
}
if(this.right!=null){
this.right.preList();
}
}
@Override
public int compareTo(Node o) {
return this.weight - o.weight; //从小到大排序
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public Node getLeft() {
return left;
}
public void setLeft(Node left) {
this.left = left;
}
public Node getRight() {
return right;
}
public void setRight(Node right) {
this.right = right;
}
public Byte getData() {
return data;
}
public void setData(Byte data) {
this.data = data;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
}
HuffmanTree.class
/**
* @author zhihua.li
* @date 2021/2/21 - 22:23
**/
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
*@program: DataStructure
*@description: 赫夫曼树结构
*@author: zhihua li
*@create: 2021-02-21 22:23
*/
public class HuffmanTree {
// 创建赫夫曼树的方法,返回赫夫曼树的根节点
private Node createHuffmanTree(int[] arr){
List<Node> nodes = new ArrayList<>();
for (int i : arr) {
// 遍历arr数组,并将arr数组中的每一个值作为一个节点并添加到Node集合中
nodes.add(new Node(i));
}
/*
* 注意:必须在实现了Comparable接口的类才能通过Collections.sort()对其进行比较
*/
while(nodes.size()>1) {
// 从小到大排序
Collections.sort(nodes);
// 选出根结点权值最小的两颗二叉树
// 最小
Node leftNode = nodes.get(0);
// 次小
Node rightNode = nodes.get(1);
// 构建新的二叉树结点
Node parent = new Node(leftNode.getValue() + rightNode.getValue());
// 为新的二叉树结点设置左右子节点
parent.setLeft(leftNode);
parent.setRight(rightNode);
// 删除原Node集合中的两结点
nodes.remove(leftNode);
nodes.remove(rightNode);
// 添加由最小和次小结点组成的新结点
nodes.add(parent);
}
return nodes.get(0);
}
}
3、注意
在使用Collections工具类提供的sort()方法时:
- 若类本身就实现了Comparable接口(如Integer)则直接使用sort()即可
- 若类本身没有实现,则需要事先对其排序方式进行规定,即在实体类中实现Comparable接口
如本例中在Node.class中:
public class Node implements Comparable<Node> {
@Override
public int compareTo(Node o) {
return this.weight - o.weight; //从小到大排序
}
}
4、代码测试
通过上面的代码,可以实现根据给定数组创建一棵赫夫曼树,而上面创建的方法返回值为赫夫曼树的根节点
可以通过对该节点进行先序遍历查看整个树结构
@Test
public void test(){
int[] arr = {13,7,8,3,29,6,1};
Node huffmanTree = createHuffmanTree(arr);
huffmanTree.preList();
}
测试结果:
3、赫夫曼编码
1、基本介绍
赫夫曼编码适用于完成内容重复率高的数据压缩
2、效率比较
1、定长编码
将每一个字符都按照ASCII码转化为二进制数后进行保存
2、变长编码
由定长编码到赫夫曼编码的过渡阶段
3、赫夫曼编码
按照内容出现的频率对其进行无二义性的前缀编码
注意:赫夫曼树根据排序方法不同,可能对应多种赫夫曼编码,但不同赫夫曼编码的WPL相同,均为最小值
3、利用赫夫曼编码压缩数据的代码实现
1、思路分析
- 实现赫夫曼编码的前提是得到数据对应的赫夫曼树
- 根据赫夫曼树,规定向左路径为0,向右路径为1,得到每个结点对应的路径
- 根据上述规则,可得赫夫曼编码,且为无损压缩(由于赫夫曼树的特性,频率高的离根结点近,频率低的离根结点远)
- 需要对单个节点增加权值属性,作为比较节点大小的依据
@Override
public int compareTo(Node o) {
return this.weight - o.weight; //从小到大排序
}
2、单个结点结构
Node.class
/**
* @author zhihua.li
* @date 2021/2/22 - 22:33
**/
/**
*@program: DataStructure
*@description: 赫夫曼树单个节点
*@author: zhihua li
*@create: 2021-02-22 22:33
*/
public class Node implements Comparable<Node> {
private Byte data; //存放字符本身
private int weight; //存放某字符的权重,表示字符出现的频率
private Node left;
private Node right;
public Node(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
// 先序遍历结点
public void preList(){
System.out.println(this);
if(this.left!=null){
this.left.preList();
}
if(this.right!=null){
this.right.preList();
}
}
@Override
public int compareTo(Node o) {
return this.weight - o.weight; //从小到大排序
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public Node getLeft() {
return left;
}
public void setLeft(Node left) {
this.left = left;
}
public Node getRight() {
return right;
}
public void setRight(Node right) {
this.right = right;
}
public Byte getData() {
return data;
}
public void setData(Byte data) {
this.data = data;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
}
3、将字符数组转换为赫夫曼树的结点
首先将要获取赫夫曼编码的字符串转换为字符数组
将字符数组中的每个字符保存为单个Node结点对象中的Data属性
/**
* @param temp
* @Method getNodes
* @Author zhihua.Li
* @Version 1.0
* @Description 将字符数组中的元素转换为Node对象
* 将准备构建的赫夫曼树的Node结点放到List中,通过List创建对应的赫夫曼树
* @Return java.util.List<Node>
* @Exception
* @Date 2021/2/23 10:07
*/
private List<Node> getNodes(byte[] temp) {
// 创建一个ArrayList用来保存Node集合
ArrayList<Node> nodes = new ArrayList<>();
// 创建一个HashMap用来接收所给字符数组,并返回对应字符出现的频次即权重
Map<Byte, Integer> map = new HashMap<>();
// 接收字符出现的次数
for (byte each : temp) {
// 遍历字符数组
// 获取字符出现的次数,若在map中不曾出现当前字符,则为map中新建一个key并设置初值为1
// 否则给其原来的key对应的value+1
// ***注意***:map中的key具有唯一性,后面的会覆盖前面的
Integer count = map.get(each);
if (count == null) {
map.put(each, 1);
} else {
map.put(each, count + 1);
}
}
// 将传入字符串中的所有字符及其出现次数全部录入map中
// 将map中的k-v转换为Node结点:
// k即字符,即为Node结点的data属性
// v即出现次数,可作为Node结点的weight属性
for (Map.Entry<Byte, Integer> entry : map.entrySet()) {
nodes.add(new Node(entry.getKey(), entry.getValue()));
}
return nodes;
}
注意:对Map进行遍历时,使用下列语句
for(Map.Entry<Byte,Integer> entry:map:entrySet())
{
do...
}
测试:
@Test
public void test() {
String content = "i like like like java do you like a java";
byte[] bytes = content.getBytes();
List<Node> nodes = getNodes(bytes);
for (Node node : nodes) {
System.out.println(node);
}
结果为:
4、创建赫夫曼树
根据上一步产生的结点集合创建对应的赫夫曼树
注意:此处生成的赫夫曼树结点只有weight而没有data
/**
* @param nodes
* @Method createHuffmanTree
* @Author zhihua.Li
* @Version 1.0
* @Description 创建赫夫曼树结点,返回赫夫曼树的根结点
* @Return Node
* @Exception
* @Date 2021/2/23 10:07
*/
private Node createHuffmanTree(List<Node> nodes) {
while (nodes.size() > 1) {
// 从大到小排序
Collections.sort(nodes);
Node leftNode = nodes.get(0);
Node rightNode = nodes.get(1);
// 创建新的Node,但该结点并没有data,只有weight
Node node = new Node(null, (leftNode.getWeight() + rightNode.getWeight()));
node.setLeft(leftNode);
node.setRight(rightNode);
nodes.remove(leftNode);
nodes.remove(rightNode);
nodes.add(node);
}
return nodes.get(0);
}
测试:
@Test
public void test() {
String content = "i like like like java do you like a java";
byte[] bytes = content.getBytes();
List<Node> nodes = getNodes(bytes);
System.out.println("HuffmanTree:");
Node huffmanTree = createHuffmanTree(nodes);
System.out.println("前序遍历Huffman");
huffmanTree.preList();
所得结果虽然与下图中结果不符,但根节点权值均为40,WPL相同,因此满足条件
5、生成赫夫曼编码表
将字符串中各字符及其赫夫曼编码保存在Map中
// 接受对应元素的赫夫曼编码值
HashMap<Byte, String> huffmanCodes = new HashMap<>();
// 拼接赫夫曼编码
StringBuilder stringBuilder = new StringBuilder();
/**
* @param node 传入结点
* @param code 生成赫夫曼编码的路径对应的0或1字符
* @param stringBuilder 用来动态接收赫夫曼编码
* @Method generateHuffmanCode 生成赫夫曼编码
* @Author zhihua.Li
* @Version 1.0
* @Description 获取传入的Node结点的所有叶子结点的赫夫曼编码,并放入huffmanCodes中
* @Return void
* @Exception
* @Date 2021/2/22 23:49
*/
private void generateHuffmanCode(Node node, String code, StringBuilder stringBuilder) {
// 给当前遍历到的结点分别添加其对应的路径编号,左0右1,不断追加直至找到叶子结点
// 拼接路径,存储某个叶子结点的路径
StringBuilder sb = new StringBuilder(stringBuilder);
sb.append(code);
// 若当前传入结点不为空则开始判断:
// 若当前结点的data域为空,说明该结点为生成赫夫曼树过程中动态生成的只有权重的中间结点,需要继续递归获取字符结点
// 而若当前结点的data域不为空,则说明该结点为字符所对应的结点
if (node != null) {
// 当前结点为非叶子结点
if (node.getData() == null) {
// 递归处理其左右子结点
generateHuffmanCode(node.getLeft(), "0", sb);
generateHuffmanCode(node.getRight(), "1", sb);
} else {
// 当前结点为叶子结点
huffmanCodes.put(node.getData(), sb.toString());
}
}
}
测试:
@Test
public void test() {
String content = "i like like like java do you like a java";
byte[] bytes = content.getBytes();
List<Node> nodes = getNodes(bytes);
System.out.println("HuffmanTree:");
Node huffmanTree = createHuffmanTree(nodes);
System.out.println("前序遍历Huffman");
huffmanTree.preList();
// 重载前方法获取赫夫曼编码
generateHuffmanCode(huffmanTree, "", stringBuilder);
System.out.println(huffmanCodes);
为了上面的方法调用方便,调用时必定是对根节点进行的,对其进行重载,使其在调用时只需要传入结点即可
private Map<Byte, String> generateHuffmanCode(Node node) {
if (node == null) return null;
generateHuffmanCode(node.getLeft(), "0", stringBuilder);
generateHuffmanCode(node.getRight(), "1", stringBuilder);
return huffmanCodes;
}
6、数据压缩
将字符串对应的byte数组通过生成的赫夫曼编码表来生成压缩后的字节数组
/**
* @param bytes 原始数组对应的字符数组
* @param map 字符数组元素对应赫夫曼码值的映射表
* @Method zip 通过赫夫曼编码对数据进行压缩
* @Author zhihua.Li
* @Version 1.0
* @Description String content = "i like like like java do you like a java"; -> byte[] bytes = content.getBytes();
* 1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
* 经过zip()处理后,将对应的赫夫曼编码后得到的字符串分割为字符数组,8个一组
* 如:返回的byte[0] = 10101000 其对应的二进制码为 -88
* (最高位符号位,当前二进制码为补码,转换成对应的原码 补->反:10101000-1 = 10100111 反—>原:10100111-> 11011000(二进制) -> -88(十进制)
* @Return byte[] 经过压缩后的字符数组
* @Exception
* @Date 2021/2/23 10:22
*/
private byte[] zip(byte[] bytes, Map<Byte, String> map) {
StringBuilder stringBuilder = new StringBuilder();
// 遍历字符数组,并对应map对其进行赫夫曼码值映射,并追加到stringBuilder字符串后
for (byte each : bytes) {
stringBuilder.append(map.get(each));
}
int length; //接受压缩后返回的byte数组长度
// if (stringBuilder.length() % 8 == 0) { //二进制位数不足8的整数倍时
// length = stringBuilder.length() / 8;
// } else {
// length = stringBuilder.length() / 8 + 1;
// }
// 上面的代码可简写为以下形势:
length = (stringBuilder.length() + 7) / 8;
byte[] temp = new byte[length];
int index = 0;
for (int i = 0; i < stringBuilder.length(); i += 8) {
String strByte;
if (i + 8 > stringBuilder.length()) {
// 若本次循环字符不足8个,从下标为i开始截取子串,即把剩下的全部拿出
strByte = stringBuilder.substring(i);
} else {
strByte = stringBuilder.substring(i, i + 8);
}
// 将二进制字符串strByte以8位二进制数并转换为byte类型后存储在temp[index]中
temp[index++] = (byte) Integer.parseInt(strByte, 2);
}
return temp;
}
测试:
@Test
public void test() {
String content = "i like like like java do you like a java";
byte[] bytes = content.getBytes();
List<Node> nodes = getNodes(bytes);
System.out.println("HuffmanTree:");
Node huffmanTree = createHuffmanTree(nodes);
System.out.println("前序遍历Huffman");
huffmanTree.preList();
// 重载前方法获取赫夫曼编码
generateHuffmanCode(huffmanTree, "", stringBuilder);
System.out.println("根据赫夫曼编码压缩后的字符数组:");
System.out.println(Arrays.toString(zip(bytes, huffmanCodes)));
//
结果为:
7、对前面的各个步骤进行整合封装
// 对上述各方法进行封装整合到一个方法中
private byte[] HuffmanZip(byte[] temp) {
// 3.将字符数组中各元素转换为Node结点的data并存入Node集合中
List<Node> nodes = getNodes(temp);
// 4.通过生成的结点集合创建赫夫曼树(根节点)
Node huffmanTree = createHuffmanTree(nodes);
// 5.通过赫夫曼树获取对应的赫夫曼编码
Map<Byte, String> huffmanCode = generateHuffmanCode(huffmanTree);
// 6.通过赫夫曼编码的map对原始字符数组映射转换为压缩后的字符数组
byte[] zip = zip(temp, huffmanCode);
return zip;
}
4、对赫夫曼编码解压
解码过程,就是编码的逆向操作
获取到了压缩到的字符数组,通过判断是否为当前字符数组的最后一个元素判断是否需要进行补位操作,若为最后一个元素,就不对其进行补位,若不是最后一个元素,就将其进行补位操作(补齐8位二进制数)
/**
* @param flag 判断目标数据是否需要补高位,true补,false不补
* @param b 传入的byte
* @Method byteToBitString
* @Author zhihua.Li
* @Version 1.0
* @Description 将通过Huffman编码Zip转换为二进制数的数字转换回二进制字符串
* @Return java.lang.String 该byte转换所得二进制字符串(按补码返回)
* @Exception
* @Date 2021/2/27 8:31
*/
private String byteToBitString(boolean flag, byte b) {
// 使用一个int数值来接受传入的byte类型数据
int temp = b;
// 当传入正数时,如1,返回的字符串为1,因此需要补齐8位二进制数,使用如下方法
// 1 0000 0000 || 0000 0001 = 1 0000 0001
if (flag) {
temp |= 256;
}
// 只有Integer才能将数据转换为二进制字符串而Byte没有
String string = Integer.toBinaryString(temp);
if (flag) {
// 又因为仅仅需要8位二进制数来表示某个数
// 例如-1用上面的方法返回的字符串是11111111111111111111111111111111(32个)刚好是int能表示的4个字节
// 因此只需要获取32位的低8位即可表示待表示数据,
return string.substring(string.length() - 8);
} else {
// 如果本次要转换的数字本就不满8位,就直接返回不需要进行补位了
return string;
}
}
解码过程:
/**
* @param huffmanCodes 赫夫曼编码表
* @param huffmanBytes 赫夫曼编码得到的字节数组
* @Method decode
* @Author zhihua.Li
* @Version 1.0
* @Description
* @Return byte[]
* @Exception
* @Date 2021/2/27 8:34
*/
private byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
// 完成字符串拼接
StringBuilder stringBuilder = new StringBuilder();
// 用来接收是否对字符串进行补齐操作,作为传入参数传入byteToBitString
boolean flag;
// 将byte数组转换成字符串
for (int i = 0; i < huffmanBytes.length; i++) {
// 如果当前元素为数组中的最后一个元素,则不对其补位操作
flag = (i == huffmanBytes.length - 1);
stringBuilder.append(byteToBitString(!flag, huffmanBytes[i]));
}
// 将字符串按指定的赫夫曼编码进行解码
// 对原来的Map进行反操作,将字符对应的赫夫曼编码作为key,字符作为Value存在Map中
HashMap<String, Byte> map = new HashMap<>();
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; //和i配合获取某字符的赫夫曼编码
boolean flag1 = true; //循环的条件
Byte b = null; //添加到集合中的元素
while (flag1) {
// 不断获取赫夫曼编码中的子串来判断是否存在于解码表中
String key = stringBuilder.substring(i, i + count);
// 判断在map中是否存在key,存在就添加到集合中,不存在就使count++,截取更长的子串
b = map.get(key);
if (b == null) {
// 没有匹配到赫夫曼编码
count++;
} else {
// 匹配到了
flag1 = false;
}
}
// 匹配到了,直接将i指向当前找到的key的最后一个字符的下一个字符
i += count;
list.add(b);
}
// 当for循环结束后,List中就存储了所有字符
// 将list中的数据放入到byte[] 并返回
byte[] bytes = new byte[list.size()];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = list.get(i);
}
return bytes;
}
测试:
@Test
public void test() {
String content = "i like like like java do you like a java";
byte[] b = content.getBytes();
直接调用封装后的方法
byte[] bytes = HuffmanZip(b);
System.out.println(Arrays.toString(bytes) + ",长度为:" + bytes.length);
byte[] decode = decode(huffmanCodes, bytes);
String s = new String(decode);
System.out.println("原来的字符串是:" + s);
}
可以看到,测试结果和原来的字符串完全一致