數據結構與算法學習十九:赫夫曼樹樹(圖文很詳細)、赫夫曼編碼、應用實踐(數據壓縮、數據解壓)、這一章自我感覺看懂就好。。。

前言

一、赫夫曼樹

1.1 基本介紹

  1. 給定 n 個權值作爲 n 個葉子結點,構造一棵二叉樹,若該樹的帶權路徑長度(wpl)達到最小,稱這樣的二叉樹爲 最優二叉樹,也稱爲哈夫曼樹(Huffman Tree), 還有的書翻譯爲霍夫曼樹。

  2. 赫夫曼樹是帶權路徑長度最短的樹權值較大的結點離根較近

1.2 赫夫曼樹的概念

  1. 路徑和路徑長度:在一棵樹中,從一個結點往下可以達到的孩子或孫子結點之間的通路,稱爲路徑。通路中分支的數目稱爲路徑長度。若規定根結點的層數爲1,則從根結點到第L層結點的路徑長度爲L-1

  2. 結點的權及結點帶權路徑長度:若將樹中結點賦給一個有着某種含義的數值,則這個數值稱爲該結點的權結點的帶權路徑長度爲:從根結點到該結點之間的路徑長度與該結點的權的乘積

  3. 樹的帶權路徑長度:樹的帶權路徑長度規定爲所有葉子結點的帶權路徑長度之和,記爲 WPL(weighted path length) ,權值越大的結點離根結點越近的二叉樹纔是最優二叉樹。

  4. WPL最小的就是赫夫曼樹

  5. 舉例說明,中間的 wpl 最小 爲 59,所以中間的纔是最優二叉樹,也是赫夫曼樹

在這裏插入圖片描述

1.3 思路圖解分析

1.3.1 案例

給定一個數列 {13, 7, 8, 3, 29, 6, 1},要求轉成一顆赫夫曼樹

1.3.2 步驟分析

在這裏插入圖片描述
圖中文字:
構成赫夫曼樹的步驟:

  1. 從小到大進行排序, 將每一個數據,每個數據都是一個節點 , 每個節點可以看成是一顆最簡單的二叉樹
  2. 取出根節點權值最小的兩顆二叉樹
  3. 組成一顆新的二叉樹, 該新的二叉樹的根節點的權值是前面兩顆二叉樹根節點權值的和
  4. 再將這顆新的二叉樹,以根節點的權值大小 再次排序, 不斷重複 1-2-3-4 的步驟,直到數列中,
    5)所有的數據都被處理,就得到一顆赫夫曼樹

1.3.3 圖文分析

  1. 第一步 排序,將初始數組 array={13, 7, 8, 3, 29, 6, 1} 排序 爲 array={1, 3, 6, 7, 8, 13, 29 }
  2. 第二步 取出兩個最小的子樹
  3. 第三步 組成新的二叉樹
    在這裏插入圖片描述
  4. 第四步 再將這顆新的二叉樹,以根節點的權值大小 再次排序,如下圖所示: 在這裏插入圖片描述
  5. 重複第二步(取兩個最小子樹)、第三步(組成新樹)、第四步(排序),再將這顆新的二叉樹,以根節點的權值大小 再次排序 ,如下圖所示。
    這次取出 結點 4 和結點 6,結合成結點 10 ,在進行排序。
    在這裏插入圖片描述
  6. 然後重複第二步(取兩個最小子樹)、第三步(組成新樹)、第四步(排序),再將這顆新的二叉樹,以根節點的權值大小 再次排序 ,如下圖所示。
    這次取出 結點 7 和結點8,結合成結點 15 ,在進行排序。
    在這裏插入圖片描述
  7. 然後重複第二步(取兩個最小子樹)、第三步(組成新樹)、第四步(排序),再將這顆新的二叉樹,以根節點的權值大小 再次排序 ,如下圖所示。
    這次取出 結點 10 和結點13,結合成結點 23 ,在進行排序。
    在這裏插入圖片描述
  8. 然後重複第二步(取兩個最小子樹)、第三步(組成新樹)、第四步(排序),再將這顆新的二叉樹,以根節點的權值大小 再次排序 ,如下圖所示。
    這次取出 結點 15 和結點23,結合成結點 38 ,在進行排序。最後在於 29 進行排序,組成新的子樹,到了這裏就成了一個二叉樹了,也就是最優二叉樹、也是赫夫曼樹。
    在這裏插入圖片描述

