(11)《數據結構與算法》之赫夫曼樹

在咱們開始介紹赫夫曼樹以前,咱們先帶入一個情景。你想發送一個文件給你朋友,可是文件太大,因此你決定將文件壓縮,變小再發送。你有沒有考慮文件是怎麼壓縮呢?做爲程序員,沒有考慮過這裏使用的什麼算法呢?赫夫曼編碼就是其中的一種解決方法。java

在介紹赫夫曼編碼以前,咱們先介紹先導知識——赫夫曼樹node

赫夫曼樹

赫夫曼樹,又稱最優樹,是一類帶權路徑長度最短的樹。程序員

定義: 假設有n個權值{$W_1,W_2...W_n$},試構造一個有n個葉子結點的二叉樹,每一個葉子結點帶權爲w,則其中帶權路徑長度WPL最小的二叉樹稱作最優二叉樹赫夫曼樹
光看着定義,大部分人,確定雲裏霧裏的。接下來咱們就好好解釋下,什麼叫作赫夫曼樹。
首先咱們瞭解兩個基本概念算法

  1. 路徑和路徑長度:在一棵樹中,從一個結點往下能夠達到的孩子或孫子結點之間的通路,稱爲路徑。通路中分支的數目稱爲路徑長度。若規定根結點的層數爲1,則從根結點到第L層結點的路徑長度爲L-1
  2. 結點的權及帶權路徑長度:若將樹中結點賦給一個有着某種含義的數值,則這個數值稱爲該結點的權。結點的帶權路徑長度爲:從根結點到該結點之間的路徑長度與該結點的權的乘積
  3. 樹的帶權路徑長度:樹的帶權路徑長度規定爲全部葉子結點的帶權路徑長度之和,記爲WPL(weighted path length) ,權值越大的結點離根結點越近的二叉樹纔是最優二叉樹
  4. $WPL= \sum_{k = 1} ^ {n} {W_k L_k}$最小的就是赫夫曼樹

畫圖描述,更加直觀
如今有3棵二叉樹,都有4個結點,分別帶權13, 7, 8, 3
在這裏插入圖片描述
第二棵樹的WPL= 59,最小,故,爲赫夫曼樹。數組

赫夫曼樹建立思路圖解

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

思路分析

{13, 7, 8, 3, 29, 6, 1}app

構成赫夫曼樹的步驟ide

  1. 從小到大進行排序,將每個數據,每一個數據都是一個節點,每一個節點能夠當作是一棵最簡單的二叉樹
  2. 取出根節點權值最小的兩棵二叉樹
  3. 利用上面取出的兩棵二叉樹,組成一棵新的二叉樹,該新的二叉樹根節點的權值是這兩棵二叉樹根節點權值之和
  4. 再將這棵新的二叉樹,以根節點的權值大小 再排序,不斷重複 1 - 2 - 3 - 4的步驟,知道數列中,全部的數據都被處理,就獲得一棵赫夫曼樹
    在這裏插入圖片描述

接下來咱們就開始寫程序學習

import java.util.*;

/**
 * 先導知識:
 *      1. String 類的方法 public byte[] getBytes()
 *          功能:使用平臺的默認字符集將此 String 編碼爲 byte 序列,並將結果存儲到一個新的 byte 數組中。即,將字符串 轉化成 字符數組,便於操做
 *      2. HashMap <Key, Value>  類的方法 public V get(Object key)
 *          功能:返回指定鍵所映射的值;若是對於該鍵來講,此映射不包含任何映射關係,則返回 null。
 *      3. Collecctions 類的 public static  void sort(List<T> list)
 *          功能:根據元素的 天然順序 對指定列表按升序進行排序。
 *
 */

/**
 * 功能:根據赫夫曼編碼壓縮的原理,須要建立"i like like like java do you like a java"對應的赫夫曼樹
 *
 * 思路:
 *      (1) Node{data (存放數據), weight(權值), left, right}
 *      (2) 獲得"i like like like java do you like a java" 對應的byte[]數組
 *      (3) 編寫一個方法,將準備構建赫夫曼的Node 結點 放到List, 形式[Node[data = '7', weight = '5']...],
 *      體現 d: 1, y: 1, u: 1, j: 2, v: 2, o: 2, l: 4, k: 4, k: 4, e: 4, i: 5, a: 5, (空格):9
 *      (4) 能夠經過List建立對應的赫夫曼樹
 */
