学习数据结构笔记(10) --- [赫夫曼树(Huffman Tree)与赫夫曼编码(Huffman coding)]
Posted 小智RE0
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学习数据结构笔记(10) --- [赫夫曼树(Huffman Tree)与赫夫曼编码(Huffman coding)]相关的知识,希望对你有一定的参考价值。
B站学习传送门–>尚硅谷Java数据结构与java算法(Java数据结构与算法)
文章目录
一:赫夫曼树
1.赫夫曼树的相关概念
赫夫曼树是带权路径长度最短的树
;并且权值较大的树距离根结点比较近
路径就是:从一个节点出发,到达它的子节点,或者孙子节点,经过的通路;就是路径;
路径长度的话;比较简单的计算方法; 比如我认为根结点所在的位置是第一层;那么假如这棵树有M层;那么从根结点到达第M层
的路径长度就是(M-1)
;
那么,我从第1层的节点到第4层的节点路径长度就是(4-1 = 3);
从第2层的节点到第4层的节点的路径长度就是(4-2 = 2)
比如,我这图中;
第一个节点到下面左节点的右结点,这段路径的长度为2;
第一个节点到下面右结点 ;这段路径的长度为 1
节点的权:
如果要把给一个节点赋予一个特殊的数值,那么就可以说这个数值就是节点的权;
节点的带权路径长度:
从根结点出发,到达这个结点的路径长度
和这个结点的权
的乘积;
例如这个案例,左下角节点的带权路径长度就是 60
树的带权路径长度
就是所有的节点带权路径之和
;
例如这棵树的带权路径长度就是130
若将树改为这样的结构;
这棵树的带全路径长度变为115;
这时的结构就是一棵赫夫曼树
若让最大权 30 远离根结点,
这时树的带权路径长度就变为125
那么,
赫夫曼树就是带权路径长度最短的二叉树;并且权值越大的节点距离根结点越近;
2.赫夫曼树的创建过程图解
(1)首先拿到这个数组后;对数组进行由小到大排序
;
可以把每个数当成一个节点;每个节点当成一棵简单的二叉树;
比如说,我这里有个数组[20,30,40,10,5,1,3];
我先让它由大到小进行排序 为
[ 1, 3, 5, 10, 20, 30, 40]
将每个数据看作是一棵简单的二叉树
(2)找出其中根节点权值最小的两棵二叉树
在我这个案例中,就查找
3
和1
(3) 然后让这两个节点组成一棵新的二叉树,这个新的二叉树权值之和为这两个节点的权值之和;
当前案例下,就把这个1 和 3 组成一棵二叉树
一般,让比较小的放在左边;
(4) 这些剩余的二叉树根据根结点的权值由小到大进行排序;
重复(1)(2)(3)(4)步骤;直到形成一棵赫夫曼树;
找到权值最小的根结点 5 和 4;
找到权值最小的根结点 10和 9;
找到权值最小的根结点 20 和 19;
找到权值最小的根结点 30 和 39;
找到权值最小的根结点 40 和 69;这样;一棵基础的赫夫曼树成型
3.用代码创建实现赫夫曼树
这里实现的话,就让数组元素的数值作为权值;
然后,具体存储用封装节点的集合实现;不过需要注意的是,这里逻辑上集合最终保留的只有一个节点(赫夫曼树的根结点);但是它有挂接的左右子节点
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @author by CSDN@小智RE0
* @date 2021-11-16 20:44
* 赫夫曼树的基本构建过程;
*/
public class HuffManTreeTest
//将数组转换得到一棵赫夫曼树;
public static TreeNode getHuffManTree(int[] array)
//这边实现的话,考虑用一个集合存储节点;
List<TreeNode> list = new ArrayList<>();
//那么就先把数组的元素封装为节点存入集合;
for (int j : array)
list.add(new TreeNode(j));
//最终的话,集合就剩余一个节点了;
while (list.size() > 1)
//先用工具类,让这个集合元素进行排序;
Collections.sort(list);
//找到最小的两个节点;构成一个新的二叉树;
TreeNode left = list.get(0);
TreeNode right = list.get(1);
TreeNode newNode = new TreeNode(left.value + right.value);
//把左右节点挂接到树上;
newNode.leftNode = left;
newNode.rightNode = right;
//这时就把刚才操作的两个节点删了;
list.remove(left);
list.remove(right);
//把新的节点添加到集合;
list.add(newNode);
//最终返回根节点;
return list.get(0);
//让二叉树进行前序遍历;
public static void prefixList(TreeNode root)
if(root==null) throw new RuntimeException("Empty tree, do not traverse");
root.prefixList();
//树的节点;为使得节点具有可比性; 需实现Compareable接口;
class TreeNode implements Comparable<TreeNode>
//这里直接就让value表示权值了;
public int value;
//左子树,右子树;
public TreeNode leftNode;
public TreeNode rightNode;
//初始化;
public TreeNode(int value)
this.value = value;
//输出节点;
@Override
public String toString()
return "TreeNode" + "value=" + value + '';
@Override
public int compareTo(TreeNode o)
//由小到大排序
return this.value - o.value;
//前序遍历;
public void prefixList()
//先输出当前结点;
System.out.println(this);
//若左子树不为空就递归;
if (this.leftNode != null)
this.leftNode.prefixList();
//若右子树不为空就递归;
if (this.rightNode != null)
this.rightNode.prefixList();
测试使用;
//测试;
public static void main(String[] args)
int[] array= 20,30,40,10,5,1,3;
TreeNode root = getHuffManTree(array);
System.out.println("赫夫曼树根结点为->"+root.value);
System.out.println("前序遍历--------");
prefixList(root);
测试结果;和刚才图解分析的树一致
赫夫曼树根结点为->109
前序遍历--------
TreeNodevalue=109
TreeNodevalue=40
TreeNodevalue=69
TreeNodevalue=30
TreeNodevalue=39
TreeNodevalue=19
TreeNodevalue=9
TreeNodevalue=4
TreeNodevalue=1
TreeNodevalue=3
TreeNodevalue=5
TreeNodevalue=10
TreeNodevalue=20
二: 赫夫曼编码
1.关于赫夫曼编码
赫夫曼编码是一种编程算法;广泛地用于数据文件的压缩;
作为可变字长编码
(VLC)之一
在线转换工具: 转换工具
通信领域的信息处理方式:定长编码;变长编码
还有一种方式就是赫夫曼编码;
首先看课程中的案例,后面我会做个案例;
(1)赫夫曼编码的话,首先会统计字符出现的次数,这样每个字符可以暂时用出现次数表示权值
;
(2)将这些字符看做是一个个简单的二叉树;按照创建赫夫曼树的流程,创建出一棵赫夫曼树;
(3)在这个赫夫曼树的基础上;规定向左的节点路径为0
,向右的节点路径为1
;
这样计算的话,每个字符得到的编码值绝对不是一样的;
那么对应的 ;生成的字符编码就是
d: 100110 y: 100111 u: 10010 j:0000 v:0001 o:1000
l: 001 k: 1110 e:1111 i:101 a:110 空格:01
(5)那么翻译字符i like like like java do you like a java
后得到的编码就是
10101001101111011110100110111101111010011011110111101000011000011101001101000011001111000100100100110111101111011100100001100001110
这样的话,会发现每次字符生成的编码都不会是一样的;
之前用ascall码表示这段字符的话得到的是长度为359的编码;
而使用赫夫曼编码后;生成的编码长度才133;那么对数据的压缩效果还是比较可观的;
2.赫夫曼编码创建案例
这里处理的话,首先会统计每个字符出现的次数;
比如说,我这里写个字符串xiaozhi like java
注意,中间有空格;
那么;统计字符出现的次数 x:1 i:3 a:3 o:1 z:1 h:1 l:1 k:1 e:1 j:1 v:1 空格:2
简单排序后;
x:1 o:1 z:1 h:1 l:1 k:1 e:1 j:1 v:1 空格:2 i:3 a:3
然后再将这些次数组合为一个数组;将字符出现的次数作为权值;
按照创建赫夫曼树的过程;将这个字符串变为一棵赫夫曼树;
大概创建出来就是这个情况的;
之前第一次创建出来是这样的;
这样的结构也是符合的;权值路径算出来的话都是59
还没结束;这里最后就按照第一种创建出的结构说说吧
;
左子树路径标记为 0 ;右子树路径为1;
就按这个绿色的树来说吧;
那么对原先的字符进行编码读取;
x:0111 o:000 z:1000 h:0110 l:1011 k:1010
e:0100 j:1000 v:0101 空格:001 i:111 a:110
将字符串xiaozhi like java
进行编码翻译;
01111111100001000011011100110111111010010000110001100101110
3.用代码具体实现赫夫曼编码的创建(文本数据的压缩)
存放的每个节点包括数据域和权值;数据域就是字符(可使用ascall码表示),权值就是字符出现的次数;
3.1首先是转成赫夫曼树
import java.util.*;
public class HuffManCodeTest01
//创建赫夫曼树;
private static Node getHuffman(byte[] bytes)
//调用方法,数组转为集合getNodeToList;
List<Node> list = getNodeToList(bytes);
//只要不到根结点,就不停止;
while (list.size()>1)
//先对集合进行排序;
Collections.sort(list);
//取出左右节点;
Node left = list.get(0);
Node right = list.get(1);
//生成新节点后,这里不存储数据域,仅存权值;
Node newNode = new Node(null,left.weight+right.weight);
//将左右节点挂接到新节点后;
newNode.leftNode = left;
newNode.rightNode = right;
//在集合中删除左右节点;
list.remove(left);
list.remove(right);
//将新节点添加到集合中;
list.add(newNode);
return list.get(0);
//将字符数据和出现的次数封装为节点,存入集合中;
private static List<Node> getNodeToList(byte[] bytes)
//创建最终需要返回的数组集合;
List<Node> list = new ArrayList<>();
//使用map接收字符与次数;
Map<Byte,Integer> map = new HashMap<>();
//将字符数组先存入map集合;
for (byte b : bytes)
//第一次就直接存入;后面若还重复则覆盖前面的,出现次数加1;
map.merge(b, 1, Integer::sum);
//最后再去遍历map集合;放入集合;
for (Map.Entry<Byte, Integer> entry : map.entrySet())
list.add(new Node(entry.getKey(), entry.getValue()));
return list;
//二叉树的前序遍历;
public static void prefixNode(Node root)
if(root!=null)
root.prefixList();
else
throw new RuntimeException("Empty tree, do not traverse");
//节点;
//为具有可比性,实现Comparable接口
class Node implements Comparable<Node>
//数据域,存放字符;
public Byte character;
//存放权值;
public int weight;
//左节点,右结点;
Node leftNode;
Node rightNode;
public Node(Byte character, int weight)
this.character = character;
this.weight = weight;
//权重按照由小到大进行排序
@Override
public int compareTo(Node o)
return this.weight - o.weight;
@Override
public String toString()
return "Node" + "character=" + character + ", weight=" + weight + '';
//前序遍历方法;
public void prefixList()
System.out.println(this);
//向左递归
if (this.leftNode != null) this.leftNode.prefixList();
//向右递归
if (this.rightNode != null) this.rightNode.prefixList();
测试使用
public static void main(String[] args)
//先将字符装进数组;
String str = "xiaozhi like java";
byte[] bytes = str.getBytes();
System.out.println("字符串的长度:"+bytes.length);
//调用方法,创建赫夫曼树;
Node node = getHuffman(bytes);
//对赫夫曼树进行前序遍历;
prefixNode(node);
测试结果;前序遍历差不多
字符串的长度:17
Nodecharacter=null, weight=17
Nodecharacter=null, weight=7
Nodecharacter=null, weight=3
Nodecharacter=111, weight=1
Nodecharacter=32, weight=2
Nodecharacter=null, weight=4
Nodecharacter=null, weight=2
Nodecharacter=101, weight=1
Nodecharacter=118, weight=1
Nodecharacter=null, weight=2
Nodecharacter=104, weight=1
Nodecharacter=120, weight=1
Nodecharacter=null, weight=10
Nodecharacter=null, weight=4
Nodecharacter=null, weight=2
Nodecharacter=106, weight=1
Nodecharacter=122, weight=1
Nodecharacter=null, weight=2
Nodecharacter=107, weight=1
Nodecharacter=108, weight=1
Nodecharacter=null, weight=6
Nodecharacter=97, weight=3
Nodecharacter=105, weight=3
3.2由赫夫曼树得到生成赫夫曼编码
这些字符节点实际上都分布在赫夫曼树的叶子节点上,(数据域不为空)
可使用map集合取到节点字符后拼接路径字符串;
最终得到这样的格式;
x(120):0111 o(111):000 z(122):1000 以上是关于学习数据结构笔记(10) --- [赫夫曼树(Huffman Tree)与赫夫曼编码(Huffman coding)]的主要内容,如果未能解决你的问题,请参考以下文章