Java 樹結構實際應用 二(哈夫曼樹和哈夫曼編碼)

十四lin發表於2021-03-14
 赫夫曼樹
1 基本介紹
1) 給定 n 個權值作為 n 個葉子結點,構造一棵二叉樹,若該樹的帶權路徑長度(wpl)達到最小,稱這樣的二叉樹為
最優二叉樹,也稱為哈夫曼樹(Huffman Tree), 還有的書翻譯為霍夫曼樹。
2) 赫夫曼樹是帶權路徑長度最短的樹,權值較大的結點離根較近
 
2 赫夫曼樹幾個重要概念和舉例說明
1) 路徑和路徑長度:在一棵樹中,從一個結點往下可以達到的孩子或孫子結點之間的通路,稱為路徑。通路
中分支的數目稱為路徑長度。若規定根結點的層數為 1,則從根結點到第 L 層結點的路徑長度為 L-1
2) 結點的權及帶權路徑長度:若將樹中結點賦給一個有著某種含義的數值,則這個數值稱為該結點的權。結
點的帶權路徑長度為:從根結點到該結點之間的路徑長度與該結點的權的乘積
3) 樹的帶權路徑長度:樹的帶權路徑長度規定為所有葉子結點的帶權路徑長度之和,記為 WPL(weighted path
length) ,權值越大的結點離根結點越近的二叉樹才是最優二叉樹。
4) WPL 最小的就是赫夫曼樹

 

3 赫夫曼樹建立思路圖解
給你一個數列 {13, 7, 8, 3, 29, 6, 1},要求轉成一顆赫夫曼樹.
 思路分析(示意圖):
{13, 7, 8, 3, 29, 6, 1}
構成赫夫曼樹的步驟:
1) 從小到大進行排序, 將每一個資料,每個資料都是一個節點 , 每個節點可以看成是一顆最簡單的二叉樹
2) 取出根節點權值最小的兩顆二叉樹
3) 組成一顆新的二叉樹, 該新的二叉樹的根節點的權值是前面兩顆二叉樹根節點權值的和
4) 再將這顆新的二叉樹,以根節點的權值大小 再次排序, 不斷重複 1-2-3-4 的步驟,直到數列中,所有的數
據都被處理,就得到一顆赫夫曼樹
5) 圖解:

 

4 赫夫曼樹的程式碼實現
package huffmanTree;

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

public class HuffmanTree {

    public static void main(String[] args) {
        int[] arr = {13, 7, 8, 3, 29, 6, 1};
        
        Node createHuffmanTree = createHuffmanTree(arr);
        
        preOrder(createHuffmanTree);
        
    }
     
    // 前序遍歷方法
    public static void preOrder(Node root) {
        if(root != null) {
            root.preOrder();
        } else {
            System.out.println("空樹!");
        }
    }
    
    // 建立哈夫曼樹
    public static Node createHuffmanTree(int[] arr) {
        // 1 遍歷arr陣列
        // 2 將arr的每個元素構成一個Node
        // 3 將Node放入ArrayList
        ArrayList<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.toString());
            
            // 取出根節點權值最小的兩個二叉樹
            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);
            // parent加入List
            nodes.add(parent);
            
//            Collections.sort(nodes);
//            System.out.println(nodes.toString());
        }
        // 返回root
        return nodes.get(0);
    }
}

// 建立節點
class Node implements Comparable<Node>{
    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();
        }
    }
    
    public Node(int value) {
        this.value = value;
    }
    
    @Override
    public String toString() {
        return "Node [value= " + value + "]";
    }

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

 

 

赫夫曼編碼
1 基本介紹
1) 赫夫曼編碼也翻譯為
哈夫曼編碼(Huffman Coding),又稱霍夫曼編碼,是一種編碼方式, 屬於一種程式演算法
2) 赫夫曼編碼是赫哈夫曼樹在電訊通訊中的經典的應用之一。
3) 赫夫曼編碼廣泛地用於資料檔案壓縮。其壓縮率通常在 20%~90%之間
4) 赫夫曼碼是可變字長編碼(VLC)的一種。Huffman 於 1952 年提出一種編碼方法,稱之為最佳編碼
 
