Java实现哈夫曼编码--数据无损压缩
数据结构之哈夫曼编码—数据无损压缩
哈夫曼编码可以可以将数据无损压缩,很强大,那么我们来了解一下哈夫曼编码:
首先,哈夫曼编码的原理是利用了哈夫曼树,通过统计数据中字符出现的次数,让出现最多的字符离哈夫曼树的根结点就越近,其路径也就越近,路径就是有1和0表示。
如数据:"i like like like java do you like a java"中,出现的最多的是空格,我们通过哈夫曼树,就可以将其路径变得最短,如本来可能是1001001,使用哈夫曼树后就可能变成110了, 说到这里大家可能还是不懂,还是上面的例子,可以知道空格出现了9次,l 出现了4次;假如,本来空格用100100表示,一个空格就要6个数字表示,l 用111表示,一个 l 要3个数字表示,存储所有空格和所有 l 就需要
96+43=66
那如果使用哈夫曼树后,就会变成空格用111表示,l 用100100表示,此时存储空间就只需要
93+46=51
数据还是那些数据,没有损耗,只是换了路径表示方式,就减少了所需的存储空间,所以哈夫曼编码是无损压缩数据的首选。
好了,看到这里原理我们大概明白了,接下来是思路,还是上述例子
数据:“i like like like java do you like a java”
①获取到数据中的所有字符,分别存放如一个字符数组contentBytes中(此时字符共40个)
②统计字符数组中同一个字符出现的次数,如空格就出现了9次,将统计结果放入Map中,字符为key,出现次数为value
③先要创建一个Node类,Node中应包含data(数据),和weight(权值),还要以weight作为指标进行排序(Node类继承comparable接口)
④将每一个Map集合中的个体都变成一个Node对象,故让map中的key作为data,value作为weight,所有Node对象放入一个ArrayList中
⑤创建一颗哈夫曼树,先要对所有ArrayList中的Node对象进行排序,排序后将结点组合成树
⑥以0为左边路径,1位右边路径树中的路径,得到以字符为key,路径为value的Map集合
⑦遍历map集合,将value中的路径拼接成一个长字符串
⑧将字符串每8位为一组,每组转换为一个字符,所有字符放入一个字节数组中(次数字符共17个)
以上就是哈夫曼编码的所有步骤,有点复杂,但是不难理解
接下来是代码:no code,no 逼逼
package cn.ycl.system.other.study.leetCode;
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();
// 尝试输出该字节数组,发现是40个十进制数字
System.out.println(Arrays.toString(contentBytes));
// [105, 32, 108, 105, 107, 101, 32, 108, 105, 107, 101, 32, 108,
// 105, 107, 101, 32, 106, 97, 118, 97, 32, 100, 111, 32,
// 121, 111, 117, 32, 108, 105, 107, 101, 32, 97, 32, 106,
// 97, 118, 97]
//测试是否转换成了Node对象
List<Node> nodes = getNodes(contentBytes);
System.out.println(nodes);
//测试一把创建的哈夫曼树
System.out.println("以下是哈夫曼树:");
Node root = createHuffmanTree(nodes);
System.out.println("前序遍历");
root.preOrder();
//测试一把是否生成了对应的哈夫曼编码
//根结点是没有路径的,故设置为空
getCodes(root, "", stringBuilder);
System.out.println("以下是Map路径:");
//遍历map主要是为了取得key和value的值来使用,若只是单纯输出,直接输出huffmanCodes即可,这里只是为了练习map遍历,写复杂了
for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
System.out.println(entry.getKey() + "---" + entry.getValue());
}
//测试一下路径拼接后的唯一一个长路径
byte[] zip = zip(contentBytes, huffmanCodes);
//得到17个十进制数字,比之前的40个十进制数字,压缩率为(40-17)/40=57.5%
System.out.println(Arrays.toString(zip));
}
/**
* 此方法的作用就是将得到的Map中的多个路径拼接成一个字符串,然后将字符串按每8位进行分割,再转成十进制
* 其中bytes就是程序开始得到的字符数组contentBytes,huffMap就是路径的Map:huffmanCodes
*/
private static byte[] zip(byte[] bytes, Map<Byte, String> huffMap) {
int index = 0;
String temp;
StringBuilder stringBuilder = new StringBuilder();
//遍历byte数组,将map中的路径拼接
for (byte b : bytes) {
stringBuilder.append(huffMap.get(b));
}
//判断该字符串可以组成多少个十进制数
int len = 0;
if (stringBuilder.length() % 8 == 0) {
len = stringBuilder.length() / 8;
} else {
len = stringBuilder.length() / 8 + 1;
}
//将字符串截取,放入字符数组
byte[] countss = new byte[len];
for (int i = 0; i < stringBuilder.length(); i = i + 8) {
if (i + 8 > stringBuilder.length()) {
temp = stringBuilder.substring(i);
countss[index] = (byte) Integer.parseInt(temp, 2);
break;
}
temp = stringBuilder.substring(i, i + 8);
//将截取的字符串转换成数字Integer.parseInt(temp,2)表示,temp的基数是二进制
countss[index] = (byte) Integer.parseInt(temp, 2);
index++;
}
return countss;
}
/**
* 此方法的作用就是生成赫夫曼树对应的赫夫曼编码
* 思路:
* 1.将赫夫曼编码表存放在Map<Byte,String>
* 2.在生成赫夫曼编码表时,需要去拼接路径,定义一个StringBuilder,存储某个叶子结点的路径
*/
static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();
static StringBuilder stringBuilder = new StringBuilder();
//其中code就是路径:规定:左子结点是0,右子结点; stringBuilder是用于拼接路径
private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
//将code加入到stringBuilder2中
stringBuilder2.append(code);
if (node != null) {
if (node.data == null) {//data空表示是非叶子结点
//向左递归
getCodes(node.left, "0", stringBuilder2);
//向右递归
getCodes(node.right, "1", stringBuilder2);
} else {//data不为空表示是叶子结点,一条路径完成,将stringbuilder2中的路径放入map中
huffmanCodes.put(node.data, stringBuilder2.toString());
}
}
}
/**
* 此方法的作用就是将字节数组转换成为List形式的node对象
*/
private static List<Node> getNodes(byte[] bytes) {
//1创建一个ArrayList
ArrayList<Node> nodes = new ArrayList<Node>();
//2遍历bytes ,统计每一个byte出现的次数-》使用map[key,value]
Map<Byte, Integer> counts = new HashMap<>();
for (byte b : bytes) {
Integer count = counts.get(b);
if (Objects.isNull(count)) {
counts.put(b, 1);
} else {
counts.put(b, count + 1);
}
}
//把每一个键值对转成一个Node对象,并加入到nodes集合中
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);
Node parentNode = new Node(null, leftNode.weight + rightNode.weight);
//关联父子结点和移除子结点,加入父结点
parentNode.left = leftNode;
parentNode.right = rightNode;
nodes.remove(leftNode);
nodes.remove(rightNode);
nodes.add(parentNode);
}
//返回最后的一个结点
return nodes.get(0);
}
}
//创建Node带数据和权值
package cn.ycl.system.other.study.leetCode;
public class Node implements Comparable<Node> {
/**
* 存放数据本身,比如 ‘a’=>97 空格 ‘ ’==>32
*/
Byte data;
/**
* 权值,表示字符出现的次数
*/
int weight;
/**
* 左树
*/
Node left;
/**
* 右树
*/
Node right;
public Node(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
@Override
public String toString() {
return "Node [data=" + data + ", weight=" + weight + "]";
}
/**
* 实现comparable接口必须实现的方法
* @param o
* @return
*/
@Override
public int compareTo(Node o) {
// 从小到大排序
return this.weight - o.weight;
}
/**
* 前序遍历
*/
public void preOrder() {
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~