什么是赫夫曼树
就是wpl: 就是树的带权路径长度
什么是路劲长度
什么是结点的权, 结点的带权路径长度
int[] arr = {13,7,8,3,29,6,1};
将这个arr变成一个赫夫曼树
就应该是这样的
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class HefumanshuTest {
public static void main(String[] args) {
int[] arr = {
13,7,8,3,29,6,1};
Nodes anHefumanshuByarr = getAnHefumanshuByarr(arr);
anHefumanshuByarr.qianxu();
}
//创建算法, 根据arr创建一个赫夫曼树
public static Nodes getAnHefumanshuByarr(int[] arr){
//先将int的数组变成树节点的ArrayList
List<Nodes> nodes = new ArrayList<Nodes>();
for (int value: arr){
nodes.add(new Nodes(value));
}
/*//现在将节点的ArrayList进行排序
Collections.sort(nodes);
//先取出权值最小的两颗二叉树
Nodes left = nodes.get(0);
Nodes right = nodes.get(1);
//合并成一颗新的二叉树, 并且父节点的权值就是他们的权值的和
Nodes parent = new Nodes(left.value+right.value);
parent.left = left;
parent.right = right;
//从树节点的ArrayList中删除已经处理的节点
nodes.remove(left);
nodes.remove(right);
//再将新的树的父节点存放进ArrayList, 并从新排序
nodes.add(parent);
Collections.sort(nodes);*/
//将以上步骤整合就是
while(nodes.size() > 1){
Collections.sort(nodes);
//先取出权值最小的两颗二叉树
Nodes left = nodes.get(0);
Nodes right = nodes.get(1);
//合并成一颗新的二叉树, 并且父节点的权值就是他们的权值的和
Nodes parent = new Nodes(left.value+right.value);
parent.left = left;
parent.right = right;
//从树节点的ArrayList中删除已经处理的节点
nodes.remove(left);
nodes.remove(right);
//再将新的树的父节点存放进ArrayList, 并从新排序
nodes.add(parent);
}
//最后返回ArrayList的最后一个节点, 及是赫夫曼树的root
return nodes.get(0);
}
}
//创建树节点
class Nodes implements Comparable<Nodes>{
int value ; //权值
Nodes left;
Nodes right;
public Nodes(int value) {
this.value = value;
}
@Override
public String toString() {
return "Nodes{" +
"value=" + value +
'}';
}
//让树节点去继承comparable接口, 继承compareTo方法, 实现从小到大或者从大到小的排序
@Override
public int compareTo(Nodes o) {
//表示从小到大排序 , 如是从大到小就是负的形式
return this.value-o.value;
}
//前序遍历
public void qianxu(){
System.out.println(this);
if (this.left!= null){
this.left.qianxu();
}
if (this.right!= null){
this.right.qianxu();
}
}
}
前序遍历结果
Nodes{
value=67}
Nodes{
value=29}
Nodes{
value=38}
Nodes{
value=15}
Nodes{
value=7}
Nodes{
value=8}
Nodes{
value=23}
Nodes{
value=10}
Nodes{
value=4}
Nodes{
value=1}
Nodes{
value=3}
Nodes{
value=6}
Nodes{
value=13}
什么是赫夫曼编码
但是这种编码方式有二义性
有没有一种编码方式, 既是变长编码(可以减短传输过程中的字长), 又是前缀编码(没有二义性).
就是赫夫曼编码
思路:
首先将字符对应的个数变成一个数列, 这个数列在变成一个赫夫曼树, 根据这个赫夫曼树给字符规定编码, 从root开始左为0, 右为1; 这样每一个字符(叶子结点)根据这个赫夫曼树, 都神奇的有自己的编码, 并且是前缀编码
需要注意的是, 树有可能不一样, 编码也就不完全一样, 但是结果是一样的, 因为只要满足wpl最小, 最后的字长还是一样的
import java.util.*;
public class HeFuManBianMaTest {
public static void main(String[] args) {
//要传输的字符串
String str = "i like you like you you";
//将字符串转化为字节数组
byte[] strarr = str.getBytes();
System.out.println("原数据的字节数组是"+Arrays.toString(strarr));
//打印一次这个字节数组的长度
System.out.println("原数据的字节数组长度是"+strarr.length); //23
/* //1
//得到树节点的ArrayList
List<CodeNode> shujiedianArrayList = getShujiedianArrayList(strarr);
System.out.println(shujiedianArrayList);
//2
//得到赫夫曼树的root并遍历
CodeNode heFuManShuroot = getHeFuManShuByList(shujiedianArrayList);
heFuManShuroot.qianxu();
//3
//得到编码表
getbianmabiao(heFuManShuroot, "", stringBuilder);
System.out.println("生成的赫夫曼编码表"+bianmabiao);
//4
//得到新的由编码表转换的并且压缩后的字节数组
byte[] zipstrarr = zip(strarr, bianmabiao);
System.out.println(zipstrarr.length); //9
//看看新的字节数组
System.out.println(Arrays.toString(zipstrarr));
//[-126, -97, -59, -52, 83, -8, -71, -105, 3]解释一下为什么是这样子, 当时先是String变成二进制的int, 再强转为byte,我们看到的是十进制的源码形式, 但是底层放的依然是二进制的形式
*/
byte[] zipByte = getZipByte(strarr);
System.out.println("用于传输的字节数组为"+Arrays.toString(zipByte)+"长度是"+zipByte.length);
//解码操作
/*System.out.println(byteToBitString(false,(byte)3)); //这个代码是有问题的呀*/
byte[] decode = decode(bianmabiao, zipByte);
System.out.println("解压后的字节数组是"+Arrays.toString(decode));
System.out.println("解压后的字节数组的长度是"+decode.length);
}
//1
//写一个方法, 传入一个字节数组得到一个树节点的ArrayList
public static List<CodeNode> getShujiedianArrayList(byte[] strarr){
//
ArrayList<CodeNode> codeNodeList = new ArrayList<CodeNode>();
//统计传进来的字节数组中的每一个节点的出现次数
Map<Byte, Integer> tongji = new HashMap<>();
for (byte data : strarr){
//先判断map中有没有这个data
Integer cishu = tongji.get(data);
if (cishu == null){
//没有这个data说明是头一次
tongji.put(data, 1); //设置quan为一
}else{
tongji.put(data, cishu+1);
}
}
//接下来就是将每一个键值对变成树节点, 并加入ArrayList
//遍历map
for (Map.Entry<Byte, Integer> entry : tongji.entrySet()){
//这个写法要记住
codeNodeList.add(new CodeNode(entry.getValue(),entry.getKey()));
}
return codeNodeList;
}
//2
//根据树节点的ArrayList得到赫夫曼树
public static CodeNode getHeFuManShuByList(List<CodeNode> shujiedianArrayList){
while(shujiedianArrayList.size() > 1){
Collections.sort(shujiedianArrayList);
//先取出权值最小的两颗二叉树
CodeNode left = shujiedianArrayList.get(0);
CodeNode right = shujiedianArrayList.get(1);
//合并成一颗新的二叉树, 并且父节点的权值就是他们的权值的和
CodeNode parent = new CodeNode(left.quan+right.quan, null);
parent.left = left;
parent.right = right;
//从树节点的ArrayList中删除已经处理的节点
shujiedianArrayList.remove(left);
shujiedianArrayList.remove(right);
//再将新的树的父节点存放进ArrayList, 并从新排序
shujiedianArrayList.add(parent);
}
//最后返回ArrayList的最后一个节点, 及是赫夫曼树的root
return shujiedianArrayList.get(0);
}
//3
//生成赫夫曼树对应的编码表
//思考: 应该用什么数据结构来存放编码表呢
//Map<Byte, String>
//String就是每个叶子结点(字符)对应的赫夫曼编码, 显然的是String不能一次性得到, 是需要拼接的
//StringBuilder: 用来存储某个叶子结点的路径
static Map<Byte, String> bianmabiao = new HashMap<Byte, String>();
static StringBuilder stringBuilder = new StringBuilder();
/**
* 用于得到编码表
* @param node 传入的节点
* @param code 路径代码, 左就是0, 右就是1
* @param stringBuilder 用于拼接路径
*/
public static void getbianmabiao(CodeNode node, String code, StringBuilder stringBuilder){
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder); //临时容器
stringBuilder2.append(code); //用临时容器拼接code
if (node != null){
//如果code==null不处理
//判断当前节点是不是叶子结点
if (node.data == null){
//非叶子结点
//递归处理
//向左
getbianmabiao(node.left, "0", stringBuilder2);
//向右
getbianmabiao(node.right, "1", stringBuilder2);
}else{
//说明是叶子结点
//就表示找到一个叶子结点
bianmabiao.put(node.data, stringBuilder2.toString());
}
}
}
//4
//将原本的字符串使用编码表转换成赫夫曼编码表对应的字符串, 并压缩成一个新的字节数组
public static byte[] zip(byte[] bytes, Map<Byte, String> bianmabiao){
//用于拼接的容器
StringBuilder stringBuilder = new StringBuilder();
for (byte b : bytes){
stringBuilder.append(bianmabiao.get(b));
}
System.out.println("原来编码表的编码串是"+stringBuilder);
//这样就得到了未压缩的根据编码表形成的源字符串的编码串
//接下来就要压缩成一个新的字节数组, 就是八位一个字节
int len;
if (stringBuilder.length() % 8 == 0){
//说明刚好是8的整数倍
len = stringBuilder.length() / 8;
}else{
len = stringBuilder.length() /8 +1;
}
//这样一来这个len就是压缩后的字节数组的长度
byte[] zipbytes = new byte[len];
int index = 0;
for (int i = 0; i < stringBuilder.length(); i+=8){
//步长为8
String str ; //临时存放每一个字节
if (i+8 > stringBuilder.length()){
str = stringBuilder.substring(i); //直接取到最后
}else{
str = stringBuilder.substring(i, i+8); //例: 其实就是0~7的字长的一段, 再来就是8~15......
}
//然后将这个临时容器里的编码转成一个字节, 并放入新的字节数组中
zipbytes[index] = (byte)Integer.parseInt(str, 2); //二进制形式的int, 在强转为byte
index ++;
}
return zipbytes;
}
//将以上四个步骤合成一个方法
public static byte[] getZipByte(byte[] strarr){
List<CodeNode> shujiedianArrayList = getShujiedianArrayList(strarr);
CodeNode heFuManShuroot = getHeFuManShuByList(shujiedianArrayList);
getbianmabiao(heFuManShuroot, "", stringBuilder);
byte[] zipstrarr = zip(strarr, bianmabiao);
return zipstrarr;
}
//解码操作
/**
* 传进一个字节, 输出它对应的二进制的字符串形式
* @param flag 判断该字节是否需要补高位
* @param b 要处理的字节
* @return b对应的二进制的字符串形式
*/
public static String byteToBitString(boolean flag , byte b){
//使用int变量保存byte
int temp = b; //把字节数据转成int数据
//如果不是最后一个字节, 我们要补高位 -------------这里解释不是最后一个字节为什么要补高位, 因为我们压缩的时候使用的是每八位编码变成一个字节数据来存储, 而最后一个字节有可能其实不满八位, 那也即是说
//不是最有一个字节的话, 一定要补满八位, 最后一个字节就直接变成二进制就可以,
if (flag){
temp |= 256; //按位与(相同0, 不同1), 256就是1 0000 0000;
}
String str = Integer.toBinaryString(temp); //把int变成二进制, 并且以字符串形式保存
if (flag){
//因为有与操作取后八位
return str.substring(str.length()-8);
}else{
//直接返回
return str;
}
}
/**
* //完成对哈夫曼树的解码
*
* @param huffmanCodes 哈夫曼编码表 map
* @param huffmanBytes 经过哈夫曼编码得到后的字节数组[-88,....]
* @return 就是原来字符串对应的数组
*/
public static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
//1。先得到huffmanByte对应的二进制字符串,形式1010100011.。。
StringBuilder stringBuilder = new StringBuilder();
//将byte数组转成二进制的字符串
for (int i = 0; i < huffmanBytes.length; i++) {
byte b = huffmanBytes[i];
//判断是不是最后一个字节。这里是与前面的bytetostring里面的方法对应,我们判断如果是最后一个字符,我们只需将其拼接,无需将其进行补全。
boolean flag = (i == huffmanBytes.length - 1);//为真,即不需要进行补位
stringBuilder.append(byteToBitString(!flag, b));//!flag取反
}
System.out.println("解压后的编码表的编码串是"+stringBuilder.toString());
//把字符串按照指定的哈夫曼编码进行解码
//把哈弗曼编码进行调换,因为反向查询 a->97 = 100 -> a
Map<String, Byte> map = new HashMap<>();
//哈夫曼编码进行调换
for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
//System.out.println(map + "map");
//创建要给集合.存放byte
ArrayList<Byte> list = new ArrayList<>();
//扫描stringbuilder,每一位一位的进行扫描,这里不能进行自加1,原因:i+count,
//遍历的时候,每次走count,直到匹配到哈夫曼对应的字母时才i+count,此时,i移动i+count,不是i++
for (int i = 0; i < stringBuilder.length(); ) {
int count = 1;//小的计数器
boolean flag = true;
Byte b = null;
while (flag) {
//取出一个1或者0,因为是二进制吗,啊
String key = stringBuilder.substring(i, i + count);//i不动,count进行后移,指导匹配到一个字符
b = map.get(key);
if (b == null) {
//说明没有匹配到
count++;
} else {
//匹配到
flag = false;
}
}
list.add(b);
i += count;//i直接移动到count
}
//for循环结束后,list中就放入了所有的字符,,将list中的数据放入byte[]并返回‘
byte[] b = new byte[list.size()];
for (int i = 0; i < list.size(); i++) {
b[i] = list.get(i);
}
return b;
}
}
//树节点带权值和具体是什么字符
class CodeNode implements Comparable<CodeNode>{
int quan; //出现的次数
Byte data; //具体是什么字符
CodeNode left;
CodeNode right;
public CodeNode(int quan, Byte data) {
this.quan = quan;
this.data = data;
}
//让树节点去继承comparable接口, 继承compareTo方法, 实现从小到大或者从大到小的排序
@Override
public int compareTo(CodeNode o) {
//表示从小到大排序 , 如是从大到小就是负的形式
return this.quan-o.quan;
}
@Override
public String toString() {
return "CodeNode{" +
"权值=" + quan +
", 字符=" + data +
'}';
}
//前序遍历
public void qianxu(){
System.out.println(this);
if (this.left!= null){
this.left.qianxu();
}
if (this.right!= null){
this.right.qianxu();
}
}
}
原数据的字节数组是[105, 32, 108, 105, 107, 101, 32, 121, 111, 117, 32, 108, 105, 107, 101, 32, 121, 111, 117, 32, 121, 111, 117]
原数据的字节数组长度是23
原来编码表的编码串是10000010100111111100010111001100010100111111100010111001100101110011
用于传输的字节数组为[-126, -97, -59, -52, 83, -8, -71, -105, 3]长度是9
解压后的编码表的编码串是100000101001111111000101110011000101001111111000101110011001011111
解压后的字节数组是[105, 32, 108, 105, 107, 101, 32, 121, 111, 117, 32, 108, 105, 107, 101, 32, 121, 111, 117, 32, 121, 107]
解压后的字节数组的长度是22
很明显这样的代码是有bug的呀