public class HuffmanCode {
    public static void main(String[] args) {

        //(2) 獲得"i like like like java do you like a java" 對應的byte[]數組
        String content = "i like like like java do you like a java";
        //如何計算出,content字符串中各個字符出現的頻率,即,對應weight
        //把字符串變成單個的字符
        byte[] contentBytes = content.getBytes();
        System.out.println(contentBytes.length);

        List<Node> nodes = getNodes(contentBytes);
        System.out.println("nodes = " + nodes);

        //測試建立的二叉樹
        System.out.println("赫夫曼樹");
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        System.out.println("前序遍歷");
        huffmanTreeRoot.preOrder();     
    }

    //前序遍歷的方法
    private static void preOrder(Node root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("赫夫曼樹爲空");
        }
    }

    //(3) 編寫一個方法,將準備構建赫夫曼的Node 結點 放到List, 形式[Node[data = '7', weight = '5']...],
    //體現 d: 1, y: 1, u: 1, j: 2, v: 2, o: 2, l: 4, k: 4, k: 4, e: 4, i: 5, a: 5, (空格):9

    /**
     *
     * @param bytes 接收字節數組
     * @return 返回List 形式   [Node[data = '7', weight = 97]...]
     */
    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尚未這個字符數據
                //第一次,加入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;
    }

    //(4) 能夠經過List建立對應的赫夫曼樹
    /**
     *
     * @param nodes List類的結點 集合
     * @return
     */
    private static Node createHuffmanTree(List<Node> nodes) {
        while (nodes.size() > 1) {
            //排序,從小到大
            Collections.sort(nodes);
            //取出第一棵最小的二叉樹
            Node leftNode = nodes.get(0);
            //取出第二小的二叉樹
            Node rightNode = nodes.get(1);
            //建立一個新的二叉樹,它的根結點parent,沒有data,只有權值weight
            Node parent = new Node(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;

            //將已經處理的兩個二叉樹從nodes刪除。nodes 是 List 類型
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //將新的二叉樹加入到nodes,
            nodes.add(parent);

        }
        //nodes 最後的結點,就是赫夫曼樹的根結點
        return nodes.get(0);
    }
}

