目录
1,赫夫曼树的介绍
- 给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree), 还有的书翻译为霍夫曼树。
- 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
- 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1。
- 结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
- 树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。
- WPL最小的就是赫夫曼树。我们以3,7,8,13举例来看,建立的二叉树中,只有第二棵树的wpl值最小,也就是我们要建立的赫夫曼树。
1.1,为什么会需要赫夫曼树
- 通信领域中信息的处理方式---定长编码
- 通信领域中信息的处理方式---变长编码
-
通信领域中信息的处理方式---赫夫曼编码
-
原始字符串:i like like like java do you like a java。
-
d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各个字符对应的个数。
-
按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值,可以知道,出现越是频繁的字符,对应的权值应该很大,那么这个字符对应的编码应该越短越好,这样可以节约字符的存储空间,提高传输的效率。
-
1.2,赫夫曼树的构建
-
构建赫夫曼树的步骤:
-
从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树。
-
取出根节点权值最小的两颗二叉树。
-
组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和。
-
再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理, 就得到一颗赫夫曼树。
-
- 下面我们以13, 7, 8, 3, 29, 6, 1这几个数字构建一棵赫夫曼树。
- 首先对数据集合进行从小到大进行排序:1, 3, 6, 7, 8, 13, 29,然后取出前两个数进行合并,合并后的节点作为这两个节点的父节点。
- 经过第一次的合并,我们得到的序列是:4,6,7,8,13,29,下面在第一次合并的基础上,再次对序列进行排序,然后取出最前面的两个数进行合并并且建立新的节点,作为父节点。
- 经过第二次合并,我们的序列变为:7,8,10,13,29,(已排序),经过多轮次的合并后,我们可以得到一棵赫夫曼树。
1.3,字符传输案例
- 带传输的字符串:i like like like java do you like a java
- 统计各个字符出现的次数:d:1, y:1, u:1 ,j:2 , v:2, o:2, l:4 , k:4 ,e:4, i:5 , a:5 ,空格 :9
- 按照上面字符出现的次数构建一颗赫夫曼树, 字符出现的次数作为权值
- 根据赫夫曼树,给各个字符,规定编码 (前缀编码), 向左的路径为0 向右的路径为1 ,
- 编码如下: o: 1000 u: 10010 d: 100110 y: 100111 i: 101 a : 110 k: 1110 e: 1111 j: 0000 v: 0001 l: 001 : 01
- 按照上面的赫夫曼编码,我们的"i like like like java do you like a java" 字符串对应的编码为 (注意这里我们使用的无损压缩) 1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110 通过赫夫曼编码处理 ,长度为 133。
- 长度为 : 133 说明: 原来长度是 359 , 压缩了 (359-133) / 359 = 62.9% 此编码满足前缀编码, 即字符的编码都不能是其他字符编码的前缀。不会造成匹配的多义性 赫夫曼编码是无损处理方案。
- 注意事项
- 注意, 这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是 wpl 是 一样的,都是最小的, 最后生成的赫夫曼编码的长度是一样,比如: 如果我们让每次生成的新的二叉树总是排在权 值相同的二叉树的最后一个,则生成的二叉树为:
2,创建赫夫曼编码
2.1,赫夫曼树的节点类型
- 这里的一个注意点就是需要对赫夫曼树的节点进行比较,所以需要实现Comparable接口,重写compareTo()方法。
//哈夫曼树的节点类型
//这里要进行排序,所以需要实现Comparable接口,重写compareTo()方法
class HuffmanNode implements Comparable<HuffmanNode>{
private Byte data;//存储字符,以二进制形式存储
private int weight;//权值,表示某一个字符出现的次数
private HuffmanNode left;
private HuffmanNode right;
public HuffmanNode(Byte arr, int weight) {
this.data = arr;
this.weight = weight;
}
public Byte getData() {
return data;
}
public int getWeight() {
return weight;
}
public HuffmanNode getLeft() {
return left;
}
public HuffmanNode getRight() {
return right;
}
public void setArr(Byte arr) {
this.data = arr;
}
public void setWeight(int weight) {
this.weight = weight;
}
public void setLeft(HuffmanNode left) {
this.left = left;
}
public void setRight(HuffmanNode right) {
this.right = right;
}
@Override
public String toString() {
return "HuffmanNode{" +
"data=" + data +
", weight=" + weight +
'}';
}
@Override
public int compareTo(HuffmanNode o) {
// 标示从小到大排序
return this.weight-o.getWeight();
}
public void preOrder(){
System.out.println(this);
if(this.left != null){
this.left.preOrder();
}
if(this.right != null){
this.right.preOrder();
}
}
}
2.1,将字节数组转换为赫夫曼树的节点类型
- 方法说明:因为我们数据是以字节数组的形式传入的,所以我们需要统计每一个字符出现的次数,然后把字符和其出现的次数封装为赫夫曼节点存储在一个列表之中再返回。
/**
* 将字节数组转换为节点类型
* @param arr 字节数组
* @return 返回一个列表,里面存储的是字节数组转换的赫夫曼树节点
*/
private static List<HuffmanNode> getNodes(byte []arr){
List<HuffmanNode> nodes=new ArrayList<HuffmanNode>();
// 遍历字节数组,统计每一个字节出现的次数
// 在这里使用map-->[key,value]的形式
HashMap<Byte,Integer>counts=new HashMap<>();
for(byte b:arr){
// 先从hashmap中获取b,看hashmap重是否有b
Integer count=counts.get(b);//获取键b对应的key值
if(count != null){
// 说明hashmap中有b,那么就向hashmap中此元素的value+1
counts.put(b,count+1);
}else {
// 也即是hashmap中没有key为b的元素,需要首次插入hashmap中
counts.put(b,1);
}
}
// 把每一个键值对转换为node存储
for(Map.Entry<Byte,Integer> entry:counts.entrySet()){
nodes.add(new HuffmanNode(entry.getKey(),entry.getValue()));
}
return nodes;
}
2.2,创建一棵赫夫曼树
- 说明:此方法是根据上面列表中的节点,来构造一棵赫夫曼树。
/**
* 通过传入的集合创建一颗哈夫曼树
* @param nodes 节点的列表
* @return 返回创建的哈夫曼树的根节点
*/
private static HuffmanNode crateHuffman(List<HuffmanNode> nodes){
while (nodes.size()>1){
Collections.sort(nodes);
// 获取左子节点
HuffmanNode leftNode=nodes.get(0);
HuffmanNode rightNode=nodes.get(1);
// 创建一个父节点
HuffmanNode parentNode=new HuffmanNode(null,leftNode.getWeight()+rightNode.getWeight());
parentNode.setLeft(leftNode);
parentNode.setRight(rightNode);
// 删除节点
nodes.remove(leftNode);
nodes.remove(rightNode);
// 插入父节点
nodes.add(parentNode);
}
return nodes.get(0);
}
2.3,构建赫夫曼编码表
- 说明:上面我们已经创建可一棵赫夫曼树,但是我们还没有对其进行编码,所以此方法就是对上面的赫夫曼树进行编码,以字典的形式返回,字典的键是字符,值是字符对应的赫夫曼编码。(注意这里应该是一个变长的编码)
/**
* 将传入的节点的所有叶子节点编码为哈夫曼编码,并且放入huffmancode的集合
* @param node 传入节点
* @param code 路径,左节点是0,右节点是1
* @param stringBuilder 用于拼接路径
*/
private static Map<Byte,String> getCode(HuffmanNode node,String code,StringBuilder stringBuilder){
StringBuilder stringBuilder1=new StringBuilder(stringBuilder);//这里是为什么
// 首先将code加入stringBuilder中
stringBuilder1.append(code);
//如果node是null就不做处理
if(node != null){
// 判断当前节点是叶子结点还是非叶子节点
if(node.getData() == null){
//说明是非叶子节点
// 先向左递归
getCode(node.getLeft(),"0",stringBuilder1);
// 向右边递归
getCode(node.getRight(),"1",stringBuilder1);
}else {
// 说明是一个叶子节点
huffmanCode.put(node.getData(),stringBuilder1.toString());
}
}
return huffmanCode;
}
/**
* 遍历哈夫曼树,是对上面的方法的重载版本
* @param root 哈夫曼树的根节点
* @return 返回一个哈夫曼编码表
*/
private static Map<Byte,String>getCode(HuffmanNode root){
if(root == null){
System.out.println("哈夫曼树是空树!");
}
// 递归处理左子树
getCode(root.getLeft(),"0",stringBuilder);
// 递归处理右子树
getCode(root.getRight(),"1",stringBuilder);
return huffmanCode;
}
2.4,压缩数据
- 通过上面的步骤,我们已经构建出来我们的赫夫曼编码表,下面我们就根据原始数据的byte数组取构建经过赫夫曼编码压缩的byte数组,此方法返回的就是经过压缩的数据。
/**
*将字符串对应的字节数组,通过生成的哈夫曼表,返回一个压缩后的byte数组
* @param bytes 原始的字符串对应的byte数组
* @param huffmancodes 生成的哈夫曼编码,是一个map
* @return 返回使用哈夫曼编码生成的byte数组
*/
private static byte[] zip(byte []bytes,Map<Byte,String>huffmancodes){
// 现在利用huffmancodes将bytes数组转换为哈夫曼编码
StringBuilder stringBuilder=new StringBuilder();
for(byte b:bytes){
stringBuilder.append(huffmancodes.get(b));
}
// 将字符串转换为byte数组
// 求转换后字节数组的长度
int len=0;
// 使用一句话表示
// len=(stringBuilder.length()+7)/8;
if(stringBuilder.length()% 8==0){
len=stringBuilder.length()/8;
}else {
len=stringBuilder.length()/8+1;
}
// 声明一个存放字节数组的Byte数组
byte []huffmanByteCodes=new byte[len];
int index=0;
for(int i=0;i<stringBuilder.length();i+=8){
String strByte;
if(i+8>stringBuilder.length()){
// 最后面的几位不够8位
strByte=stringBuilder.substring(i);
}else {
strByte=stringBuilder.substring(i,i+8);//左闭右开
}
// 把8位二进制字符串转换为一个二进制
huffmanByteCodes[index++]=(byte)Integer.parseInt(strByte,2);
}
return huffmanByteCodes;
}
2.5,压缩数据方法封装
- 说明:传入需要压缩字符串的字节数组,返回经过赫夫曼编码压缩后的字节数组。
/**
* 对给定字符串的byte数组转换为哈夫曼字节数组
* @param bytes 原始字符串的字节数组
* @return 编码后的字节数组
*/
private static byte[] HuffmanZip(byte [] bytes){
// 将字节数组转换为节点类型
List<HuffmanNode>nodes=getNodes(bytes);
// 创建一颗哈夫曼树
HuffmanNode node=crateHuffman(nodes);
// 创建哈夫曼编码
Map huffmanCodes=getCode(node,"",stringBuilder);
// 返回压缩后的字节数组
return zip(bytes,huffmanCodes);
}
2.6,遍历操作
/**
* 遍历哈夫曼树,是对上面的方法的重载版本
* @param root 哈夫曼树的根节点
* @return 返回一个哈夫曼编码表
*/
private static Map<Byte,String>getCode(HuffmanNode root){
if(root == null){
System.out.println("哈夫曼树是空树!");
}
// 递归处理左子树
getCode(root.getLeft(),"0",stringBuilder);
// 递归处理右子树
getCode(root.getRight(),"1",stringBuilder);
return huffmanCode;
}
2.7,解压缩
2.7.1,将byte转换为字符串
- 说明:我们在进行解压缩的时候,需要先把每一个byte转换为字符串,对于不够8位的我们还需要做补齐操作,这是因为我们在编码的时候都是按照8位进行的编码。
/**
* 将一个byte转换为一个二进制字符串
* @param bytes byte数据
* @param flag 标示是否需要补高位,如果是true,就需要补高位,否则不需要补高位
* 也就是对于一串数字,中间的数字转换为二进制不够8位的话,需要补足8位,但是最后一个数字不需要补足8位
* @return 返回bytes对应的二进制字符串(注意是按照补码返回的)而编码的时候也是按照补码的形式进行编码
*/
private static String byteToString(boolean flag,byte bytes){
// 使用一个整形变量保存b,也就是将byte数组转换为整数
int temp=bytes;
// 如果是正数,我们还需要补高位
if(flag){
temp |=256;
}
// 这里返回的补码是32位
String str=Integer.toBinaryString(temp);//注意,这里返回的是二进制的补码
// 返回最后面的8位
if(flag){
return str.substring(str.length()-8);
}else {
return str;
}
}
2.7.2,解压缩操作
/**
* 对哈夫曼编码进行解码操作
* @param huffmanCode 哈夫曼编码表
* @param huffmanCodes 哈夫曼编码后的字节数组
* @return 解码后的字节数组
*/
private static byte[]decode(Map<Byte,String>huffmanCode,byte []huffmanCodes){
// 先得到哈夫曼对应的二进制字符串
StringBuilder stringBuilder=new StringBuilder();
for(int i=0;i<huffmanCodes.length;i++){
byte b=huffmanCodes[i];
// 判断是不是最后一个字节
boolean flag=( i == huffmanCodes.length-1);
stringBuilder.append( byteToString(!flag,b));
}
//把哈夫曼编码字符串按照指定的哈夫曼表进行解码操作
Map<String,Byte>map=new HashMap<String,Byte>();
for(Map.Entry<Byte,String> entry:huffmanCode.entrySet()){
map.put(entry.getValue(),entry.getKey());
}
// 创建一个集合,存放我们的byte
List<Byte> list=new ArrayList<Byte>();
for(int i=0;i<stringBuilder.length(); ){
int count=1;//计数器
boolean flag=true;//标志是否读取到一个字符
Byte b=null;
while (flag){
String key=stringBuilder.substring(i,i+count);//i不动,一直向后面匹配字符
b=map.get(key);//判断map中是否有一个key的值
if(b == null)//已经匹配到
count++;
else//还没有匹配到
flag=false;
}
list.add(b);
i+=count;//移动到count位置进行下一次比较
}
// 当for循环结束后,list存放了所有的字符
// 把list集合中的数组放入byte数组
byte []b=new byte[list.size()];
for(int j=0;j<b.length;j++)
{
b[j]=list.get(j);
}
return b;
}
3,文件压缩
3.1,文件压缩
/**
* 对文件进行压缩
* @param srcFile 需要压缩文件的目录
* @param dstFile 压缩后文件的目录
*/
private static void zipFile(String srcFile,String dstFile) {
// 创建文件读取输入流
InputStream fileInputStream=null;
OutputStream outputStream=null;
ObjectOutputStream objectOutputStream=null;
try {
fileInputStream=new FileInputStream(srcFile);
// available()返回文件的大小
byte bytes[]=new byte[fileInputStream.available()];
// 把文件内容读取到字节数组
fileInputStream.read(bytes);
// 直接对源文件进行压缩,返回压缩后的byte数组
byte []zipCode=HuffmanZip(bytes);
// 把文件输出
outputStream=new FileOutputStream(dstFile);
// 创建一个和文件输出关联的objectOutputStream
objectOutputStream=new ObjectOutputStream(outputStream);
// 使用ObjectOutputStream,这样可以直接输出一个数组,以对象流的方式写入哈夫曼编码,目的是为了后面恢复源文件使用
// 把哈夫曼编码后的字节数组写入压缩文件
objectOutputStream.writeObject(zipCode);
// 注意,这里一定要把哈夫曼编码写入压缩文件
objectOutputStream.writeObject(huffmanCode);//这里写入的是哈夫曼编码表,写入压缩文件
}catch (Exception e){
e.printStackTrace();
}finally {
try {
fileInputStream.close();
objectOutputStream.close();
outputStream.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
3.2,文件解压缩
/**
* 对文件进行解压缩
* @param srcFile 原文件的文件目录
* @param dstFile 压缩后的文件存储目录
*/
private static void unZipFile(String srcFile,String dstFile){
// 因为在压缩的时候,我们是以对象字节流的形式把文件字节流和哈夫曼字节流保存起来,所以可以直接进行解压缩
// 定义文件输入流
InputStream inputStream=null;
// 定义一个对象输入流
ObjectInputStream objectInput=null;
// 定义文件输出流
OutputStream outputStream=null;
try {
inputStream=new FileInputStream(srcFile);
// 创建对象输出流
objectInput=new ObjectInputStream(inputStream);
// 读取byte文件数组
byte huffman[]=(byte [])objectInput.readObject();
// 读取哈夫曼编码表,按照存入文件时候的顺序读取即可
Map<Byte,String >huffmanCodes=(Map<Byte, String>) objectInput.readObject();
// 对文件进行解码操作
byte[]sourseByte= decode(huffmanCodes,huffman);
// 定义文件输出流
outputStream=new FileOutputStream(dstFile);
// 写出数据到文件
outputStream.write(sourseByte);
}catch (Exception e){
e.printStackTrace();
}finally {
// 关闭操作和打开文件流顺序相反操作
try {
outputStream.close();
objectInput.close();
inputStream.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
4,测试代码
注:以上所有代码需要放在HuffmanCodeDemo 这个类中。
public class HuffmanCodeDemo {
public static void main(String[] args) {
String context="i like like like java do you like a java";
// 将字符串转换为字节数组
byte []contextBytes=context.getBytes();
// System.out.println(Arrays.toString(arr));
// 将字节数组转换为节点类型
List<HuffmanNode>nodes=getNodes(contextBytes);
// System.out.println(nodes);
// 创建一颗哈夫曼树
HuffmanNode node=crateHuffman(nodes);
// preOrder(node);
// 创建哈夫曼编码
Map huffmanCodes=getCode(node,"",stringBuilder);
// System.out.println(getCode(node));
// 对元素数组进行压缩
byte []huffmanCodeArr=zip(contextBytes,huffmanCodes);
// System.out.println(Arrays.toString(huffmanCodeArr));
// 封装过的函数
// byte []zipCode=HuffmanZip(contextBytes);
// 对哈夫曼编码进行解码
byte sourceCode[]= decode(huffmanCodes,huffmanCodeArr);
System.out.println(new String(sourceCode));
// 文件的压缩
zipFile("H://word.txt","H://word.zip");
// 对文件进行解压缩
unZipFile("H://word.zip","H://word1.txt");
}
// 存放哈夫曼编码
private static HashMap<Byte,String> huffmanCode=new HashMap<>();
// 在生成哈夫曼树的过程中,需要去拼接路径,stringBuilder用于拼接路径
static StringBuilder stringBuilder=new StringBuilder();
}