2 原理剖析
 通訊領域中資訊的處理方式 1-定長編碼

通訊領域中資訊的處理方式 2-變長編碼

 

通訊領域中資訊的處理方式 3-赫夫曼編碼 
步驟如下:
 
傳輸的 字串
1) i like like like java do you like a java
2) d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5
:9 // 各個字元對應的個數
3) 按照上面字元出現的次數構建一顆赫夫曼樹, 次數作為權值
步驟:
構成赫夫曼樹的步驟:
1) 從小到大進行排序, 將每一個資料,每個資料都是一個節點 , 每個節點可以看成是一顆最簡單的二叉樹
2) 取出根節點權值最小的兩顆二叉樹
3) 組成一顆新的二叉樹, 該新的二叉樹的根節點的權值是前面兩顆二叉樹根節點權值的和
4) 再將這顆新的二叉樹,以根節點的權值大小 再次排序, 不斷重複 1-2-3-4 的步驟,直到數列中,所有的資料都被處理,
就得到一顆赫夫曼樹
4) 根據赫夫曼樹,給各個字元,規定編碼 (字首編碼), 向左的路徑為 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
5) 按照上面的赫夫曼編碼,我們的"i like like like java do you like a java"
字串對應的編碼為 (注
意這裡我們使用的無失真壓縮)
10101001101111011110100110111101111010011011110111101000011000011100110011110000110
01111000100100100110111101111011100100001100001110 通過赫夫曼編碼處理 長度為 133
6) 長度為 : 133 
說明:
原來長度是 359 , 壓縮了 (359-133) / 359 = 62.9%
此編碼滿足字首編碼, 即字元的編碼都不能是其他字元編碼的字首。不會造成匹配的多義性
赫夫曼編碼是無損處理方案 
注意事項
注意, 這個赫夫曼樹根據排序方法不同,也可能不太一樣,這樣對應的赫夫曼編碼也不完全一樣,但是 wpl 是
一樣的,都是最小的, 最後生成的赫夫曼編碼的長度是一樣,比如: 如果我們讓每次生成的新的二叉樹總是排在權
值相同的二叉樹的最後一個,則生成的二叉樹為: 

 

3 最佳實踐-資料壓縮(建立赫夫曼樹)
將給出的一段文字,比如 "i like like like java do you like a java" , 根據前面的講的赫夫曼編碼原理,對其進行數
據壓縮處理,形式如:
"1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100
110111101111011100100001100001110
"
步驟 :根據赫夫曼編碼壓縮資料的原理,需要建立 "i like like like java do you like a java" 對應的赫夫曼樹
思路:前面已經分析過了,而且我們已然講過了構建赫夫曼樹的具體實現。
程式碼實現:
package com.lin.HuffmanCode_0314;

import java.util.ArrayList;
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 content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();
        System.out.println(contentBytes.length); // 40
        
        List<Node> nodes = getNodes(contentBytes);
        System.out.println(nodes);
        
        // 建立哈夫曼樹
        System.out.println("哈夫曼樹");
        Node createHuffmanTree = createHuffmanTree(nodes);
        
        preOrder(createHuffmanTree);
        
    }
    
    /**
     * 
     * @Description:生成赫夫曼樹對應的赫夫曼編碼<br>
     *                 思路:將赫夫曼編碼存放在Map< 
     * @author LinZM  
     * @date 2021-3-14 21:09:30 
     * @version V1.8
     */
    
    // 前序遍歷
    private static void preOrder(Node root){
        if(root != null) {
            root.preOrder();
        } else {
            System.out.println("空樹!");
        }
    }
    /**
     * 
     * @Description: 
     * @author LinZM  
     * @date 2021-3-14 20:45:23 
     * @version V1.8
     * @param bytes接收位元組陣列
     * @param 
     */
    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集合
        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);
            
            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);
    }
    
    
}

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

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

    @Override
    public int compareTo(Node o) {
        // TODO Auto-generated method stub
        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();
        }
    }
}

 

 

僅供參考,有錯誤還請指出!

有什麼想法,評論區留言,互相指教指教。

覺得不錯的可以點一下右邊的推薦喲

相關文章