1.4 代碼實現

1.4.1 Node 節點類

package com.feng.ch13_huffmantree;

/*
* 創建節點類
* // 爲了讓 Node 對象 持續排序 Collection 集合排序
* 讓 Node 實現 Comparable 接口,重寫 compareTo() 方法
* */
public class Node implements Comparable<Node> {

    private int value;// 結點權值
    private Node left; // 指向左子結點
    private Node right; // 指向右子結點

    public Node(int value) {
        this.value = value;
    }

    // 前序遍歷
    public void preOrder(){
        System.out.println(this);
        if (this.left != null){
            this.left.preOrder();
        }
        if (this.right != null){
            this.right.preOrder();
        }
    }

    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public Node getLeft() {
        return left;
    }

    public void setLeft(Node left) {
        this.left = left;
    }

    public Node getRight() {
        return right;
    }

    public void setRight(Node right) {
        this.right = right;
    }

    @Override
    public int compareTo(Node o) {
        // 表示從小到大進行排序  -(this.value - o.value):從大到小排列
        return this.value - o.value;
    }
}

1.4.2 HuffmanTree赫夫曼樹類

package com.feng.ch13_huffmantree;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/*
 * 哈弗曼樹
 * 構成赫夫曼樹的步驟:
 * 1、從小到大進行排序, 將每一個數據,每個數據都是一個節點 , 每個節點可以看成是一顆最簡單的二叉樹
 * 2、取出根節點權值最小的兩顆二叉樹
 * 3、組成一顆新的二叉樹, 該新的二叉樹的根節點的權值是前面兩顆二叉樹根節點權值的和
 * 4、再將這顆新的二叉樹,以根節點的權值大小 再次排序, 不斷重複  1-2-3-4 的步驟,直到數列中,所有的數據都被處理,就得到一顆赫夫曼樹
 *
 * 注意點:這裏使用 ArrayList 集合 儲存 數組的元素,表示每個元素爲一個二叉樹,這裏僅保存根節點
 * */
public class HuffmanTree {

    public static void main(String[] args) {
        int array[] = {13, 7, 8, 3, 29, 6, 1};
        Node root = createHuffmanTree(array);

        // 測試一把
        preOrder(root);
    }

    // 前序遍歷
    public static void preOrder(Node root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("是空樹,不能遍歷~~~");
        }
    }

    /*
     * 創建 哈弗曼樹的方法
     * @param array 需要創建成哈弗曼樹的數組
     * @return 創建好的哈弗曼樹root 結點
     * */
    public static Node createHuffmanTree(int[] array) {
        /*
         * 第一步,爲了操作方便
         * 1、遍歷 array 數組
         * 2、將 array 的每個元素構成 一個 Node
         * 3、將 Node 放入到 ArrayList 中
         * */
        List<Node> nodes = new ArrayList<>();
        for (int value : array) {
            nodes.add(new Node(value));
        }

        /*
         * 第二步:
         * 處理過程 是循環過程
         * 1、從小到大進行排序, 將每一個數據,每個數據都是一個節點 , 每個節點可以看成是一顆最簡單的二叉樹
         * 2、取出根節點權值最小的兩顆二叉樹
         * 3、組成一顆新的二叉樹, 該新的二叉樹的根節點的權值是前面兩顆二叉樹根節點權值的和
         * 4、再將這顆新的二叉樹,以根節點的權值大小 再次排序, 不斷重複  1-2-3-4 的步驟,直到數列中,所有的數據都被處理,就得到一顆赫夫曼樹
         * 最後,循環完後,list集合中,只有一個數據,也就是哈弗曼樹的根節點。
         * */
        while (nodes.size() > 1) {
            // 排序 從小到大
            Collections.sort(nodes);
//            System.out.println("nodes=" + nodes); //nodes=[Node{value=1}, Node{value=3}, Node{value=6}, Node{value=7}, Node{value=8}, Node{value=13}, Node{value=29}]

            // 取出根節點權值最小的兩顆二叉樹
            // 1、取出權值最小的結點(二叉樹)
            Node leftNode = nodes.get(0);
            // 2、取出權值第二小的結點(二叉樹)
            Node rightNode = nodes.get(1);

            // 3、構建一顆新的二叉樹
            Node parent = new Node(leftNode.getValue() + rightNode.getValue());
            parent.setLeft(leftNode);
            parent.setRight(rightNode);

            // 4、從 ArrayList 中刪除 處理過的二叉樹
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            // 5、將parent 加入到 nodes
            nodes.add(parent);
//            System.out.println("第一次處理後:" + nodes);
        }

        // 返回 哈弗曼樹的 root 結點
        return nodes.get(0);
    }
}

