[數據結構與算法]赫夫曼樹與赫夫曼編碼

赫夫曼樹

  • 給定n個權值作爲n個葉子結點,構造一棵二叉樹,若該 樹的帶權路徑長度(wpl)達到最小 ,稱這樣的二叉樹爲最優二叉樹,也稱爲哈夫曼樹(Huffman Tree), 或赫/霍夫曼樹

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

赫夫曼樹幾個重要概念和舉例說明

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

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

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

  • WPL最小的就是赫夫曼樹(如下圖可以看到,中間就是赫夫曼樹)

在這裏插入圖片描述

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

思路分析(示意圖):
在這裏插入圖片描述

代碼實現
在這裏插入圖片描述

/**
 * 赫夫曼樹
 *
 * @author TimePause
 * @create 2020-02-16 22:15
 */
public class HuffmanTreeDemo {
    public static void main(String[] args) {
        int[] arr = {13, 7, 8, 3, 29, 6, 1};
        Node root = createHuffmanTree(arr);
        preOrder(root);
    }

    /**
     * 主類中編寫前序遍歷的調用方法
     * @param root 根節點
     */
    public static void preOrder(Node root){
        if (root!=null){
            root.preOrder();
        }else {
            System.out.println("是空樹,無法進行前序遍歷");
        }
    }

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

        while (nodes.size()>1){
            //排序.從小到大
            Collections.sort(nodes);
            System.out.println("nodes = " + nodes);

            //取出節點權值最小的兩個二叉樹
            //(1)取出權值最小的二叉樹
            //(2)取出權值第二小的二叉樹
            Node leftNode = nodes.get(0);
            Node rightNode = nodes.get(1);
            //(3)構建一顆新的二叉樹
            Node parent = new Node(leftNode.value + rightNode.value);
            parent.left = leftNode;
            parent.right = rightNode;

            //(4)從ArrayList中刪除處理過的二叉樹
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //(5)將parent加入到nodes
            nodes.add(parent);
        }
        //返回赫夫曼樹的root節點, 目的是方便我們進行前序遍歷
        return nodes.get(0);

    }
}

/**
 * 創建節點類
 * 爲了讓Node對象支持Collections結合排序, 需要讓讓Node實現Compare接口
 */
class Node implements Comparable<Node>{
    // 省略訪問修飾符friendly ,缺省的,在同一個包中的類可以訪問,其他包中的類不能訪問;
     int value;//節點的權值
     Node left;//指向左節點
     Node right;//指向右節點

    public void preOrder(){
        //父->左->右
        System.out.println(this);
        if (this.left!=null){
            this.left.preOrder();
        }
        if (this.right!=null){
            this.right.preOrder();//遍歷有問題,結果可能出錯,例如這裏調用this.left.preOrder(),結果只出現67,29,29
        }
    }

    /**
     * 重載構造方法,實例化時只需傳入權值即可
     * @param value
     */
    public Node(int value) {
        this.value = value;
    }

    //*** 如果toString()書寫有誤,name在打印時可能爲空
    @Override
    public String toString() {
        return "Node [value=" + value + "]";
    }

    @Override
    public int compareTo(Node o) {
        return this.value-o.value;//這樣寫表示正序排序
    }
}

赫夫曼編碼

赫夫曼編碼也翻譯爲 哈夫曼編碼(Huffman Coding),又稱霍夫曼編碼,是一種編碼方式, 屬於一種程序算法
赫夫曼編碼是赫哈夫曼樹在電訊通信中的經典的應用之一。

  • 赫夫曼編碼廣泛地用於數據文件壓縮。其壓縮率通常在20%~90%之間
  • 赫夫曼碼是可變字長編碼(VLC)的一種。Huffman於1952年提出一種編碼方法,稱之爲最佳編碼

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

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  // 各個字符對應的個數

按照上面字符出現的次數構建一顆赫夫曼樹, 次數作爲權值.(圖後)

在這裏插入圖片描述

實現思路圖

在這裏插入圖片描述

實現代碼

/**
 * 赫夫曼編碼
 *
 * @author TimePause
 * @create 2020-02-18 15:25
 */
