1. 程式人生 > 其它 >赫夫曼樹和赫夫曼編碼

赫夫曼樹和赫夫曼編碼

赫夫曼樹

定義

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

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

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

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

2)結點的權及帶權路徑長度:若將樹中結點賦給一個有著某種含義的數值,則這個數值稱為該結點的權。結點的帶權路徑長度

為:從根結點到該結點之間的路徑長度與該結點的權的乘積

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

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

赫夫曼樹建立思路

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

構成赫夫曼樹的步驟:

1)從小到大進行排序, 將每一個數據,每個資料都是一個節點 , 每個節點可以看成是一顆最簡單的二叉樹

2)取出根節點權值最小的兩顆二叉樹

3)組成一顆新的二叉樹, 該新的二叉樹的根節點的權值是前面兩顆二叉樹根節點權值的和

4)再將這顆新的二叉樹,以根節點的權值大小 再次排序, 不斷重複 1-2-3-4 的步驟,直到數列中,所有的資料都被處理,就得到一顆赫夫曼樹

圖解:

最後形成的樹就是赫夫曼樹。

程式碼實現

/**
 * @author wen.jie
 * @date 2021/8/26 15:21
 * 赫夫曼樹
 */
public class HuffmanTree {

    private Node root;

    public HuffmanTree(int[] arr){
        List<Node> nodes = new ArrayList<>();
        for (int value : arr) {
            nodes.add(new Node(value));
        }

        while (nodes.size()>1) {
            //排序所有元素
            Collections.sort(nodes);
            Node leftNode = nodes.get(0);
            Node rightNode = nodes.get(1);
            Node parent = new Node(leftNode.value + rightNode.value);
            parent.left = leftNode;
            parent.right = rightNode;
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            nodes.add(parent);
        }
        root = nodes.get(0);
    }

    /**
     * @author wen.jie
     * @date 2021/8/26 15:39
     * 前序遍歷
     */
    public void preOrder() {
        if(root == null) return;
        root.preOrder();
    }

    private class Node implements Comparable<Node>{
        //節點權值
        int value;
        Node left;
        Node right;

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

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

        void preOrder() {
            System.out.println(toString());
            if(this.left != null){
                this.left.preOrder();
            }
            if(this.right != null){
                this.right.preOrder();
            }
        }

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

測試:

        int[] arr = {13, 7, 8, 3, 29, 6, 1};
        HuffmanTree tree = new HuffmanTree(arr);
        tree.preOrder();

前序遍歷的結果:

赫夫曼編碼

簡介

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

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

3)赫夫曼編碼廣泛地用於資料檔案壓縮。其壓縮率通常在20%~90%之間

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

定長編碼

•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/

變長編碼

•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

說明:

1)原來長度是 359 , 壓縮了 (359-133) / 359 = 62.9%

2)此編碼滿足字首編碼, 即字元的編碼都不能是其他字元編碼的字首。不會造成匹配的多義性

注意, 這個赫夫曼樹根據排序方法不同,也可能不太一樣,這樣對應的赫夫曼編碼也不完全一樣,但是wpl是一樣的,都是最小的, 比如: 如果我們讓每次生成的新的二叉樹總是排在權值相同的二叉樹的最後一個,則生成的二叉樹為:

赫夫曼編碼實踐

資料壓縮

建立赫夫曼樹

根據赫夫曼編碼壓縮資料的原理,需要建立 "i like like like java do you like a java" 對應的赫夫曼樹.

這裡不難,程式碼如下:


/**
 * @author wen.jie
 * @date 2021/8/26 16:33
 */
public class HuffmanCode {

    private Node root = null;

    public HuffmanCode(String content){
        byte[] contentBytes = content.getBytes();
        //建立節點
        List<Node> nodes = createNodes(contentBytes);
        //建立赫夫曼樹
        root = createHuffmanTree(nodes);
        preOrder();
    }

    private void preOrder(){
        if(root == null) return;
        //前序遍歷
        root.preOrder();
    }