二、赫夫曼編碼

2.1 基本介紹

  1. 赫夫曼編碼也翻譯爲 哈夫曼 編碼(Huffman Coding),又稱霍夫曼編碼,是一種編碼方式, 屬於一種程序算法

  2. 赫夫曼編碼是赫哈夫曼樹在電訊通信中的經典的應用之一。

  3. 赫夫曼編碼 廣泛地用於數據文件壓縮。其壓縮率通常在20%~90%之間

  4. 赫夫曼碼是 可變字長編碼(VLC)的一種。Huffman於1952年提出一種編碼方法,稱之爲最佳編碼

2.2 原理剖析

2.2.1 通信領域中信息的處理方式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 (包括空格)

  • 在線轉碼 工具 :https://www.mokuge.com/tool/asciito16/

2.2.2 通信領域中信息的處理方式1-變長編碼

  • i like like like java do you like a java // 共40個字符(包括空格)

  • 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=a, 10=i, 11=e, 100=k, 101=l, 110=o, 111=v, 1000=j, 1001=u, 1010=y, 1011=d 說明:按照各個字符出現的次數進行編碼,原則是出現次數越多的,則編碼越小,比如 空格出現了9 次, 編碼爲0 ,其它依次類推.

  • 按照上面給各個字符規定的編碼,則我們在傳輸 “i like like like java do you like a java” 數據時,編碼就是 10010110100…

  • 字符的編碼都不能是其他字符編碼的前綴,符合此要求的編碼叫做 前綴編碼, 即不能匹配到重複的編碼(這個在赫夫曼編碼中,我們還要進行舉例說明, 不捉急)

2.2.3 通信領域中信息的處理方式1-赫夫曼編碼

  • i like like like java do you like a java // 共40個字符(包括空格)

  • d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9 // 各個字符對應的個數

  • 按照上面字符出現的次數構建一顆赫夫曼樹, 次數作爲權值.(圖後)
    在這裏插入圖片描述
    在這裏插入圖片描述
    注意, 這個赫夫曼樹根據排序方法不同,也可能不太一樣,這樣對應的赫夫曼編碼也不完全一樣,但是wpl 是一樣的,都是最小的, 比如: 如果我們讓每次生成的新的二叉樹總是排在權值相同的二叉樹的最後一個,則生成的二叉樹爲:
    在這裏插入圖片描述

三、最佳實踐-數據壓縮

3.1 案例說明

將給出的一段文本,比如 “i like like like java do you like a java” , 根據前面的講的赫夫曼編碼原理,對其進行數據壓縮處理 ,形式如 “1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110”

3.2 步驟一 創建赫夫曼樹

步驟1:根據赫夫曼編碼壓縮數據的原理,需要創建 “i like like like java do you like a java” 對應的赫夫曼樹.

3.3 代碼實現

3.3.1 Node類

package com.feng.ch14_huffmancode;

/*
* 創建 Node,帶數據和權值
* */
public class Node implements Comparable<Node> {

    private Byte data; // 存放數據(字符)本身,比如 'a'=>97 ' '=> 32
    private int weight; // 權值,表示字符出現的次數
    private Node left;
    private Node right;

    public Node(Byte data, int weight) {
        this.data = data;
        this.weight = weight;
    }