//(1) Node{data (存放數據), weight(權值), left, right}
//建立Node,帶數據和權值
//接口Comparable<Node> 對Node類型的數據進行比較
class Node implements Comparable<Node> {
    //存放數據(字符)自己,好比'a' => 97 '' => 32
    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) {
        //Node類型的數據從小到大排序
        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. 赫夫曼編碼也翻譯爲 哈夫曼編碼(Huffman Coding),又稱霍夫曼編碼,是一種編碼方式, 屬於一種程序算法
  2. 赫夫曼編碼普遍地用於數據文件壓縮。其壓縮率一般在20%~90%之間
  3. 赫夫曼碼是可變字長編碼(VLC)的一種。

原理剖析

定長編碼
  • 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 (包括空格)

    變長編碼
  • 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...
  • 字符的編碼都不能是其餘字符編碼的前綴,符合此要求的編碼叫作前綴編碼, 即不能匹配到重複的編碼

赫夫曼編碼
  • 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 , 編碼以下:
    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
 說明:
  原來長度是 359 , 壓縮了 (359-133) / 359 = 62.9%
 此編碼知足前綴編碼, 即字符的編碼都不能是其餘字符編碼的前綴。不會形成匹配的多義性

注意事項

這個赫夫曼樹根據排序方法不一樣,也可能不太同樣,這樣對應的赫夫曼編碼也不徹底同樣,可是wpl 是同樣的,都是最小的, 好比: 若是咱們讓每次生成的新的二叉樹老是排在權值相同的二叉樹的最後一個,則生成的二叉樹爲:

在這裏插入圖片描述
接下來咱們開始進行實際應用

根據赫夫曼編碼編碼原理對 字符串"i like like like java do you like a java" ,對其進行數據壓縮處理 ,形式如 1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110,最後獲得,十進制數組 [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]。可是這並非結束,由於壓縮以後,一定也須要解壓。解壓的過程就是將 [-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28] =>字符串"i like like like java do you like a java"

解題步驟

步驟1: 根據赫夫曼編碼壓縮數據的原理,須要建立 "i like like like java do you like a java" 對應的赫夫曼樹.
步驟2

  1. 生成赫夫曼樹對應的赫夫曼編碼 , 以下表: (空格) =01 a=100 d=11000 u=11001 e=1110 v=11011 i=101 y=11010 j=0010 k=1111 l=000 o=0011
  2. 使用赫夫曼編碼來生成赫夫曼編碼數據 ,即按照上面的赫夫曼編碼,將"i like like like java do you like a java" 字符串生成對應的編碼數據, 形式以下.1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
  3. 再將取得的字符串,每取8位,得到十進制數組[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28],例如:1010 1000(負數補碼)=》源碼 1101 1000 =》 十進制 -88
    步驟3
  4. 將壓縮後的結果[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28],從新先轉成 赫夫曼編碼對應的二進制字符串「1010100010111...」
  5. 將赫夫曼編碼對應的二進制的字符串"1010100010111..." =》 對照 赫夫曼編碼 =》"i like like like java do you like a java"

具體代碼實現

import java.util.*;

/**
 *  壓縮 =》 解壓
 */
 
public class HuffmanCode {
    public static void main(String[] args) {

        //(2) 獲得"i like like like java do you like a java" 對應的byte[]數組
        String content = "i like like like java do you like a java";
        //如何計算出,content字符串中各個字符出現的頻率,即,對應weight
        //把字符串變成單個的字符
        byte[] contentBytes = content.getBytes();
        System.out.println(contentBytes.length);

        byte[] huffmanCodeBytes = huffmanZip(contentBytes);
        System.out.println("壓縮後的結果huffmanCodeBytes = " + Arrays.toString(huffmanCodeBytes));
        System.out.println("長度 = " + huffmanCodeBytes.length);
        
        /**
         *  數據壓縮已經完成,那麼解壓怎麼操做呢?
         */

        //測試 byteToBitString方法
        System.out.println("===========================================================");
//        byteToBitString((byte)-1);
        //可是當輸入爲 1 時,就會出現新的問題,StringIndexOutOfBoundsException: String index out of range: -7
//        byteToBitString((byte) 1);

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

    }

    //完成數據的解壓
    //思路
    //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數組,形式如:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28],轉成二進制的字符串
        for (int i = 0; i < huffmanBytes.length; i++) {
            byte b = huffmanBytes[i];

            //判斷是否是最後一個字節
            boolean flag = (i == huffmanBytes.length - 1);
            stringBuilder.append(byteToBitString(!flag, b));
        }
        System.out.println("赫夫曼字節數組對應的二進制字符串 = " + stringBuilder.toString());

        //把字符串按照指定的赫夫曼編碼進行解碼
        //把赫夫曼編碼表進行調換,由於反向查詢 97 =》 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
                //i 不動, 讓count移動,指定匹配到一個字符
                String key = stringBuilder.substring(i, 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 轉成一個二進制的字符串
     * @param b
     * @param flag 標誌是否須要補高位。若是是true, 表示須要補高位;若是是false表示不補。若是是最後一個字節,無需補高位
     * @return 是該b 對應二進制的字符串,(注意是按補碼返回)
     */
    private static String byteToBitString(boolean flag, byte b) {
        //使用變量保存b,將b 轉成 int
        int temp = b;
        //若是是正數,還存在補高位的問題
        if (flag) {
            // temp 按位與 256 , 假如temp = 1, 1 0000 0000 | 0000 0001 =》 1 0000 0001
            /**
             * 爲何要存在 temp |= 256;?
             *  huffmanBytes 存在 負數 和 正數。負數轉化成二進制補碼是8位,可是 正數 轉成 二進制 並不必定是 8位,好比 77,這個數,轉化成二進制位 100 1101
             *  根據赫夫曼壓縮程序可知, 壓縮後的數據 77 是取壓縮後的二進制字符數組的 8位而得來, 因此咱們在將77 從新轉爲而二進制時,每一位(包括高位的0)均不可省略
             *  因此,咱們 將 77 | 256 獲得 1 0100 1101 ,再取後8位,這樣就能夠獲得77 完整的二進制數
             *
             *  可能你又要問,爲何在flag爲 true,即,不是最後一個數的時候,執行temp |= 256;?
             *   一樣,咱們要看壓縮的時候,是怎麼取二進制數,並將其轉爲 byte 數的。咱們就會發現,就算最後一位數的二進制數,不滿8位,也不影響,由於壓縮的時候就沒有取 8位
             */
            temp |= 256;
        }

        String str = Integer.toBinaryString(temp);

        if (flag) {
            System.out.println("str = " + str);
            //輸出結果:str = 11111111111111111111111111111111
            //可是咱們只要後8位便可
            System.out.println("str = " + str.substring(str.length() - 8));
            //輸出結果:str = 11111111
        } else {
            return str;
        }

        return str.substring(str.length() - 8);
    }

    //使用一個方法,將面前的方法封裝起來,便於咱們的調用
    /**
     *
     * @param contentBytes 原始的字符串對應的字節數組
     * @return 是通過赫夫曼編碼後的字節數組(壓縮後的數據)
     */
    private static byte[] huffmanZip(byte[] contentBytes) {
        List<Node> nodes = getNodes(contentBytes);
        //根據nodes建立赫夫曼樹
        Node huffmaTreeRoot = createHuffmanTree(nodes);

        //生成對應的哈夫曼編碼(根據赫夫曼樹)
        Map<Byte, String> huffmanCodes = getCodes(huffmaTreeRoot);

        //根據生成的赫夫曼編碼壓縮獲得壓縮後的赫夫曼編碼字節數組
        byte[] huffmanCodeBytes = zip(contentBytes,huffmanCodes);

        return huffmanCodeBytes;
    }


    //編寫一個方法,將一個字符串對應的byte[] 數組,經過生成的赫夫曼編碼表,返回一個赫夫曼編碼 壓縮後的byte數組
    /**
     *
     * @param bytes  原始的字符串對應的byte[],即,對應程序中的contentBytes
     * @param huffmanCodes  生成的赫夫曼編碼Map
     * @return  返回赫夫曼編碼處理後的byte[] ,
     * 舉例:String content = "i like like like java do you like a java"; =》 byte[] contentBytes = content.getBytes();
     * 返回的字符串是:1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
     * =》 對應的byte[] huffmanCodeBytes , 即 8位對應一個byte, 放入huffmanCodeBytes
     * huffmanCodeBytes[0] = 1010 1000(補碼)=》源碼 1101 1000 =》 十進制 -88
     * huffmanCodeBytes[1]
     */
    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);

        //將字符串「101010011011...」 轉成 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;
        }
        //建立huffmanCodeBytes
        //建立 存儲壓縮後的 byte 數組
        byte[] huffmanCodeBytes = new byte[len];
        //記錄第幾個byte
        int index = 0;
        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> 形式以下:
     *          (空格)= 01, a = 100, d = 1100, u = 11001, e = 1110, v = 11011, i = 101
     *          y = 11010, j = 0010,  k = 1111, l = 000, o = 0011
     *      2. 在生成赫夫曼編碼表時,須要建立拼接路徑,定義一個StringBuilder,存儲某個葉子結點的路徑
     *
     */

    static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();

    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);
        //若是node == null 則不處理
        if (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("赫夫曼樹爲空");
        }
    }

    //(3) 編寫一個方法,將準備構建赫夫曼的Node 結點 放到List, 形式[Node[data = '7', weight = '5']...],
    //體現 d: 1, y: 1, u: 1, j: 2, v: 2, o: 2, l: 4, k: 4, k: 4, e: 4, i: 5, a: 5, (空格):9
    /**
     *
     * @param bytes 接收字節數組
     * @return      返回List 形式   [Node[data = '7', weight = 97]...]
     */
    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尚未這個字符數據
                //第一次,加入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;
    }

    //(4) 能夠經過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);
            //建立一個新的二叉樹,它的根結點parent,沒有data,只有權值weight
            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);
    }
}

