前言
上面这位就是本文的编码方式发明者, 哈夫曼先生
下面会介绍哈夫曼树的定义,名词解释(基本概念),构造算法和哈夫曼编码.
提示:以下是本篇文章正文内容,下面案例可供参考
一、Huffman Tree是什么?
1.定义
哈夫曼树是一种带权路径长度最短的树 , 下面会给出何为带权路径. 我们又称其为最优树 , 是目前效率最高的判别树.
我们在构造的过程中, 会人为地将权值大的结点放到较上层,如此, 该结点的路径长度最小,权值大,所以权值*路径长度
也就最小 (贪心)
2.基本概念
这里先给出一棵哈夫曼树,下面会根据此树解释概念
1. 路径长度: 分支上的所有路径数, 比如b的路径长为2,c为3
2. 权: 是对于实体的某一属性的量化描述 ,如下面会讲的字符出现次数
3. 树的带权路径长度: 树的每个叶子结点的带权路径长度之和(即 路径长度*权值,再求和),记作WPL,
上图WPL=1*18+2*12+3*10+3*11
4. 哈夫曼树 : 即WPL最小的树
二、哈夫曼算法
下面说说如何 “拥有” 一颗哈夫曼树
1.结点是必需的
这里我们将结点的值域data
同时也作为权值使用 , 由于哈夫曼树权重大的结点排列在顶层的原因,我们的Node类需要进行排序 , 因此需要实现一个Comparable
接口用于规定排序规则
代码如下(示例):
class Node implements Comparable<Node>{
int data ;
Node leftNode;
Node rightNode;
public Node(int data) {
this.data = data;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
'}';
}
@Override
public int compareTo(Node o) {
return this.data - o.data;
}
//先序遍历
public void preOrder(){
System.out.print(this.data+" ");
if(this.leftNode != null){
this.leftNode.preOrder();
}
if(this.rightNode != null){
this.rightNode.preOrder();
}
}
}
2.创建哈夫曼树
关键点来了,先列举出思路
1. 定义一个辅助数组,并向其传入结点node
2. 循环,对目标数组(即传入的参数)进行排序,并取出每次排序结果中最小的两个结点
3. 创建一个新结点,将取出的两个树挂在新结点的同时,将新结点的权值置为二者权值之和(注意,不是值域)
4. 将新结点加入到辅助数组中并移出最小的那两个结点
5. 当数组的大小等于1时,循环终止
代码如下(示例):
class HuffmanTree {
public static Node createHuffmanTree(int[] arr){
ArrayList<Node> nodes = new ArrayList<>();
for (int val: //向array list传值
arr) {
nodes.add(new Node(val));
}
while(nodes.size() > 1){
Collections.sort(nodes); //将nodes排序,排序得放里面排,因为remove后需重新排一次序
//1. 找到权值最小的两个结点
Node leftNode = nodes.get(0);
Node rightNode = nodes.get(1);
//2. 构建一颗新树
Node parent = new Node(leftNode.data+rightNode.data);
parent.leftNode = leftNode;
parent.rightNode = rightNode;
//3. 将新树加入到ArrayList
nodes.add(parent);
//4.删除结点
nodes.remove(leftNode);
nodes.remove(rightNode);
}
return nodes.get(0);
}
}
三、哈夫曼编码
1.定义
哈夫曼树可以应用于数据的压缩 , 基本思想是对出现次数多的字符编较短的码,且是一种最优前缀编码 , 也就是说每个字符的编码都不会是其它字符编码的前缀.
在设计算法的时候我们可以将字符出现的次数作为权值,将出现次数最多的字符放到树的上层,就达到了压缩的效果
2.思路
1.统计各个字符出现的次数
2.以字符出现次数作为权值(键值对中的值),字符作为键值对的键构建哈夫曼树
3.将字符串转为变长编码
代码如下(示例):
public class HuffmanCodeDemo {
public static void main(String[] args) {
String str = "i like like like java do you like a java";
byte[] strBytes = str.getBytes();
System.out.println(strBytes.length);
List<Node> nodes = HuffmanCode.getNodes(strBytes);
Node root = HuffmanCode.createHuffmanCode(nodes);
root.preOrder();
}
public static void preOrder(Node root){
if(root != null){
root.preOrder();
}
}
}
class HuffmanCode{
public static List<Node> getNodes(byte[] bytes){
//定义一个ArrayList,存放Node数组
ArrayList<Node> nodes = new ArrayList<>();
//统计各个字符出现的次数
Map<Byte,Integer> counts = new HashMap<>(); //统计的map
for (byte b:
bytes) {
Integer count = counts.get(b); //在map中查找b
if(count == null){
//counts中还没加入b这个字符,则加入b,并且让b的出现次数(即权重为1)
counts.put(b,1);
} else {
counts.put(b,++count);
}
}
//把键值对转换成Node对象,并加入List<Node>
for (Map.Entry<Byte,Integer> entry:
counts.entrySet()) {
nodes.add(new Node(entry.getKey(),entry.getValue()));
}
return nodes;
}
public static Node createHuffmanCode(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.leftNode = leftNode;
parent.rightNode = rightNode;
//将新树加入到nodes
nodes.add(parent);
nodes.remove(leftNode);
nodes.remove(rightNode);
}
return nodes.get(0) ; //返回根结点
}
}
class Node implements Comparable<Node>{
Byte data; //数据域
Integer weight; //权重
Node leftNode;
Node rightNode;
public Node(Byte data, Integer weight) {
this.data = data;
this.weight = weight;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
@Override
public int compareTo(Node o) {
return this.weight - o.weight; //按照权值从小到大排列
}
public void preOrder(){
System.out.println(this);
if(this.leftNode != null){
this.leftNode.preOrder();
}
if(this.rightNode != null){
this.rightNode.preOrder();
}
}
}
具体编程思路
1. 最核心的部分是统计字符出现次数并将键值对转换为Node对象
2.构建哈夫曼编码表
思路有点难理解,需要debug慢慢调试哦~
1. 创建一个静态的StringBuilder和Map,前者用于拼接路径,后者用于构造编码表
2. 创建编码函数, 当前结点不为叶子结点时,对其左右子结点递归,
反之以键值对方式加入编码表
3.重载该方法. 优雅地传入一个哈夫曼树的根节点,返回编码表
代码如下(示例):
//用于拼接路径
static StringBuilder stringBuilder = new StringBuilder();
//用于存放哈夫曼编码
static Map<Byte,String> huffmanCode = new HashMap<Byte, String>();
//对getCodes(p1,p2,p3)重载
public static Map<Byte,String> getCodes(Node root){
if(root == null){
return null;
}
getCodes(root.leftNode,"0",stringBuilder);
getCodes(root.rightNode,"1",stringBuilder);
return huffmanCode;
}
/**
* 传入结点,转为Huffman编码
* @param node 传入的结点
* @param code 存放路径
* @param stringBuilder 拼接
*
*/
public static void getCodes(Node node,String code,StringBuilder stringBuilder){
//用传入的StringBuilder再生成一个StringBuilder
StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);
stringBuilder1.append(code); //拼接code
if(node != null){
if(node.data == null){
//非叶子结点,就继续递归调用
//递归调用左右结点
getCodes(node.leftNode,"0",stringBuilder1);
getCodes(node.rightNode,"1",stringBuilder1);
} else {
//叶子结点
huffmanCode.put(node.data,stringBuilder1.toString()); //以键值对方式加入编码表
}
}
}