    // 前序遍歷
    public void preOrder(){
        System.out.println(this);
        if (this.left != null){
            this.left.preOrder();
        }
        if (this.right != null){
            this.right.preOrder();
        }
    }

    @Override
    public String toString() {
        return "Node{" +
                "data=" + data +
                ", weight=" + weight +
                '}';
    }

    public Byte getData() {
        return data;
    }

    public void setData(Byte data) {
        this.data = data;
    }

    public int getWeight() {
        return weight;
    }

    public void setWeight(int weight) {
        this.weight = weight;
    }

    public Node getLeft() {
        return left;
    }

    public void setLeft(Node left) {
        this.left = left;
    }

    public Node getRight() {
        return right;
    }

    public void setRight(Node right) {
        this.right = right;
    }

    @Override
    public int compareTo(Node o) {
        // 從小到大排序
        return this.weight - o.weight;
    }
}

3.3.2 HuffmanCode 類 赫夫曼編碼類

package com.feng.ch14_huffmancode;

import java.io.*;
import java.util.*;

public class HuffmanCode {

    public static void main(String[] args) {
        String content = "i like like like java do you like a java";

        //######################################################  哈夫曼數據壓縮 測試  ##########################################

        /*
         * 一、字符串 轉成 字節數組
         * 字節數組 儲存的是 ASCII 表對應的 數字
         * */
        byte[] contentBytes = content.getBytes();
        System.out.println("字符串轉成的字節數組:" + Arrays.toString(contentBytes));// [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]
        System.out.println("字符串轉成的字節數組大小:" + contentBytes.length); // 40

        /*
         * 將下面的 4 步,封裝成一個方法 huffmanZip()
         *
         * */
        byte[] huffmanCodesBytes = huffmanZip(contentBytes);
        System.out.println("壓縮後的結果大小是:" + huffmanCodesBytes.length); // 40
        System.out.println("壓縮後的結果是:" + Arrays.toString(huffmanCodesBytes)); //  [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]

        /*
         * 二、字節數組 轉成 list集合
         * 1、對字節數組進行遍歷,統計每一個 byte 出現的次數,封裝在 Map 集合
         * 2、然後對map 集合進行遍歷,對每個 key-value 生成一個 Node 結點,以 Node 結點形式 封裝在 List 集合 中
         * */
//        List<Node> nodes = getNodes(contentBytes);
//        System.out.println("nodes=" + nodes); // nodes=[Node{data=32, weight=9}, Node{data=97, weight=5}, Node{data=100, weight=1}, Node{data=101, weight=4}, Node{data=117, weight=1}, Node{data=118, weight=2}, Node{data=105, weight=5}, Node{data=121, weight=1}, Node{data=106, weight=2}, Node{data=107, weight=4}, Node{data=108, weight=4}, Node{data=111, weight=2}]

        /*
         * 三、list集合 轉成 哈弗曼樹,
         * 1、使用 Collections 集合 對 List 集合進行從小到大排序,
         * 2、取出根節點權值最小的兩顆二叉樹
         * 3、組成一顆新的二叉樹, 該新的二叉樹的根節點的權值是前面兩顆二叉樹根節點權值的和
         * 4、再將這顆新的二叉樹,以根節點的權值大小 再次排序, 不斷重複  1-2-3-4 的步驟,直到數列中,所有的數據都被處理,就得到一顆赫夫曼樹
         * 最後,循環完後,list集合中,只有一個數據,也就是哈弗曼樹的根節點。
         *
         * 前序遍歷中, data 爲空的爲 父結點, 不爲空的爲葉子結點。
         * */
//        System.out.println("哈弗曼樹");
//        Node huffmanTreeRoot = createHuffmanTree(nodes);
//        System.out.println("測試一把 ,創建的二叉樹,前序遍歷哈弗曼樹 前序遍歷:");
//        preOrder(huffmanTreeRoot);
//        System.out.println();

        /*
         * 四、哈夫曼樹 轉成 哈夫曼編碼
         * 哈夫曼編碼使用Map<Byte, String> 來儲存
         * 哈夫曼編碼:前面的每個元素對應的 ASCII 和 次數 構成一個 葉子結點,形成的哈弗曼樹,左邊爲0,右邊爲1。元素=路徑,葉子節點的路徑稱爲 哈夫曼編碼
         * */
//        Map<Byte, String> huffmanCodes = getCodes(root);// 對 getCodes(root, "", stringBuilder); 進行了重載
//        System.out.println("生成的 哈夫曼編碼表 huffmanCodes:" + huffmanCodes); //{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
//        System.out.println("~生成的 哈夫曼編碼表 codes:" + codes); //{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
//        System.out.println();

        /*
         * 五、對哈夫曼編碼進行壓縮,得到壓縮後的 哈夫曼編碼字節數組
         * 傳入的 字符串 轉成 的字節數組 和 哈夫曼編碼表
         * 測試
         * */
//        byte[] huffmanCodeBytes = zip(contentBytes, huffmanCodes);
//        System.out.println("huffmanCodeBytes=" + Arrays.toString(huffmanCodeBytes)); // 17個   huffmanCodeBytes=[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]

        //######################################################  哈夫曼數據解壓  ##########################################
        System.out.println();
        System.out.println();
        System.out.println("哈夫曼數據解壓:");
//        byteToBitString((byte) 1);

        byte[] sourceBytes = decode(huffmanCodes, huffmanCodesBytes);
        System.out.println("原來的字符串大小=" + new String(sourceBytes).length()); //  i like like like java do you like a java
        System.out.println("原來的字符串=" + new String(sourceBytes)); //  i like like like java do you like a java


        //######################################################  哈夫曼編碼應用實例:壓縮和解壓文件  ##########################################
        System.out.println();
        System.out.println();
        System.out.println("哈夫曼編碼應用實例:壓縮和解壓文件");

//        String srcFile = "D:\\HuffmanCode.java";
//        String dstFile = "D:\\HuffmanCode.zip";
//
//        zipFile(srcFile, dstFile);
//        System.out.println("壓縮文件成功~~");

        String zipFile = "D:\\HuffmanCode.zip";
        String dstFile2 = "D:\\HuffmanCode2.java";

        unZipFile(zipFile, dstFile2);
        System.out.println("解壓文件成功~~~");

    }

