Qz学算法-数据结构篇(哈夫曼树&哈夫曼编码)
哈夫曼树
1.基本介绍
- 给定n个权值作为个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树Huffman Tree,还有的书翻译为霍夫曼树。
- 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
2.赫夫曼树几个重要概念和举例说明
- 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1
- 结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
- 树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL(weighted path length),权值越大的结点离根结点越近的二叉树才是最优二叉树。
- WPL最小的就是赫夫曼树
3.思路分析
构成赫夫曼树的步骤:
1)从小到大进行排序,将每一个数据,每个数据都是一个节点,每个节点可以看成是一颗最简单的二叉树
2)取出根节点权值最小的两颗二叉树
3)组成一颗新的二叉树,该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
4)再将这颗新的二叉树,以根节点的权值大小再次排序,不断重复1-2-3-4的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树
4.代码实现
package huffmantree;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @author LeeZhi
* @version 1.0
*/
public class HuffmanTree {
public static void main(String[] args) {
int arr[] = {13,7,8,3,29,6,1};
Node node = createHuffmanTree(arr);
//测试一把
preOrder(node);
}
//编写一个前序遍历的方法
public static void preOrder(Node root){
if (root!=null){
root.preOrder();
}else{
System.out.println("是空树,不能遍历~");
}
}
//创建赫夫曼树的方法
/**
*
* @param arr 需要创建成哈夫曼树的数组
* @return 创建好后的赫夫曼树的root节点
*/
public static Node createHuffmanTree(int []arr){
//第一步,为了操作方便
//1.遍历arr数组
//2.将arr的每个元素构成一个Node
//3.将Node放入到ArrayList中
List<Node>nodes = new ArrayList<Node>();
for(int value:arr){
nodes.add(new Node(value));
}
//我们处理的过程是一个循环的过程
while(nodes.size()>1){
//排序从小到大
Collections.sort(nodes);
System.out.println("nodes="+nodes);
//取出根节点权值最小的两棵二叉树
//(1)取出权值最小的节点(二叉树)
Node leftNode = nodes.get(0);
//(2)取出权值第二小的节点(二叉树)
Node rightNode = nodes.get(1);
//(3)构建一颗心的二叉树
Node parent = new Node(leftNode.value+rightNode.value);
parent.left = leftNode;
parent.right = rightNode;
//(4)从ArrayList删除处理过的二叉树
nodes.remove(leftNode);
nodes.remove(rightNode);
//(5)将parent加入nodes
nodes.add(parent);
}
//返回哈夫曼树的root节点
return nodes.get(0);
}
}
//创建结点类
//为了让Node 对象持续排序Collection集合排序
//让Node实现Comparable接口
class Node implements Comparable<Node>{
int value;//节点权值
Node left;//指向左子节点
Node right;//指向右子节点
//写一个前序遍历
public void preOrder(){
System.out.println(this);
if (this.left!=null){
this.left.preOrder();
}
if (this.right!=null){
this.right.preOrder();
}
}
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;
}
}
哈夫曼编码
1.基本介绍
1)赫夫曼编码也翻译为哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,属于一种程序算法 2)赫夫曼编码是赫哈夫曼树在电讯通信中的经典的应用之一。 3)赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间 4)赫夫曼码是可变字长编码(LC)的一种。Huffman于1952年提出一种编码方法,称之为最佳编码
2.原理剖析
通信领域中信息的处理方式1-定长编码
- i like like like java do you like a java∥共40个字符(包括空格)
- 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 /对应Ascii码
- 01101001 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101010 01100001 01110110 01100001 00100000 01100100 01101111 00100000 01111001 01101111 01110101 00100000 01101100 01101001 01101011 01100101 00100000 01100001 00100000 01101010 01100001 01110110 01100001 //对应的二进制
- 按照二进制来传递信息,总的长度是359(包括空格)
- 在线转码工具:hhtps://www.mokuge.com/tool/asciito16/
通信领域中信息的处理方式2-变长编码
- i like like like java do you like a java//40个字符(包括空格)
- d:1 y:1 u:1 j:2 v:2 o:2 I:4 k:4 e:4 i:5 a:5 :9 //各个字符对应的个数
- 0=,1=a,10=i,11=e,100=k,101=l,110=0,111=V,1000=j,1001=u,1010=y,1011=d 说明:按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小,比如 空格出现了9次,编码为0,其它依次类推.
- 按照上面给各个字符规定的编码,则我们在传输"i like like like java do you like a java"数据时,编码就是10010110100.
- 字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码,即不能匹配到重复的编码
通信领域中信息的处理方式3-赫夫曼编码
//根据赫夫曼树,给各个字符 //规定编码,向左的路径为0 //向右的路径为1,编码如下: o:1000 u:10010 d:100110 y:100111 i:101 a:110 k:1110 e:1111 j:0000 v:0001 I:001 :01 按照上面的赫夫曼编码,我们的"i like like like java do you like a java"字符串对应的编码为(注意这里 我们使用的无损压缩) 1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
长度为:133
说明:
1)原来长度是359,压缩了(359-133)/359=62.9%
2)此编码满足前斑编码,即字符的编码都不能是其 他字符编码的前缀。不会造成匹配的多义性
3.代码实现
思路
(1)Node{data存放数据,weight{权值},left和right}
(2)得到"i likelikelike java do you like a java"对应的byte[]数组
(3)编写一个方法,将准备构建赫夫曼树的Node节点放到List,形式[Node[date='7!weight=5],Node[]date=32,weight =9].......d:1 y:1 u:1 j:2 v:2 o:2 1:4 k:4 e:4 i:5 a:5 :9
(4)可以通过List创建对应的赫夫曼树
package huffmantree;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* @author LeeZhi
* @version 1.0
*/
public class HuffmanCode {
public static void main(String[] args) {
String str = "i like like like java do you like a java";
byte[] contentBytes = str.getBytes(StandardCharsets.UTF_8);
System.out.println(contentBytes.length);//40
List<Node> nodes = getNodes(contentBytes);
System.out.println("nodes:"+nodes);
//测试一把,创建的二叉树
System.out.println("赫夫曼树");
Node huffmanTreeRoot = createHuffmanTree(nodes);
System.out.println("前序遍历");
huffmanTreeRoot.preOrder();
//测试生成
getCodes(huffmanTreeRoot,"",stringBuilder);
System.out.println("生成的huffmam编码表"+huffmanCodes);
}
//生成哈夫曼树对应的哈发满编码
//思路
//1.将哈夫曼编码表存放在Map<Byte,String> 形式
// 32-> =01 a=100 d=11000 u=11001 e=1110 v=11011 i=101 y=11010 j=0010 k=11111 l=0000 o=0011
static Map<Byte,String> huffmanCodes = new HashMap<>();
//2.在生成哈夫曼编码表时,需要去拼接路径,定义一个StringBuilder 存储某个叶子节点路径
static StringBuilder stringBuilder = new StringBuilder();
//为了调用方便,我们重载getCodes
private static Map<Byte,String> getCodes(Node root){
if (root == null){
return null;
}
//处理root左子树
getCodes(root.left,"0",stringBuilder);
//处理root右子树
getCodes(root.right,"1",stringBuilder);
return huffmanCodes;
}
/**
* 功能:将传入的node节点的所有的叶子节点的哈夫曼编码得到,饼放入到huffmanCodes集合
* @param node 传入节点
* @param code 路径: 左子结点是0 右子结点1
* @param stringBuilder 用于拼接路径
*/
private static void getCodes(Node node,String code,StringBuilder stringBuilder){
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
//将code 加入到stringBuilder2
stringBuilder2.append(code);
if (node!=null){ //如果node==null不处理
//单端当前node是叶子结点还是非叶子结点
if (node.data == null){//非叶子结点
//递归处理
//向左
getCodes(node.left,"0",stringBuilder2);
//向右递归
getCodes(node.right,"1",stringBuilder2);
}else{
//说明是一个叶子结点
//就表示找到某个叶子结点的最后
huffmanCodes.put(node.data,stringBuilder2.toString());
}
}
}
//前序遍历
private static void preOrder(Node root){
if(root!=null){
root.preOrder();
}else{
System.out.println("赫夫曼树为空");
}
}
private static List<Node> getNodes(byte[] bytes){
//1.创建一个ArrayList
ArrayList<Node> nodes = new ArrayList<>();
//遍历bytes 统计每一个byte出现的次数 ->map[key,value]
Map<Byte,Integer> counts = new HashMap<>();
for (byte b:bytes) {
Integer count = counts.get(b);
if (count==null){//Map还没有这个字符数据,第一次
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);
//创建一颗新的二叉树,他的根节点,没有data,只有权值
Node parent = new Node(null, leftNode.weight+rightNode.weight);
parent.left=leftNode;
parent.right=rightNode;
//将已经处理的两颗二叉树从nodes删除
nodes.remove(leftNode);
nodes.remove(rightNode);
//将新的二叉树你,加入到nodes
nodes.add(parent);
}
//nodes 最后的节点,就是哈夫曼树的根节点
return nodes.get(0);
}
}
//创建Node,带数据和权值
class Node implements Comparable<Node>{
Byte data;//存放数据本身,比如'a'=>97 ' '=>32
int weight;//权值
Node left;
Node right;
public Node(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
@Override
public int compareTo(Node o) {
//从小到大排序
return this.weight-o.weight;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
", left=" + left +
", right=" + right +
'}';
}
//前序遍历
public void preOrder(){
System.out.println(this);
if (this.left!=null){
this.left.preOrder();
}
if (this.right!=null){
this.right.preOrder();
}
}
}
转载自:https://juejin.cn/post/7248145094600572989