//(1) Node{data (存放數據), weight(權值), left, right}
//建立Node,帶數據和權值
//接口Comparable<Node> 對Node類型的數據進行比較
class Node implements Comparable<Node>{
    //存放數據(字符)自己,好比'a' => 97 '' => 32
    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) {
        //Node類型的數據從小到大排序
        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();
        }
    }
}

赫夫曼編碼應用:文件壓縮與解壓

代碼

注意: 文件壓縮的代碼是直接在上面代碼的基礎上寫的,因此,有不少殘留的的註釋。因此,本文的三個代碼最好先從第一個開始看起

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

/**
 * 實際應用:將 文件 壓縮
 */

public class HuffmanCode {
    public static void main(String[] args) {

        //測試壓縮文件
        String srcFile = "E:\\1學習\\程序\\Data-Structures(java)\\src.bmp";
        String dstFile = "E:\\1學習\\程序\\Data-Structures(java)\\dst.zip";

        zipFile(srcFile, dstFile);
        System.out.println("壓縮文件成功");

//        //測試解壓文件
//        String zipFile = "E:\\\\1學習\\\\程序\\\\Data-Structures(java)\\\\dst.zip";
//        String dstFile = "E:\\\\1學習\\\\程序\\\\Data-Structures(java)\\\\src2.bmp";
//        unZipFile(zipFile, dstFile);
//        System.out.println("解壓成功");

    }

