1. Huffman Tree(最优二叉树)定义
Huffman Tree也是一种特殊的二叉树,这种树的所有叶子结点都带有权值,从中构造出带权路径长度最短的二叉树,即Huffman Tree。
所谓路径
是指:在一棵二叉树中,定义从A结点到B结点所经过的分支序列为从A结点到B结点的路径;所谓路径长度
是指:从A结点到B结点所经过的分支个数;从二叉树的根节点到二叉树中所有节点的路径长度之和为二叉树的路径长度
。
设二叉树有n个带权值的叶结点,那么从二叉树的根结点到所有叶子结点的路径长度与相应节点权值的乘积之和为该二叉树的带权路径长度
,记作:WPL。
如果给定一组具有确定权值的叶子结点,可以构造出不同的带权二叉树,他们的带权路径并不相同。把其中最小的带权路径长度的二叉树称为哈夫曼树。
2. 构造Huffman Tree
根据哈夫曼树的定义,一棵二叉树要使其WPL值最小,必须使权值最大的叶子节点越靠近根节点,而权值最小的叶子结点越远离根节点。那么如何构造呢?
- 用给定的权值的n个结点构造n棵只有一个叶子结点的二叉树,从而得到一个二叉树的集合。
- 在集合中选取权值最小和次小的两颗二叉树作为左、右子树构造一颗新的二叉树。这颗新的二叉树根节点的权值为其左、右子树根结点权值之和。
- 在集合中删除作为左右子树的两棵二叉树,并将新建立的二叉树加入到集合F中;
- 重复2、3步,当F中只剩下一棵二叉树时,这颗二叉树便是所要建立的哈夫曼树。
图解:
代码(java)
/**
* @author Emma
* @create 2020 - 04 - 03 - 10:15
* 结点
*/
public class Node implements Comparable<Node>{
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
public int compareTo(Node o){
return -(this.value - o.value);
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
", left=" + left +
", right=" + right +
'}';
}
}
/**
* @author Emma
* @create 2020 - 04 - 03 - 10:16
* 测试代码
*/
public class Test {
public static void main(String[] args) {
int[] arr = {3,7,8,29,5,11,23,14};
Node node = createHuffmanTree(arr);
System.out.println(node);
}
//创建哈夫曼树
public static Node createHuffmanTree(int[] arr){
//创建n个二叉树,并放入集合
List<Node> nodes = new ArrayList<>();
for(int value : arr){
nodes.add(new Node(value));
}
while(nodes.size() > 1){
//排序(从大到小排序,最小的在后面
Collections.sort(nodes);
//取出权值最小的两个二叉树
Node left = nodes.get(nodes.size()-1);
Node right = nodes.get(nodes.size()-2);
//创建一颗新的二叉树
Node parent = new Node(left.value+right.value);
parent.left = left;
parent.right = right;
//删除结点
nodes.remove(left);
nodes.remove(right);
//放入原来的二叉树集合中
nodes.add(parent);
}
return nodes.get(0);
}
}
3. 哈夫曼编码
哈夫曼编码在压缩文件方面有着很重要的用途,首先利用一个例子来理解一下哈夫曼编码。
假设我要传递一串字符串:“can you can a can as a can canner can a can.”,计算机在发送字符串的时候并不能直接发送,因为两个台计算机在交互时是以二进制的方式。所以:首先得把字符串按照某种编码格式(这里是ASCII码)转为数字:“99 97 110 32 121 111 117 32 99 97 110 32 97 32 99 97 110 32 97…”,然后再将数字转化为二进制数字传递:“01100011 01100001 01101110 00100000 01111001 01101111 01110101 00100000 01100011 01100001 01101110 00100000 01100001 00100000 01100011 01100001 01101110…”(实际传递的时候是两个数字之间是没有空格的)这样的话,这么一句短短的字符串“can you can a can as a can canner can a can.”在实际传递的时候需要传递396个二进制数字。
还是传递这一句字符串“can you can a can as a can canner can a can.”,然后计算一下每个字符出现的次数:“a(11), 空格(11), n(8), c(7), o(1), .(1), y(1), e(1), u(1), s(1), r(1)”;然后按照字符出现的频率我们来给每一个字符分配一个对应的二进制数字,频率越高的分配的数字越小,比如:“a(0), 空格(1), n(10), c(11), o(100), .(101), y(110), e(111),u(1000), s(1001), r(1010) ”然后按照这种方式来重组字符串:“比如can = 11010, you = 1101001000…”这样的话,在传递字符串“can you can a can as a can canner can a can.”时就会短很多。
上面我们是按照字符出现的频率来安排每个字符对应的二进制,然而对于哈夫曼编码来说,对于每个字符以及出现的次数:“a(11), 空格(11), n(8), c(7), o(1), .(1), y(1), e(1), u(1), s(1), r(1)”;我们会把每一个字符都作为一个叶子结点,并以出现的次数为权值创建一颗哈夫曼树。在新创建的哈夫曼树中约定:指向左子树的分支表示“0”码,指向右子树的分支表示“1”码,又因为从根结点到每个叶子结点都有一条唯一路径,取这条路径上的“0/1”的序列作为各个叶子结点对应字符的二进制序列,这就是哈夫曼编码。
图解哈夫曼编码
假设要传递的字符串是:“CAST TAT A SA”,统计字母的频度:f( C) = 1, f(S) = 2, f(T) = 3, f(空格) = 3,f(A) = 4。然后生成哈夫曼树如下,那么字符c对应的二进制数字 = 000,字符a对应的二进制数字 = 11…然后按照这个重新编码字符串:CAST = 0001100101, TAT = 011101, A = 11, SA = 00111,将00011001011001110110111000111传递过去。
代码
/**
* @author Emma
* @create 2020 - 04 - 03 - 16:11
*/
public class Node implements Comparable<Node>{
Byte data;//字符,希望可以为空,所以用封装类
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 o.weight-this.weight;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
}
/**
* @author Emma
* @create 2020 - 04 - 03 - 23:14
*/
public class Test2 {
//全局变量:哈夫曼编码表
static Map<Byte, String> huffcodes = new HashMap<>();
public static void main(String[] args) {
String msg = "can you can a can as a can canner can a can.";//"CAST TAT A SA";//
//将字符串转化为对应的byte数组
byte[] bytes = msg.getBytes();//存储每一个字符对应的ascii码
//哈夫曼编码,返回值为byte数组
byte[] bytes1 = huffmanCode(bytes);
for(byte b : bytes1){
System.out.println(b);
}
}
//哈夫曼编码
private static byte[] huffmanCode(byte[] bytes) {
//统计字符出现次数并放入集合
List<Node> nodes = getNodes(bytes);
//根据集合创建哈夫曼树
Node tree = createHuffmanTree(nodes);
//根据哈夫曼树创建一个哈夫曼编码表
Map<Byte, String> codes = getCodes(tree);
//根据编码表对原字符串进行编码
byte[] b = zip(bytes, codes);
//将结果返回
return b;
}
//根据编码表对原字符串进行编码
private static byte[] zip(byte[] bytes, Map<Byte, String> codes) {
//将字符串对应的哈夫曼编码放入字符串temp中
StringBuilder temp = new StringBuilder();
for(byte b : bytes){
temp.append(codes.get(b));
}
//将temp中的内容按照每8位分割为一个byte数,存储在result集合中
//1.计算出byte数组的容量大小
int t = temp.length()%8;//用于记录最后剩余几个数字,以防最后是0开头
int c = temp.length()/8;
int size = (t == 0 ? c+1 : c+2);//在result中的最后一位用于存储最后剩余几个数字
byte[] result = new byte[size];
//2. 将temp中的内容按照每8位放入一个byte中
int index = 0;//用于记录result的下标
for(int i = 0; i <temp.length(); i+=8){
String tmp;
if(i+8 > temp.length()){
//超过边界
tmp = temp.substring(i);
}else{
//没超过边界就8个放入其中
tmp = temp.substring(i, i+8);
}
//将临时字符串tmp转化为byte放入result中
result[index++] = (byte)Integer.parseInt(tmp,2);//将二进制转化为10进制后,再转化为byte
}
result[index] = (byte)t;//将后剩余几个数字记录一下
return result;
}
//根据哈夫曼树创建一个哈夫曼编码表
private static Map<Byte, String> getCodes(Node tree) {
if(tree == null){
return null;
}
//需要遍历树,递归
StringBuilder sb = new StringBuilder();//用来保存哈夫曼编码
getCodes(tree.left, "0",sb);//递归左节点
getCodes(tree.right, "1",sb);//递归右节点
return huffcodes;
}
private static void getCodes(Node node, String code, StringBuilder sb) {
StringBuilder temp = new StringBuilder(sb);//获取上一步的哈夫曼编码
temp.append(code);
if(node.data == null){
//说明不是叶子结点,需要接着向下
getCodes(node.left, "0", temp);
getCodes(node.right, "1", temp);
}else{
//是叶子结点,就将节点放入哈夫曼编码表
huffcodes.put(node.data, temp.toString());
}
}
//根据List集合中的结点创建哈夫曼树
private static Node createHuffmanTree(List<Node> nodes) {
while(nodes.size() > 1){
//排序
Collections.sort(nodes);
//取出权值最低的二叉树
Node left = nodes.get(nodes.size()-1);
Node right = nodes.get(nodes.size()-2);
//构建新树
Node parent = new Node(null, left.weight+right.weight);
parent.left = left;
parent.right = right;
//从集合中删除两节点
nodes.remove(left);
nodes.remove(right);
//将新树添加到集合中
nodes.add(parent);
}
return nodes.get(0);//返回这棵树
}
//统计byte数组中每个元素出现的次数,并将结果转化为Node
private static List<Node> getNodes(byte[] bytes) {
//用于保存Node集合
List<Node> nodes = new ArrayList<>();
//统计每个字符出现的次数
Map<Byte, Integer> counts = new HashMap<>();
for(byte b : bytes){
Integer value = counts.get(b);
//判断当前b是否已经存在
if(counts.containsKey(b)){
//已经存在了 value++
counts.put(b,value+1);
}else{
//不存在
counts.put(b,1);
}
}
//拿到每一个键值对并转为node类型
for(Map.Entry<Byte, Integer> entry : counts.entrySet()){
nodes.add(new Node(entry.getKey(), entry.getValue()));
}
return nodes;
}
}
4. 哈夫曼解码
/**
* @author Emma
* @create 2020 - 04 - 03 - 23:14
*/
public class Test2 {
public static void main(String[] args) {
String msg = "can you can a can as a can canner can a can.";//"CAST TAT A SA";//
//将字符串转化为对应的byte数组
byte[] bytes = msg.getBytes();//存储每一个字符对应的ascii码
//哈夫曼编码,返回值为byte数组
byte[] bytes1 = huffmanCode(bytes);
for(byte b : bytes1){
System.out.println(b);
}
System.out.println("--------------------");
//解码
byte[] bytes2 = decode(huffcodes, bytes1);
//再把这个数组转化为字符串
String re = new String(bytes2);
System.out.println(re);
}
//使用指定的哈夫曼编码表进行解码
private static byte[] decode(Map<Byte, String> huffcodes, byte[] bytes) {
StringBuilder sb = new StringBuilder();
//把byte数组转化为一个二进制的字符串
for(int i = 0; i<bytes.length -1; i++){
byte b = bytes[i];
//对于byte中的元素,只要不是倒数第2个和倒数第一个就正常处理
if(i != bytes.length -2){
//正常处理
sb.append(byteToBitStr(b, 8));
}else{
sb.append(byteToBitStr(b, (int)bytes[bytes.length -1]));
}
}
//把这个字符串按照哈夫曼编码表进行解码
//1.把哈夫曼树的键值对进行调换
Map<String, Byte> map = new HashMap<>();
for(Map.Entry<Byte, String> entry : huffcodes.entrySet()){
map.put(entry.getValue(), entry.getKey());
}
//创建一个集合来存储byte
List<Byte> list = new ArrayList<>();
//2.处理字符串
for(int i = 0; i<sb.length();){
int count = 1;
boolean flag = true;
Byte b = null;
while(flag){
String key = sb.substring(i, i+count);//从sb中开始取
b = map.get(key);
if(b == null){
//没有取到
count++;
}else{
flag = false;
}
}
list.add(b);
i += count;
}
//把集合转为数组
byte[] by = new byte[list.size()];
for(int i = 0; i<by.length; i++){
by[i] = list.get(i);
}
return by;
}
//将输入的byte转化为8位二进制
private static String byteToBitStr(byte b){
int temp = b;
temp |= 256;//与256按位或,就是在前面补0
String str = Integer.toBinaryString(temp);
return str.substring(str.length()-8);
}
//将输入的byte转化为二进制,不一定是8位,位数取决于b2
private static String byteToBitStr(byte b, int b2){
int temp = b;
temp |= 256;//与256按位或,在前面补齐0
String str = Integer.toBinaryString(temp);
return str.substring(str.length()-b2);
}
}
5. 利用哈夫曼编码对文件进行压缩与解压缩
/**
* @author Emma
* @create 2020 - 04 - 03 - 23:14
*/
public class Test2 {
public static void main(String[] args) {
//压缩文件
String src = "Day02/other/test.txt", dst="Day02/other/zip.zip";
try {
zipFile(src, dst);
} catch (IOException e) {
e.printStackTrace();
}
//解压文件
String src1 = "Day02/other/zip.zip", dst1="Day02/other/zip.txt";
try {
unzipFile(src1, dst1);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
//压缩文件
public static void zipFile(String src, String dst) throws IOException {
InputStream is = new FileInputStream(src);
byte[] b = new byte[is.available()];//创建一个byte数组,大小与输入文件的字节数相等
//给b中读取文件内容
is.read(b);
is.close();
//进行赫夫曼编码进行压缩
byte[] zip = huffmanCode(b);
//输出流:实际上就是一个压缩包,不仅要保存压缩后的文件,还得有哈夫曼编码表
OutputStream os = new FileOutputStream(dst);
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(zip);
oos.writeObject(huffcodes);
oos.close();
os.close();
}
//解压文件
public static void unzipFile(String src, String dst) throws IOException, ClassNotFoundException {
//先读取文件内容
InputStream is = new FileInputStream(src);
ObjectInputStream ois = new ObjectInputStream(is);
//读取文件
byte[] b = (byte[])ois.readObject();
//读取哈夫曼编码表
Map<Byte, String> code = (Map<Byte, String>)ois.readObject();
ois.close();
is.close();
//解码
byte[] bytes = decode(code, b);
//将解码的文件写入dst中
OutputStream os = new FileOutputStream(dst);
os.write(bytes);
os.close();
}
}