    private Node createHuffmanTree(List<Node> nodes) {
        while (nodes.size() > 1) {
            Collections.sort(nodes);
            Node leftNode = nodes.get(0);
            Node rightNode = nodes.get(1);
            Node parent = new Node(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            nodes.add(parent);
        }
        return nodes.get(0);
    }

    private List<Node> createNodes(byte[] contentBytes){
        Byte[] bytes = new Byte[contentBytes.length];
        for (int i = 0; i < contentBytes.length; i++) bytes[i] = contentBytes[i];
        ArrayList<Node> nodes = new ArrayList<>();
        Map<Byte, Integer> collect = Arrays.stream(bytes)
                .collect(Collectors.groupingBy(Function.identity(), Collectors.reducing(0, e -> 1, Integer::sum)));
        collect.forEach((k, v) -> nodes.add(new Node(k, v)));
        return nodes;
    }

    private class Node implements Comparable<Node>{
        //存放資料本身
        Byte data;
        //權值,表示字元出現的次數
        int weight;
        Node left;
        Node right;

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

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

        @Override
        public int compareTo(Node o) {
            return this.weight - o.weight;
        }

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

測試:

        String str = "i like like like java do you like a java";
        HuffmanCode huffmanCode = new HuffmanCode(str);

生成赫夫曼編碼表

思路:

1.將赫夫曼編碼表放到Map中:32->01 97->100 等等

程式碼實現:

    //赫夫曼編碼表
    private Map<Byte, String> huffmanCodes = new HashMap<>();
    private StringBuilder sb = new StringBuilder();
    private final String LEFT = "0";
    private final String RIGHT = "1";

    public HuffmanCode(String content){
        byte[] contentBytes = content.getBytes();
        //建立節點
        List<Node> nodes = createNodes(contentBytes);
        //建立赫夫曼樹
        root = createHuffmanTree(nodes);
        getCodes(root);
        System.out.println(huffmanCodes);
    }

    private void getCodes(Node root){
        if (root == null) return;
        //如果只有一個根節點,就直接放進huffmanCodes中
        if (root.left == null && root.right == null)
            huffmanCodes.put(root.data, LEFT);
        getCodes(root.left, LEFT, sb);
        getCodes(root.right, RIGHT, sb);
    }

    /**
     * @author wen.jie
     * @date 2021/8/26 17:13
     * @param code 左子節點為0,右子節點為1
     */
    private void getCodes(Node node, String code, StringBuilder stringBuilder){
        StringBuilder builder = new StringBuilder(stringBuilder);
        builder.append(code);
        if(node != null ) {
            if(node.data == null){
                //非葉子節點
                getCodes(node.left, LEFT, builder);
                getCodes(node.right, RIGHT, builder);
            }else {
                //葉子節點
                huffmanCodes.put(node.data, builder.toString());
            }
        }
    }

重新執行測試方法:

壓縮生成赫夫曼編碼

修改程式碼:

/**
 * @author wen.jie
 * @date 2021/8/26 16:33
 */
public class HuffmanCode {

    private Node root = null;

    //赫夫曼編碼表
    private Map<Byte, String> huffmanCodes = new HashMap<>();
    private StringBuilder sb = new StringBuilder();
    private String content;
    private final String LEFT = "0";
    private final String RIGHT = "1";

    public HuffmanCode(String content){
        this.content = content;
        //建立節點
        List<Node> nodes = createNodes(content.getBytes());
        //建立赫夫曼樹
        root = createHuffmanTree(nodes);
        getCodes(root);
    }

    public byte[] huffmanZip(){
        return huffmanZip(content.getBytes(), huffmanCodes);
    }

    private static byte[] huffmanZip(byte[] bytes, Map<Byte, String> huffmanCodes) {
        StringBuilder stringBuilder = new StringBuilder();
        for (byte b : bytes)
            stringBuilder.append(huffmanCodes.get(b));
        int len = (stringBuilder.length() + 7) / 8;
        byte[] huffmanCodeBytes = new byte[len];
        //每8位對應一個byte,所以步長為8
        int index = 0;
        for (int i = 0; i < stringBuilder.length(); i+= 8) {
            String strByte;
            if(i+8 > stringBuilder.length()){
                strByte = stringBuilder.substring(i);
            }else {
                strByte = stringBuilder.substring(i, i +8);
            }
            //將strByte轉成一個byte
            huffmanCodeBytes[index++] = (byte)Integer.parseInt(strByte, 2);
        }
        return huffmanCodeBytes;
    }
	//Node、createNodes、createHuffmanTree、preOrder、getCodes程式碼省略
}

測試:

        String str = "i like like like java do you like a java";
        byte[] zip = HuffmanCode.huffmanZip(str);
        System.out.println(Arrays.toString(zip));

原本長度為40,現在長度為17,實現了壓縮。

資料的解壓

要求:將[-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

程式碼實現:

    public Map<Byte, String> getHuffmanCodes() {
        return huffmanCodes;
    }

    /**
     * @author wen.jie
     * @date 2021/8/26 20:15
     * @param huffmanCodes 赫夫曼編碼表
     * @param huffmanBytes 赫夫曼編碼得到的位元組陣列
     */
    public String decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
        //先得到huffmanBytes對應的二進位制的字串
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < huffmanBytes.length; i++) {
            boolean flag = (i == huffmanBytes.length -1);
            sb.append(byteToBitString(huffmanBytes[i], !flag));
        }
        HashMap<String, Byte> map = new HashMap<>();
        for (Byte bt : huffmanCodes.keySet()){
            map.put(huffmanCodes.get(bt), bt);
        }
        ArrayList<Byte> list = new ArrayList<>();
        //這裡利用棧資料結構對字串進行匹配
        Stack<String> stack = new Stack<>();
        for (int i = 0; i < sb.length(); i++) {
            String str = sb.substring(i, i + 1);
            stack.push(str);
            String key = jointStack(stack);
            if(map.containsKey(key)){
                list.add(map.get(key));
                stack.removeAll();
            }
        }
        byte[] b = new byte[list.size()];
        for (int i = 0; i < b.length; i++)
            b[i] = list.get(i);
        return new String(b);
    }

    /**
     * @author wen.jie
     * @date 2021/8/26 21:02
     * 拼接棧中所有元素
     */
    private static String jointStack(Stack<String> stack){
        StringBuilder builder = new StringBuilder();
        for (String str : stack) {
            builder.append(str);
        }
        return builder.reverse().toString();
    }

    /**
     * @author wen.jie
     * @date 2021/8/26 19:51
     * 將一個byte轉成一個二進位制字串
     * @param flag 標誌是否需要補高位,最後一位不需要補高位
     */
    private static String byteToBitString(byte b, boolean flag) {
        return flag
                ? Integer.toBinaryString((b & 0xFF) + 0x100).substring(1)
                : Integer.toBinaryString(b);
    }

測試:

    @Test
    public void test2(){
        String str = "i like like like java do you like a java";
        // 1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
        HuffmanCode huffmanCode = new HuffmanCode(str);
        byte[] bytes = huffmanCode.huffmanZip();
        System.out.println("加密:" + Arrays.toString(bytes));
        System.out.println("解密:" + huffmanCode.decode(huffmanCode.getHuffmanCodes(), bytes));
    }

檔案壓縮

這裡檔案壓縮與上面的資料壓縮思路一致,檔案壓縮主要多了一個檔案讀取和寫入的操作:

    public void zipFile(String src, String dest) {
        try (FileInputStream fis = new FileInputStream(src);
             ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(dest))) {
            byte[] b = new byte[fis.available()];
            fis.read(b);
            byte[] huffmanBytes = huffmanZip(b);
            oos.writeObject(huffmanBytes);
            oos.writeObject(huffmanCodes);
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    public byte[] huffmanZip(byte[] bytes) {
        List<Node> nodes = createNodes(bytes);
        //建立赫夫曼樹
        root = createHuffmanTree(nodes);
        getCodes(root);
        return huffmanZip(bytes, huffmanCodes);
    }

測試壓縮:

        HuffmanCode huffmanCode = new HuffmanCode();
        huffmanCode.zipFile("D:\\紅樓夢.txt", "D:\\紅樓夢.zip");

檔案解壓

    public void unzipFile(String zipFile, String destFile) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(zipFile));
             FileOutputStream fos = new FileOutputStream(destFile)){
            byte[] huffmanBytes = (byte[])ois.readObject();
            Map<Byte, String> codes = (Map<Byte, String>)ois.readObject();
            byte[] bytes = decode(codes, huffmanBytes).getBytes();
            fos.write(bytes);
        } catch (IOException | ClassNotFoundException e){
            e.printStackTrace();
        }
    }

測試:

    HuffmanCode huffmanCode = new HuffmanCode();
    huffmanCode.unzipFile("D:\\紅樓夢.zip", "D:\\紅樓夢2.txt");

檔案開啟來,也是可讀無誤的:

赫夫曼編碼壓縮檔案注意事項

1)如果檔案本身就是經過壓縮處理的,那麼使用赫夫曼編碼再壓縮效率不會有明顯變化, 比如視訊,ppt 等等檔案 [舉例壓一個 .ppt]

2)赫夫曼編碼是按位元組來處理的,因此可以處理所有的檔案(二進位制檔案、文字檔案) [舉例壓一個.xml檔案]

3)如果一個檔案中的內容,重複的資料不多,壓縮效果也不會很明顯.

本文所有程式碼均已上傳:https://gitee.com/wj204811/algorithm