    //編寫一個方法完成對壓縮文件的解壓

    /**
     * @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> huffumanCodes = (Map<Byte, String>) ois.readObject();

            //解碼
            byte[] bytes = decode(huffumanCodes, huffmanBytes);
            //將bytes 數組寫入到 目標文件,輸出流
            os = new FileOutputStream(dstFile);
            //寫數據到dstFile文件中
            os.write(bytes);

        } catch (Exception e) {
            e.printStackTrace();

        } finally {

            //順序不能錯
            try {
                os.close();
                ois.close();
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

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

    /**
     * @param srcFile 傳入的但願壓縮的文件的全路徑
     * @param dstFile 咱們壓縮後將壓縮文件放到哪一個文件目錄下
     */
    public static void zipFile(String srcFile, String dstFile) {
        //建立輸出流
        OutputStream os = null;

        //建立文件的輸出流的對象
        //!!!關鍵
        ObjectOutputStream oos = null;

        //建立文件的輸入流
        FileInputStream fis = null;
        try {
            //建立一個文件的輸入流
            fis = new FileInputStream(srcFile);

            //建立一個和源文件大小同樣的byte[]
            byte[] b = new byte[fis.available()];
            //讀取文件
            fis.read(b);

            //直接對源文件進行壓縮
            byte[] huffmanBytes = huffmanZip(b);

            //建立文件的輸出流,存放壓縮文件
            os = new FileOutputStream(dstFile);
            //建立一個和文件輸出流關聯的ObjectOutputStream
            oos = new ObjectOutputStream(os);

            //把 赫夫曼編碼後的字節數組寫入壓縮文件
            oos.writeObject(huffmanBytes);

            //咱們以對象流的方式寫入赫夫曼的編碼,是爲了之後咱們恢復源文件時使用
            //必定要把赫夫曼編碼寫入 壓縮文件
            oos.writeObject(huffmanCodes);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                fis.close();
                oos.close();
                os.close();
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
    }

    /**
     * @param huffmanCodes 赫夫曼編碼表 Map
     * @param huffmanBytes 赫夫曼編碼獲得的字節數組
     * @return 就是原來的字符串對應的數組
     */
    //編寫一個方法完成對壓縮數據的解碼
    private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
        //1. 先獲得huffmanBytes 對應的 二進制的字符串,形式如"1010100010111..."
        StringBuilder stringBuilder = new StringBuilder();
        //將byte數組,形式如:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28],轉成二進制的字符串
        for (int i = 0; i < huffmanBytes.length; i++) {
            byte b = huffmanBytes[i];
            //判斷是否是最後一個字節
            boolean flag = (i == huffmanBytes.length - 1);
            stringBuilder.append(byteToBitString(!flag, b));
        }
//        System.out.println("赫夫曼字節數組對應的二進制字符串 = " + stringBuilder.toString());

        //把字符串按照指定的赫夫曼編碼進行解碼
        //把赫夫曼編碼表進行調換,由於反向查詢 97 =》 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
                //i 不動, 讓count移動,指定匹配到一個字符
                String key = stringBuilder.substring(i, 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 轉成一個二進制的字符串
     *
     * @param b
     * @param flag 標誌是否須要補高位,若是是true, 表示須要補高位,若是是false表示不補.若是是最後一個字節,無需補高位
     * @return 是該b 對應二進制的字符串,(注意是按補碼返回)
     */
    private static String byteToBitString(boolean flag, byte b) {
        //使用變量保存b,將b 轉成 int
        int temp = b;
        //若是是正數,還存在補高位的問題
        if (flag) {
            // temp 按位與 256 , 假如temp = 1, 1 0000 0000 | 0000 0001 =》 1 0000 0001
            temp |= 256;
        }

        String str = Integer.toBinaryString(temp);

        if (flag) {
//            System.out.println("str = " + str);
            //輸出結果:str = 11111111111111111111111111111111
            //可是咱們只要後8位便可
//            System.out.println("str = " + str.substring(str.length() - 8));
            //輸出結果:str = 11111111
        } else {
            return str;
        }
        return str.substring(str.length() - 8);
    }

    //使用一個方法,將面前的方法封裝起來,便於咱們的調用

    /**
     * @param contentBytes 原始的字符串對應的字節數組
     * @return 是通過赫夫曼編碼後的字節數組(壓縮後的數據)
     */
    private static byte[] huffmanZip(byte[] contentBytes) {
       //根據數組創造結點
        List<Node> nodes = getNodes(contentBytes);

        //根據nodes建立赫夫曼樹
        Node huffmaTreeRoot = createHuffmanTree(nodes);

        //生成對應的哈夫曼編碼(根據赫夫曼樹)
        Map<Byte, String> huffmanCodes = getCodes(huffmaTreeRoot);

        //根據生成的赫夫曼編碼壓縮獲得壓縮後的赫夫曼編碼字節數組
        byte[] huffmanCodeBytes = zip(contentBytes, huffmanCodes);

        return huffmanCodeBytes;
    }


    //編寫一個方法,將一個字符串對應的byte[] 數組,經過生成的赫夫曼編碼表,返回一個赫夫曼編碼 壓縮後的byte數組

    /**
     * @param bytes        原始的字符串對應的byte[],即,對應程序中的contentBytes
     * @param huffmanCodes 生成的赫夫曼編碼Map
     * @return 返回赫夫曼編碼處理後的byte[] ,
     * 舉例:String content = "i like like like java do you like a java"; =》 byte[] contentBytes = content.getBytes();
     * 返回的字符串是:1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
     * =》 對應的byte[] huffmanCodeBytes , 即 8位對應一個byte, 放入huffmanCodeBytes
     * huffmanCodeBytes[0] = 1010 1000(補碼)=》源碼 1101 1000 =》 十進制 -88
     * huffmanCodeBytes[1]
     */
    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);

        //將字符串「101010011011...」 轉成 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;
        }
        //建立huffmanCodeBytes
        //建立 存儲壓縮後的 byte 數組
        byte[] huffmanCodeBytes = new byte[len];
        //記錄第幾個byte
        int index = 0;
        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;
    }

    //生成的赫夫曼樹對應的赫夫曼編碼
    static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();
    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);
        //若是node == null 則不處理
        if (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("赫夫曼樹爲空");
        }
    }

    //(3) 編寫一個方法,將準備構建赫夫曼的Node 結點 放到List, 形式[Node[data = '7', weight = '5']...],
    //體現 d: 1, y: 1, u: 1, j: 2, v: 2, o: 2, l: 4, k: 4, k: 4, e: 4, i: 5, a: 5, (空格):9
    /**
     * @param bytes 接收字節數組
     * @return 返回List 形式   [Node[data = '7', weight = 97]...]
     */
    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尚未這個字符數據
                //第一次,加入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;
    }

    //(4) 能夠經過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);
            //建立一個新的二叉樹,它的根結點parent,沒有data,只有權值weight
            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);
    }
}