    //######################################################  哈夫曼數據壓縮  ##########################################

    /**
     * 使用一個方法,將前面的方法封裝起來,便於我們的調用
     *
     * @param contentBytes 原始字符串對應的字節數組
     * @return 是經過哈夫曼編碼處理後的字節數組(壓縮後的數組)
     */
    private static byte[] huffmanZip(byte[] contentBytes) {
        //二、字節數組 轉成 list集合
        List<Node> nodes = getNodes(contentBytes);
        //三、list集合 轉成 哈弗曼樹,
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        //四、哈夫曼樹 轉成 哈夫曼編碼
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        //五、對哈夫曼編碼進行壓縮,得到壓縮後的 哈夫曼編碼字節數組
        byte[] huffmanCodeBytes = zip(contentBytes, huffmanCodes);

        return huffmanCodeBytes;
    }

    /*
     * 二、字節數組 轉成 list集合
     * @param bytes 接收一個 字節數組
     * @return 返回的就是一個 List 集合,如形式 [Node[date=97 ,weight = 5], Node[]date=32,weight = 9]......],
     * */
    private static List<Node> getNodes(byte[] bytes) {

        // 1、先創建一個 ArrayList
        ArrayList<Node> nodes = new ArrayList<>();

        // 遍歷 bytes 字節數組,統計每一個 byte 出現的次數 -》 map[key, value]
        Map<Byte, Integer> map = new HashMap(); // Byte 爲數據,Integer 爲次數
        for (byte b : bytes) {
            Integer count = map.get(b);
            if (count == null) { // Map 還沒有這個字符數據,第一次
                map.put(b, 1);
            } else {
                map.put(b, count + 1);
            }
        }

        // 把每一個鍵值對 轉成 一個 Node 對象,並加入到 nodes 集合中
        // 遍歷map
        for (Map.Entry<Byte, Integer> entry : map.entrySet()) {
            nodes.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodes;
    }

    /*
     * 三、list集合 轉成 哈弗曼樹,
     * 通過一個 list 創建對應的哈弗曼樹
     * */
    public 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.getWeight() + rightNode.getWeight());