public class HuffmanCodeDemo {
    public static void main(String[] args) {
        String strs = "i like like like java do you like a java";
        byte[] bytes = strs.getBytes();
        System.out.println(bytes.length);

        //按照赫夫曼編碼存放數據
        List<Node> nodes = getNodes(bytes);
        System.out.println("nodes = " + nodes);

        //測試一把創建的二叉樹
        Node huffmanTreeNode = createHuffmanTree(nodes);
        System.out.println("執行前序遍歷");
        huffmanTreeNode.preOrder();
    }


    /**
     * 接收字節數組
     * @param bytes 字節數組(單個字節形參的數組a,b,c...)
     * @return 返回的值就是List形式 [Node[date='7',weight=5],Node[date='32',weight=9] ......]
     */
    public static List<Node> getNodes(byte[] bytes){
        // 創建一個arrayList數組
        ArrayList<Node> nodeArrayList = new ArrayList<>();
        // 遍歷bytes,統計每一個byte出現的次數->map[key,value]=>mao(字符,以及字符出現的個數)
        HashMap<Byte, Integer> counts = new HashMap<>();
        for (byte b:bytes){
            //在我們的map集合中查看數據是否存在
            Integer count = counts.get(b);
            if (count == null) {
                //如果不存在則添加
                counts.put(b, 1);
            }else {
                //如果存在則將原來數值加1
                counts.put(b, count + 1);
            }
        }

        // 將每一個鍵值轉成一個node對象 ,並加入到nodes集合
        //遍歷>map[key,value]=>mao(字符,以及字符出現的個數
        for (Map.Entry<Byte,Integer> entry: counts.entrySet()){
            nodeArrayList.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodeArrayList;
    }

    /**
     * 前序遍歷
     * @param root 赫夫曼樹的根節點
     */
    private void preOder(Node root){
        if (root!=null){
            root.preOrder();
        }else {
            System.out.println("爲空,無法執行前序遍歷");
        }
    }

    /**
     * 根據lIst集合創建赫夫曼樹
     * @param nodes Node類型的List數組
     * @return Node節點
     */
    private 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.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;
            // 在集合中刪除使用過的節點
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            // 將這個新的二叉樹, 加入到nodes
            nodes.add(parent);

        }
        // 返回赫夫曼樹的根節點
        return nodes.get(0);
    }
}




/**
 * 節點類
 */
class Node implements Comparable<Node>{
    Byte data;//存放的數據本身,比如'a'=>97 ' '=>32
    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 this.weight-o.weight;//正序排序
    }

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

    // 前序遍歷方法
    public void preOrder(){
        //輸出當父節點
        System.out.println(this);
        //左子樹遞歸調用前序遍歷
        if (this.left!=null){
            this.left.preOrder();
        }
        if (this.right!=null){
            //右子樹遞歸調用前序遍歷
            this.right.preOrder();
        }
    }
}

演示結果

在這裏插入圖片描述

數據壓縮和解壓

生成赫夫曼編碼