//(1) Node{data (存放數據), weight(權值), left, right}
//建立Node,帶數據和權值
//接口Comparable<Node> 對Node類型的數據進行比較
class Node implements Comparable<Node> {
    //存放數據(字符)自己,好比'a' => 97 '' => 32
    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) {
        //Node類型的數據從小到大排序
        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. 若是文件自己就是通過壓縮處理的,那麼使用赫夫曼編碼再壓縮效率不會有明顯變化, 好比視頻,ppt 等等文件 (ppt文件自己就是已經被壓縮過的)
  2. 赫夫曼編碼是按字節來處理的,所以能夠處理全部的文件(二進制文件、文本文件)
  3. 若是一個文件中的內容,重複的數據很少,壓縮效果也不會很明顯. 舉例:一張純白色的照片壓縮的效果比一張視頻截圖的壓縮效果好。由於白色照片的重複數據比較多

此文是看過韓順平老師的《數據結構與算法》(java版)以後在寫的,一是爲了之後複習,二是爲了方便你們。若是有人須要韓順平老師的課件和代碼能夠私信我

以上全部程序都在IDEA中運行過,沒有任何問題。謝謝你們!共勉! PS:剛開始寫博客,不少內容本想寫的詳細一些,奈何自己實力和寫做能力欠缺沒法完成。若是寫的不對的地方,但願你們能夠提出寶貴的意見。謝謝!

相關文章
相關標籤/搜索