            parent.setLeft(leftNode);
            parent.setRight(rightNode);

            // 將已經處理的兩顆二叉樹從nodes 刪除
            nodes.remove(leftNode);
            nodes.remove(rightNode);

            // 將新的二叉樹 添加到 nodes
            nodes.add(parent);
        }
        // 循環後,此集合 也就只有一個節點了,返回的爲 nodes 第一個節點,此結點爲根節點
        return nodes.get(0);
    }

    // 前序遍歷
    public static void preOrder(Node root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("哈弗曼樹爲空~~");
        }
    }

    /*
     * huffmanCodes : 存放所有葉子節點的哈弗曼編碼
     * stringBuilder :拼接路徑
     * */
    static Map<Byte, String> huffmanCodes = new HashMap<>();
    static StringBuilder stringBuilder = new StringBuilder();


    /*
    * 四、哈夫曼樹 轉成 哈夫曼編碼
    * 重載 getCodes
    * */
    public static Map<Byte, String> getCodes(Node root) {
        if (root == null) {
            return null;
        }
        // 處理root 的左子樹
        getCodes(root.getLeft(), "0", stringBuilder);
        // 處理 root 的右子樹
        getCodes(root.getRight(), "1", stringBuilder);
        return huffmanCodes;
    }

    /*
     * 四、哈夫曼樹 轉成 哈夫曼編碼
     * 功能:將傳入的 node結點 的所有葉子結點的哈夫曼編碼,並放入到  huffmanCodes 集合
     *
     * 生成 哈弗曼樹 對應 的哈夫曼編碼
     * 思路:
     * 1、將哈夫曼編碼表 存放在 Map<Byte,String> 形式爲:32=>01 97=>100 100=>11000 等等
     * 2、在生成 哈夫曼編碼表 時,需要去拼接這個路徑,所以定義一個StringBuilder 存儲 某個葉子結點的路徑
     *
     * @param node 傳入結點
     * @param code 路徑:左子結點是 0, 右子結點是 1
     * @param stringBuilder 是用於拼接路徑
     * */
    public static void getCodes(Node node, String code, StringBuilder stringBuilder) {
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);

        // 將 code 加入到 stringBuilder2
        stringBuilder2.append(code);

        if (node != null) {
            // 判斷當前 node,是葉子結點 還是非葉子結點
            if (node.getData() == null) { // 非葉子結點
                // 遞歸處理
                // 向左遞歸
                getCodes(node.getLeft(), "0", stringBuilder2);
                // 向右遞歸
                getCodes(node.getRight(), "1", stringBuilder2);
            } else { // 葉子結點
                huffmanCodes.put(node.getData(), stringBuilder2.toString());
            }
        }
    }

    /*
     * 五、對哈夫曼編碼進行壓縮,得到壓縮後的 哈夫曼編碼字節數組
     * 編寫一個方法,將字符串對應的byte[] 數組,通過生成的 哈夫曼編碼表,返回一個哈夫曼編寫 壓縮後 的 byte[]
     *
     * @param bytes 這時原始的字符串對應的 byte[]
     * @param huffmanCodes 生成的哈夫曼編碼 map
     * @return 返回的哈夫曼編碼處理後的 byte[]
     * 舉例 String content = "i like like like java do you like a java"; => byte[] contentBytes = content.getBytes()
     * 返回的字符串 "1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
     * => 對應的 byte[] huffmanCodeBytes , 即 8 位對應一個 byte,放入到 huffmanCodeBytes
     * huffmanCodeBytes[0] = 10101000(補碼) => byte [推導 10101000 => 10101000-1 => 10100111(反碼)=> 11011000= -88 ]
     * huffmanCodeBytes[0] = -88
     * */
    private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
        // 1、利用 HuffmanCodes 將 bytes 轉成 哈夫曼編碼對應的字符串
        StringBuilder stringBuilder = new StringBuilder();
        // 遍歷 bytes 數組
        for (byte b : bytes) {
            stringBuilder.append(huffmanCodes.get(b));  // 以 bytes 數組 的每個元素爲 key ,取出 value值,value 值爲其 路徑,也就是哈夫曼編碼map集合中的 value值
        }
        System.out.println("測試 stringBuilder=" + stringBuilder.toString()); //測試 stringBuilder= 11011011000110111111111101011000110111111111101011000110111111111101011001011001110111100101111000100111011110101001111100110110001101111111111010111001011001011001110111100

        // 將 “110110110001101111111111010110。。。” 轉成 byte[]
        // 統計返回 byte[] huffmanCodeBytes 長度
        int len;
        if (stringBuilder.length() % 8 == 0) {
            len = stringBuilder.length() / 8;
        } else {
            len = stringBuilder.length() / 8 + 1;
        }

        // 創建存儲壓縮後的 byte 數組
        byte[] huffmanCodeBytes = new byte[len];
        int index = 0; // 記錄第幾個byte
        for (int i = 0; i < stringBuilder.length(); i += 8) {
            String strByte;
            if (i + 8 > stringBuilder.length()) { // 不夠 8位
                strByte = stringBuilder.substring(i);  // 從i位 到最後
            } else {
                strByte = stringBuilder.substring(i, i + 8);
            }
            // 將 strByte 裝成一個byte ,放入到 huffmanCodeBytes
            huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
            index++;
        }
        return huffmanCodeBytes;
    }


    //######################################################  哈夫曼數據解壓  ##########################################

    //
    //
    // 1、將huffmanCodeBytes[]
    //
    /*
     * 完成數據的解壓
     * 思路
     * 1. 將huffmanCodeBytes [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
     *    重寫先轉成 赫夫曼編碼對應的二進制的字符串 "1010100010111..."
     * 2. 赫夫曼編碼對應的二進制的字符串 "1010100010111..." =》 對照 赫夫曼編碼  =》 "i like like like java do you like a java"
     * */


    /*
     *
     * 功能:將一個byte 轉成一個二進制的字符串
     * @param flag 標誌是否需要補高位如果是true ,表示需要補高位,如果是false表示不補, 如果是最後一個字節,無需補高位
     * @param b 傳入的 byte,需要將其轉化爲 二進制字符串,
     * @return
     *
     * 如果僅使用 String str = Integer.toBinaryString(temp); 進行返回,
     * 測試:輸入 (byte)-1 ,輸出 str=11111111111111111111111111111111
     *       輸入 (byte)1  , 輸出  java.lang.StringIndexOutOfBoundsException: String index out of range: -7 報錯。
     * 說明 要對String str = Integer.toBinaryString(temp); 返回的數據進行處理,
     * 1、先進行截取後8位。
     * 2、對於正數 還要補高位。 |= 按位與 256   10000 0000 | 0000 0001 =》 10000 0001
     * */
    private static String byteToBitString(boolean flag, byte b) {
        // 使用變量 保存 b
        int temp = b;
        // 如果是正數我們還存在補高位
        if (flag) {
            temp |= 256; // |= 按位與 256   10000 0000 | 0000 0001 =》 10000 0001
        }
        String str = Integer.toBinaryString(temp);
        if (flag) {
//            System.out.println("str=" + str); // -1 str=11111111111111111111111111111111 , 1 java.lang.StringIndexOutOfBoundsException: String index out of range: -7 報錯。
            return str.substring(str.length() - 8);
        } else {
            return str;
        }
    }

    /*
     * 編寫 一個方法,完成對壓縮數據的解碼
     * */
    /**
     * @param huffmanCodes 哈夫曼編碼表 map
     * @param huffmanBytes 哈夫曼編碼得到的字節數組
     * @return 就是原來的字符串對應的數組
     */
    private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {

        // 1、先得到 HuffmanBytes 對應的二進制的字符串,形式  1010100010111...
        StringBuilder stringBuilder = new StringBuilder();
        // 將 byte 數組轉成 二進制的字符串
        for (int i = 0; i < huffmanBytes.length; i++) {
            // 判斷是不是最後一個字節
            boolean flag = i == huffmanBytes.length - 1;
            stringBuilder.append(byteToBitString(!flag, huffmanBytes[i]));
        }
        System.out.println("哈夫曼字節數組 對應的 二進制字符串=" + stringBuilder.toString()); // 1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100

        // 2、把字符串按照指定的哈夫曼編碼進行解碼
        // 把哈夫曼編碼進行調換,因爲反向查詢 a->100 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); // map={000=108, 01=32, 100=97, 101=105, 11010=121, 0011=111, 1111=107, 11001=117, 1110=101, 11000=100, 11011=118, 0010=106}

        // 創建一個集合, 存放 byte
        List<Byte> list = new ArrayList<>();
        // i 可以理解爲 索引,掃描 stringBuilder  10101000101111111100100010111111110010001....
        for (int i = 0; i < stringBuilder.length(); ) { // 這裏不再進行調整 i, 因爲下面有 i += count ,已經調整了
            int count = 1;
            boolean flag = true;
            Byte b = null;
            while (flag) {
                // 10101000101111111100100010111111110010001....
                // 遞增的取出 key 1
                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 中 就存放了所有的字符 “i like like like java do you like a java”
        byte b[] = new byte[list.size()];
        for (int i = 0; i < b.length; i++) {
            b[i] = list.get(i);
        }
        return b;
    }


    /*
    * 編寫方法,將一個文件進行壓縮
    * */

    /**
     *
     * @param srcFile 傳入的壓縮的文件全路徑
     * @param dstFile 壓縮後將壓縮文件存放目錄
     */
    public static void zipFile(String srcFile, String dstFile){

        FileInputStream fis = null;
        FileOutputStream fos = null;
        ObjectOutputStream oos = null;
        try {
            // 創建文件的輸入流、輸出流
            fis = new FileInputStream(srcFile);
            // 創建一個和源文件大小一樣的 byte 數組
            byte[] b = new byte[fis.available()];
            //讀取文件
            fis.read(b);

            // 直接 對源文件 進行壓縮
            byte[] huffmanBytes = huffmanZip(b);
            // 創建文件的輸出流, 存放壓縮文件
            fos = new FileOutputStream(dstFile);
            // 創建一個和文件輸出流關聯的 ObjectOutputStream
            oos = new ObjectOutputStream(fos);
            // 以對象流的方式寫入 哈夫曼編碼,是爲了以後 我們 恢復源文件時 使用
            oos.writeObject(huffmanBytes);
            /*
            * 這裏我們以對象流的方式寫入 赫夫曼編碼,是爲了以後我們恢復源文件時使用
            * 注意一定要把赫夫曼編碼 寫入壓縮文件
            * */
            oos.writeObject(huffmanCodes);

        }catch (Exception e){
            System.out.println(e.getMessage());
        } finally {
            try {
                fis.close();
                fos.close();
                oos.close();
            } catch (IOException e) {
                System.out.println(e.getLocalizedMessage());
            }
        }
    }

    //編寫一個方法,完成對壓縮文件的解壓
    /**
     *
     * @param zipFile 準備解壓的文件
     * @param dstFile 將文件解壓到哪個路徑
     */
    public static void unZipFile(String zipFile, String dstFile) {
        //定義 文件輸入流
        InputStream is = null;
        //定義一個 對象輸入流
        ObjectInputStream ois = null;
        //定義文件的輸出流
        OutputStream os = null;
        try {
            //創建文件輸入流
            is = new FileInputStream(zipFile);
            //創建一個和  is關聯的對象輸入流
            ois = new ObjectInputStream(is);

            //讀取 哈夫曼字節數組  huffmanBytes
            byte[] huffmanBytes = (byte[])ois.readObject();

            //讀取 赫夫曼編碼表
            Map<Byte,String> huffmanCodes = (Map<Byte,String>)ois.readObject();

            //解碼
            byte[] bytes = decode(huffmanCodes, huffmanBytes);

            //將bytes 數組寫入到目標文件
            os = new FileOutputStream(dstFile);
            //寫數據到 dstFile 文件
            os.write(bytes);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        } finally {
            try {
                os.close();
                ois.close();
                is.close();
            } catch (Exception e2) {
                System.out.println(e2.getMessage());
            }
        }
    }

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章