//生成赫夫曼樹對應的赫夫曼編碼
    //思路:
    //1. 將赫夫曼編碼表存放在 Map<Byte,String> 形式
    //   生成的赫夫曼編碼表{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
    static Map<Byte, String> huffmanCodes = new HashMap<Byte,String>();
    //2. 在生成赫夫曼編碼表示,需要去拼接路徑, 定義一個StringBuilder 存儲某個葉子結點的路徑
    static StringBuilder stringBuilder = new StringBuilder();


    //爲了調用方便,我們重載 getCodes
    private static Map<Byte, String> getCodes(Node root) {
        if(root == null) {
            return null;
        }
        //處理root的左子樹
        getCodes(root.left, "0", stringBuilder);
        //處理root的右子樹
        getCodes(root.right, "1", stringBuilder);
        return huffmanCodes;
    }

    /**
     * 功能:將傳入的node結點的所有葉子結點的赫夫曼編碼得到,並放入到huffmanCodes集合
     * @param node  傳入結點
     * @param code  路徑: 左子結點是 0, 右子結點 1
     * @param stringBuilder 用於拼接路徑
     */
    private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //將code 加入到 stringBuilder2
        stringBuilder2.append(code);
        if(node != null) { //如果node == null不處理
            //判斷當前node 是葉子結點還是非葉子結點
            if(node.data == null) { //非葉子結點
                //遞歸處理
                //向左遞歸
                getCodes(node.left, "0", stringBuilder2);
                //向右遞歸
                getCodes(node.right, "1", stringBuilder2);
            } else { //說明是一個葉子結點
                //就表示找到某個葉子結點的最後
                huffmanCodes.put(node.data, stringBuilder2.toString());
            }
        }
    }


    /**
     * 接收字節數組
     * @param bytes 字節數組(單個字節形參的數組a,b,c...)
     * @return 返回的值就是List形式 [Node[date='7',weight=5],Node[date='32',weight=9] ......]
     */
    public static List<Node> getNodes(byte[] bytes){
        // 創建一個arrayList數組
        ArrayList<Node> nodeArrayList = new ArrayList<>();
        // 遍歷bytes,統計每一個byte出現的次數->map[key,value]=>mao(字符,以及字符出現的個數)
        HashMap<Byte, Integer> counts = new HashMap<>();
        for (byte b:bytes){
            //在我們的map集合中查看數據是否存在
            Integer count = counts.get(b);
            if (count == null) {
                //如果不存在則添加
                counts.put(b, 1);
            }else {
                //如果存在則將原來數值加1
                counts.put(b, count + 1);
            }
        }

        // 將每一個鍵值轉成一個node對象 ,並加入到nodes集合
        //遍歷>map[key,value]=>mao(字符,以及字符出現的個數
        for (Map.Entry<Byte,Integer> entry: counts.entrySet()){
            nodeArrayList.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodeArrayList;
    }

生成赫夫曼字節數組

 /**
     * 生成赫夫曼字節數組
     * 編寫一個方法,將字符串對應的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[1] = -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));
        }

        //System.out.println("測試 stringBuilder~~~=" + stringBuilder.toString());

        //將 "1010100010111111110..." 轉成 byte[]

        //統計返回  byte[] huffmanCodeBytes 長度
        //一句話 int len = (stringBuilder.length() + 7) / 8;
        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) { //因爲是每8位對應一個byte,所以步長 +8
            String strByte;
            if(i+8 > stringBuilder.length()) {//不夠8位
                strByte = stringBuilder.substring(i);
            }else{
                strByte = stringBuilder.substring(i, i + 8);
            }
            //將strByte 轉成一個byte,放入到 huffmanCodeBytes
            huffmanCodeBytes[index] = (byte)Integer.parseInt(strByte, 2);
            index++;
        }
        return huffmanCodeBytes;
    }

測試生成的赫夫曼編碼表與字節數組
在這裏插入圖片描述

封裝赫夫曼數組

     *使用一個方法,將前面的方法封裝起來,便於我們的調用.
     *
     * @param bytes 原始的字符串對應的字節數組
     * @return 是經過 赫夫曼編碼處理後的字節數組(壓縮後的數組)
     */
    private static byte[] huffmanZip(byte[] bytes) {
        List<Node> nodes = getNodes(bytes);
        //根據 nodes 創建的赫夫曼樹
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        //對應的赫夫曼編碼(根據 赫夫曼樹)
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        //根據生成的赫夫曼編碼,壓縮得到壓縮後的赫夫曼編碼字節數組
        byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
        return huffmanCodeBytes;
    }

字節轉二進制字符串

 /**
     * 將一個byte 轉成一個二進制的字符串, 如果看不懂,可以參考我講的Java基礎 二進制的原碼,反碼,補碼
     * @param b 傳入的 byte
     * @param flag 標誌是否需要補高位如果是true ,表示需要補高位,如果是false表示不補, 如果是最後一個字節,無需補高位
     * @return 是該b 對應的二進制的字符串,(注意是按補碼返回)
     */
    private static String byteToBitString(boolean flag, byte b) {
        //使用變量保存 b
        int temp = b; //將 b 轉成 int
        //如果是正數我們還存在補高位
        if(flag) {
            temp |= 256; //按位與 256  1 0000 0000  | 0000 0001 => 1 0000 0001
        }
        String str = Integer.toBinaryString(temp); //返回的是temp對應的二進制的補碼
        if(flag) {
            return str.substring(str.length() - 8);
        } else {
            return str;
        }
    }

壓縮數據的解碼

  //完成數據的解碼
    //思路
    //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"


    /**
     *編寫一個方法,完成對壓縮數據的解碼
     *
     * @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++) {
            byte b = huffmanBytes[i];
            //判斷是不是最後一個字節
            boolean flag = (i == huffmanBytes.length - 1);
            stringBuilder.append(byteToBitString(!flag, b));
        }
        //把字符串安裝指定的赫夫曼編碼進行解碼
        //把赫夫曼編碼表進行調換,因爲反向查詢 a->100 100->a
        Map<String, Byte>  map = new HashMap<String,Byte>();
        for(Map.Entry<Byte, String> entry: huffmanCodes.entrySet()) {
            map.put(entry.getValue(), entry.getKey());
        }

壓縮文件

/**
     * 將文件進行壓縮的方法
     * @param srcFile 希望被壓縮的文件的路徑
     * @param targetFile 壓縮後的文件的路徑
     */
    public static void zipFile(String srcFile,String targetFile){
        //創建輸出流
        OutputStream os = null;
        // 用對象流包裝
        ObjectOutputStream oos = null;
        // 創建文件輸入流
        FileInputStream fis = null;

        try {
            // 創建輸出流
            fis = new FileInputStream(srcFile);
            // 創建一個和源文件一樣大小的byte[]
            byte[] b = new byte[fis.available()];//available()可以返回有多少流可以讀取
            // 讀取文件
            fis.read(b);
            // 將文件進行壓縮
            byte[] huffmanBytes = huffmanZip(b);
            // 寫壓縮後的文件寫入目標文件
            os = new FileOutputStream(targetFile);
            oos = new ObjectOutputStream(os);
            // 把赫夫曼編碼後的字節數組寫入壓縮文件
            oos.writeObject(huffmanBytes);
            // 把赫夫曼編碼寫到壓縮文件, 目的是以後我們恢復文件時使用
            oos.writeObject(huffmanCodes);

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if (oos!=null){
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (os!=null){
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (fis!=null){
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
    /**
     *使用一個方法,將前面的方法封裝起來,便於我們的調用.
     *
     * @param bytes 原始的字符串對應的字節數組
     * @return 是經過 赫夫曼編碼處理後的字節數組(壓縮後的數組)
     */
    private static byte[] huffmanZip(byte[] bytes) {
        List<Node> nodes = getNodes(bytes);
        //根據 nodes 創建的赫夫曼樹
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        //對應的赫夫曼編碼(根據 赫夫曼樹)
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        //根據生成的赫夫曼編碼,壓縮得到壓縮後的赫夫曼編碼字節數組
        byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
        return huffmanCodeBytes;
    }

測試壓縮結果

 //測試壓縮文件
        String srcFlie = "d://a.png";
        String tagetFIle = "d://a.zip";
        zipFile(srcFlie,tagetFIle);
        System.out.println("壓縮文件完成");

在這裏插入圖片描述

解壓文件

/**
     * 完成對壓縮文件的解壓
     * @param zipFile 準備解壓的文件
     * @param targetFile 將文件解壓到哪個路徑
     */
    public static void unZipFile(String zipFile,String targetFile){
        FileInputStream fis = null;
        ObjectInputStream ois = null;
        OutputStream os = null;
        try {
            //創建文件出入流,讀取文件
            fis = new FileInputStream(zipFile);
            //關聯fis,以對象的形式讀取文件
            ois = new ObjectInputStream(fis);
            //讀取數組 huffmanBytes
            byte[] huffmanBytes = (byte[]) ois.readObject();
            //讀取赫夫曼編碼表
            Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();

            //解碼
            byte[] bytes = decode(huffmanCodes, huffmanBytes);
            //將bytes數組寫入到目標文件
            os = new FileOutputStream(targetFile);
            //寫到目標文件
            os.write(bytes);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if (os!=null){
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (ois!=null){
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fis!=null){
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

測試結果

   //測試解壓文件
        String zipFile = "d://a.zip";
        String targetFile = "d://a1.png";
        unZipFile(zipFile,targetFile);
        System.out.println("解壓文件完成");

在這裏插入圖片描述

赫夫曼編碼壓縮文件注意事項

  • 如果文件本身就是經過壓縮處理的,那麼使用赫夫曼編碼再壓縮效率不會有明顯變化, 比如視頻,ppt 等等文件
  • 赫夫曼編碼是按字節來處理的,因此可以處理所有的文件(二進制文件、文本文件) [舉例壓一個.xml文件]
  • 如果一個文件中的內容,重複的數據不多,壓縮效果也不會很明顯.

全部代碼整理

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HuffmanCode {

	public static void main(String[] args) {
		
		//測試壓縮文件
//		String srcFile ="d://a.png";
//		String dstFile = "d://a.zip";
//		
//		zipFile(srcFile, dstFile);
//		System.out.println("壓縮文件ok~~");
		
		
		//測試解壓文件
		String zipFile = "d://a.zip";
		String dstFile = "d://a1.png";
		unZipFile(zipFile, dstFile);
		System.out.println("解壓成功!");
		
		/*
		String content = "i like like like java do you like a java";
		byte[] contentBytes = content.getBytes();
		System.out.println(contentBytes.length); //40
		
		byte[] huffmanCodesBytes= huffmanZip(contentBytes);
		System.out.println("壓縮後的結果是:" + Arrays.toString(huffmanCodesBytes) + " 長度= " + huffmanCodesBytes.length);
		
		
		//測試一把byteToBitString方法
		//System.out.println(byteToBitString((byte)1));
		byte[] sourceBytes = decode(huffmanCodes, huffmanCodesBytes);
		
		System.out.println("原來的字符串=" + new String(sourceBytes)); // "i like like like java do you like a java"
		*/
		
		
		
		//如何將 數據進行解壓(解碼)  
		//分步過程
		/*
		List<Node> nodes = getNodes(contentBytes);
		System.out.println("nodes=" + nodes);
		
		//測試一把,創建的赫夫曼樹
		System.out.println("赫夫曼樹");
		Node huffmanTreeRoot = createHuffmanTree(nodes);
		System.out.println("前序遍歷");
		huffmanTreeRoot.preOrder();
		
		//測試一把是否生成了對應的赫夫曼編碼
		Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
		System.out.println("~生成的赫夫曼編碼表= " + huffmanCodes);
		
		//測試
		byte[] huffmanCodeBytes = zip(contentBytes, huffmanCodes);
		System.out.println("huffmanCodeBytes=" + Arrays.toString(huffmanCodeBytes));//17
		
		//發送huffmanCodeBytes 數組 */
		
		
	}
	
	//編寫一個方法,完成對壓縮文件的解壓
	/**
	 * 
	 * @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);
			//讀取byte數組  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) {
			// TODO: handle exception
			System.out.println(e.getMessage());
		} finally {
			
			try {
				os.close();
				ois.close();
				is.close();
			} catch (Exception e2) {
				// TODO: handle exception
				System.out.println(e2.getMessage());
			}
			
		}
	}
	
	//編寫方法,將一個文件進行壓縮
	/**
	 * 
	 * @param srcFile 你傳入的希望壓縮的文件的全路徑
	 * @param dstFile 我們壓縮後將壓縮文件放到哪個目錄
	 */
	public static void zipFile(String srcFile, String dstFile) {
		
		//創建輸出流
		OutputStream os = null;
		ObjectOutputStream oos = null;
		//創建文件的輸入流
		FileInputStream is = null;
		try {
			//創建文件的輸入流
			is = new FileInputStream(srcFile);
			//創建一個和源文件大小一樣的byte[]
			byte[] b = new byte[is.available()];
			//讀取文件
			is.read(b);
			//直接對源文件壓縮
			byte[] huffmanBytes = huffmanZip(b);
			//創建文件的輸出流, 存放壓縮文件
			os = new FileOutputStream(dstFile);
			//創建一個和文件輸出流關聯的ObjectOutputStream
			oos = new ObjectOutputStream(os);
			//把 赫夫曼編碼後的字節數組寫入壓縮文件
			oos.writeObject(huffmanBytes); //我們是把
			//這裏我們以對象流的方式寫入 赫夫曼編碼,是爲了以後我們恢復源文件時使用
			//注意一定要把赫夫曼編碼 寫入壓縮文件
			oos.writeObject(huffmanCodes);
			
			
		}catch (Exception e) {
			// TODO: handle exception
			System.out.println(e.getMessage());
		}finally {
			try {
				is.close();
				oos.close();
				os.close();
			}catch (Exception e) {
				// TODO: handle exception
				System.out.println(e.getMessage());
			}
		}
		
	}
	
	//完成數據的解壓
	//思路
	//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"
	
	
	//編寫一個方法,完成對壓縮數據的解碼
	/**
	 * 
	 * @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++) {
			byte b = huffmanBytes[i];
			//判斷是不是最後一個字節
			boolean flag = (i == huffmanBytes.length - 1);
			stringBuilder.append(byteToBitString(!flag, b));
		}
		//把字符串安裝指定的赫夫曼編碼進行解碼
		//把赫夫曼編碼表進行調換,因爲反向查詢 a->100 100->a
		Map<String, Byte>  map = new HashMap<String,Byte>();
		for(Map.Entry<Byte, String> entry: huffmanCodes.entrySet()) {
			map.put(entry.getValue(), entry.getKey());
		}
		
		//創建要給集合,存放byte
		List<Byte> list = new ArrayList<>();
		//i 可以理解成就是索引,掃描 stringBuilder 
		for(int  i = 0; i < stringBuilder.length(); ) {
			int count = 1; // 小的計數器
			boolean flag = true;
			Byte b = null;
			
			while(flag) {
				//1010100010111...
				//遞增的取出 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"
		//把list 中的數據放入到byte[] 並返回
		byte b[] = new byte[list.size()];
		for(int i = 0;i < b.length; i++) {
			b[i] = list.get(i);
		}
		return b;
		
	}
 	
	/**
	 * 將一個byte 轉成一個二進制的字符串, 如果看不懂,可以參考我講的Java基礎 二進制的原碼,反碼,補碼
	 * @param b 傳入的 byte
	 * @param flag 標誌是否需要補高位如果是true ,表示需要補高位,如果是false表示不補, 如果是最後一個字節,無需補高位
	 * @return 是該b 對應的二進制的字符串,(注意是按補碼返回)
	 */
	private static String byteToBitString(boolean flag, byte b) {
		//使用變量保存 b
		int temp = b; //將 b 轉成 int
		//如果是正數我們還存在補高位
		if(flag) {
			temp |= 256; //按位與 256  1 0000 0000  | 0000 0001 => 1 0000 0001
		}
		String str = Integer.toBinaryString(temp); //返回的是temp對應的二進制的補碼
		if(flag) {
			return str.substring(str.length() - 8);
		} else {
			return str;
		}
	}
	
	//使用一個方法,將前面的方法封裝起來,便於我們的調用.
	/**
	 * 
	 * @param bytes 原始的字符串對應的字節數組
	 * @return 是經過 赫夫曼編碼處理後的字節數組(壓縮後的數組)
	 */
	private static byte[] huffmanZip(byte[] bytes) {
		List<Node> nodes = getNodes(bytes);
		//根據 nodes 創建的赫夫曼樹
		Node huffmanTreeRoot = createHuffmanTree(nodes);
		//對應的赫夫曼編碼(根據 赫夫曼樹)
		Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
		//根據生成的赫夫曼編碼,壓縮得到壓縮後的赫夫曼編碼字節數組
		byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);
		return huffmanCodeBytes;
	}
	
	
	//編寫一個方法,將字符串對應的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[1] = -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));
		}
		
		//System.out.println("測試 stringBuilder~~~=" + stringBuilder.toString());
		
		//將 "1010100010111111110..." 轉成 byte[]
		
		//統計返回  byte[] huffmanCodeBytes 長度
		//一句話 int len = (stringBuilder.length() + 7) / 8;
		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) { //因爲是每8位對應一個byte,所以步長 +8
				String strByte;
				if(i+8 > stringBuilder.length()) {//不夠8位
					strByte = stringBuilder.substring(i);
				}else{
					strByte = stringBuilder.substring(i, i + 8);
				}	
				//將strByte 轉成一個byte,放入到 huffmanCodeBytes
				huffmanCodeBytes[index] = (byte)Integer.parseInt(strByte, 2);
				index++;
		}
		return huffmanCodeBytes;
	}
	
	//生成赫夫曼樹對應的赫夫曼編碼
	//思路:
	//1. 將赫夫曼編碼表存放在 Map<Byte,String> 形式
	//   生成的赫夫曼編碼表{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
	static Map<Byte, String> huffmanCodes = new HashMap<Byte,String>();
	//2. 在生成赫夫曼編碼表示,需要去拼接路徑, 定義一個StringBuilder 存儲某個葉子結點的路徑
	static StringBuilder stringBuilder = new StringBuilder();
	
	
	//爲了調用方便,我們重載 getCodes
	private static Map<Byte, String> getCodes(Node root) {
		if(root == null) {
			return null;
		}
		//處理root的左子樹
		getCodes(root.left, "0", stringBuilder);
		//處理root的右子樹
		getCodes(root.right, "1", stringBuilder);
		return huffmanCodes;
	}
	
	/**
	 * 功能:將傳入的node結點的所有葉子結點的赫夫曼編碼得到,並放入到huffmanCodes集合
	 * @param node  傳入結點
	 * @param code  路徑: 左子結點是 0, 右子結點 1
	 * @param stringBuilder 用於拼接路徑
	 */
	private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
		StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
		//將code 加入到 stringBuilder2
		stringBuilder2.append(code);
		if(node != null) { //如果node == null不處理
			//判斷當前node 是葉子結點還是非葉子結點
			if(node.data == null) { //非葉子結點
				//遞歸處理
				//向左遞歸
				getCodes(node.left, "0", stringBuilder2);
				//向右遞歸
				getCodes(node.right, "1", stringBuilder2);
			} else { //說明是一個葉子結點
				//就表示找到某個葉子結點的最後
				huffmanCodes.put(node.data, stringBuilder2.toString());
			}
		}
	}
	
	//前序遍歷的方法
	private static void preOrder(Node root) {
		if(root != null) {
			root.preOrder();
		}else {
			System.out.println("赫夫曼樹爲空");
		}
	}
	
	/**
	 * 
	 * @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<Node>();
		
		//遍歷 bytes , 統計 每一個byte出現的次數->map[key,value]
		Map<Byte, Integer> counts = new HashMap<>();
		for (byte b : bytes) {
			Integer count = counts.get(b);
			if (count == null) { // Map還沒有這個字符數據,第一次
				counts.put(b, 1);
			} else {
				counts.put(b, count + 1);
			}
		}
		
		//把每一個鍵值對轉成一個Node 對象,並加入到nodes集合
		//遍歷map
		for(Map.Entry<Byte, Integer> entry: counts.entrySet()) {
			nodes.add(new Node(entry.getKey(), entry.getValue()));
		}
		return nodes;
		
	}
	
	//可以通過List 創建對應的赫夫曼樹
	private 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.weight + rightNode.weight);
			parent.left = leftNode;
			parent.right = rightNode;
			
			//將已經處理的兩顆二叉樹從nodes刪除
			nodes.remove(leftNode);
			nodes.remove(rightNode);
			//將新的二叉樹,加入到nodes
			nodes.add(parent);
			
		}
		//nodes 最後的結點,就是赫夫曼樹的根結點
		return nodes.get(0);
		
	}
	

}



//創建Node ,待數據和權值
class Node implements Comparable<Node>  {
	Byte data; // 存放數據(字符)本身,比如'a' => 97 ' ' => 32
	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 this.weight - o.weight;
	}
	
	public String toString() {
		return "Node [data = " + data + " weight=" + weight + "]";
	}
	
	//前序遍歷
	public void preOrder() {
		System.out.println(this);
		if(this.left != null) {
			this.left.preOrder();
		}
		if(this.right != null) {
			this.right.preOrder();
		}
	}
}

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