哈夫曼編碼(Huffman coding)的那些事,(編碼技術介紹和程序實現)
前言
哈夫曼編碼(Huffman coding)是一種可變長的前綴碼。哈夫曼編碼使用的算法是David A. Huffman還是在MIT的學生時提出的,並且在1952年發表了名為《A Method for the Construction of Minimum-Redundancy Codes》的文章。編碼這種編碼的過程叫做哈夫曼編碼,它是一種普遍的熵編碼技術,包括用於無損數據壓縮領域。由於哈夫曼編碼的運用廣泛,本文將簡要介紹:
哈夫曼編碼的編碼(不包含解碼)原理
代碼(java)實現過程
一、哈弗曼編碼原理
哈夫曼編碼使用一種特別的方法為信號源中的每個符號設定二進制碼。出現頻率更大的符號將獲得更短的比特,出現頻率更小的符號將被分配更長的比特,以此來提高數據壓縮率,提高傳輸效率。具體編碼步驟主要為,
1、統計:
在開始編碼時,通常都需要對信號源,也就是本文的一段文字,進行處理,計算出每個符號出現的頻率,得到信號源的基本情況。接下來就是對統計信息進行處理了
2、構造優先對列:
把得到的符號添加到優先隊列中,此優先隊列的進出邏輯是頻率低的先出,因此在設計優先隊列時需要如此設計,如果不熟悉優先隊列,請閱讀相關書籍,在此不做過多概述。得到包含所有字符的優先隊列後,就是處理優先隊列中的數據了。
3、構造哈夫曼樹:
哈夫曼樹是帶權值得二叉樹,我們使用的哈夫曼樹的權值自然就是符號的頻率了,我們構建哈夫曼樹是自底向上的,先構建葉子節點,然後逐步向上,最終完成整顆樹。先把隊列中的一個符號出列,也就是最小頻率的符號,,然後再出列一個符號。這兩個符號將作為哈夫曼樹的節點,而且這兩個節點將作為新節點,也就是它們父節點,的左右孩子節點。新節點的頻率,即權值,為孩子節點的和。把這個新節點添加到隊列中(隊列會重新根據權值排序)。重復上面的步驟,兩個符號出列,構造新的父節點,入列……直到隊列最後只剩下一個節點,這個節點也就是哈夫曼樹的根節點了。
4、為哈弗曼樹編碼:
哈夫曼樹的來自信號源的符號都是葉子節點,需要知道下。樹的根節點分配比特0,左子樹分配0,右字數分配1。然後就可以得到符號的碼值了。
二、示例(轉自這的)
假如我有A,B,C,D,E五個字符,出現的頻率(即權值)分別為5,4,3,2,1,那麽我們第一步先取兩個最小權值作為左右子樹構造一個新樹,即取1,2構成新樹,其結點為1+2=3,如圖:
虛線為新生成的結點,第二步再把新生成的權值為3的結點放到剩下的集合中,所以集合變成{5,4,3,3},再根據第二步,取最小的兩個權值構成新樹,如圖:
再依次建立哈夫曼樹,如下圖:
其中各個權值替換對應的字符即為下圖:
所以各字符對應的編碼為:A->11,B->10,C->00,D->011,E->010
如下圖也可以加深大家的理解(圖片來自於wikipedia)
下面的這個圖片是互動示例的截圖,來自http://www.hightechdreams.com/weaver.php?topic=huffmancoding,輸入符號就會動態展示出樹的構建,有興趣的朋友可以去看看
三、 代碼實現
先是設計一個用於節點比較的接口
1 package com.huffman; 2 3 //用來實現節點比較的接口 4 public interface Compare<T> { 5 //小於 6 boolean less(T t); 7 }
然後寫一個哈夫曼樹節點的類,主要是用於儲存符號信息的,實現上面的接口,get、set方法已經省略了
1 package com.huffman; 2 3 public class Node implements Compare<Node>{ 4 5 //節點的優先級 6 private int nice; 7 8 //字符出現的頻率(次數) 9 private int count; 10 11 //文本中出現的字符串 12 private String str; 13 14 //左孩子 15 private Node leftNode; 16 17 //右孩子 18 private Node rightNode; 19 20 //對應的二進制編碼 21 private String code; 22 23 public Node(){ 24 } 25 26 public Node(int nice, String str, int count){ 27 this.nice = nice; 28 this.str = str; 29 this.count = count; 30 } 31 32 //把節點(權值,頻率)相加,返回新的節點 33 public Node add(Node node){ 34 Node n = new Node(); 35 n.nice = this.nice + node.nice; 36 n.count = this.count + node.count; 37 return n; 38 } 39 40 public boolean less(Node node) { 41 return this.nice < node.nice; 42 } 43 44 45 public String toString(){ 46 return String.valueOf(this.nice); 47 }
設計一個優先隊列
1 package com.huffman; 2 3 import java.util.List; 4 5 public class PriorityQueue<T extends Compare<T>> { 6 7 public List<T> list = null; 8 9 public PriorityQueue(List<T> list){ 10 this.list = list; 11 } 12 13 public boolean empty(){ 14 return list.size() == 0; 15 } 16 17 public int size(){ 18 return list.size(); 19 } 20 21 //移除指定索引的元素 22 public void remove(int number){ 23 int index = list.indexOf(number); 24 if (-1 == index){ 25 System.out.println("data do not exist!"); 26 return; 27 } 28 list.remove(index); 29 //每次刪除一個元素都需要重新構建隊列 30 buildHeap(); 31 } 32 33 //彈出隊首元素,並把這個元素返回 34 public T pop(){ 35 //由於優先隊列的特殊性,第一個元素(索引為0)是不使用的 36 if (list.size() == 1){ 37 return null; 38 } 39 T first = list.get(1); 40 list.remove(1); 41 buildHeap(); 42 return first; 43 44 } 45 46 //加入一個元素到隊列中 47 public void add(T object){ 48 list.add(object); 49 buildHeap(); 50 } 51 52 //維護最小堆 53 private List<T> minHeap(List<T> list, int position, int heapSize){ 54 int left = 2 * position; //得到左孩子的位置 55 int right = left + 1; //得到右孩子的位置 56 int min = position; //min儲存最小值的位置,暫時假定當前節點是最小節點 57 //尋找最小節點 58 if (left < heapSize && list.get(left).less(list.get(min))){ 59 min = left; 60 } 61 if (right < heapSize && list.get(right).less(list.get(min))){ 62 min = right; 63 } 64 65 if (min != position){ 66 exchange(list, min, position); //交換當前節點與最小節點的位置 67 minHeap(list, min, heapSize); //重新維護最小堆 68 } 69 return list; 70 } 71 72 //交換元素位置 73 private List<T> exchange(List<T> list, int former, int latter){ 74 T temp = list.get(former); 75 list.set(former, list.get(latter)); 76 list.set(latter, temp); 77 return list; 78 } 79 80 //構建最小堆 81 public List<T> buildHeap(){ 82 int i; 83 for (i = list.size() - 1; i > 0; i--){ 84 minHeap(list, i, list.size()); 85 } 86 return list; 87 } 88 89 }
最後是用一個類構建哈夫曼樹
1 package com.huffman; 2 3 import java.util.ArrayList; 4 import java.util.HashMap; 5 import java.util.List; 6 import java.util.Map; 7 8 public class Huffman { 9 //優先隊列 10 private PriorityQueue<Node> priorQueue; 11 12 //需要處理的文本 13 private String[] text; 14 15 //文本處理後的統計信息 16 private Map<String, Integer> statistics; 17 18 //huffman編碼最終結果 19 private Map<String, String> result; 20 21 public Huffman(String text) { 22 this.text = text.split("\\W+"); 23 init(); 24 } 25 26 private void init() { 27 statistics = new HashMap<String, Integer>(); 28 result = new HashMap<String, String>(); 29 } 30 31 //獲取字符串統計信息,得到如"abc":3,"love":12等形式map 32 private void getStatistics() { 33 int count; 34 for (String c : text) { 35 if (statistics.containsKey(c)) { 36 count = statistics.get(c); 37 count++; 38 statistics.put(c, count); 39 } else { 40 statistics.put(c, 1); 41 } 42 } 43 } 44 45 //構建huffman樹 46 private void buildTree() { 47 List<Node> list = new ArrayList<Node>(); 48 list.add(new Node(2222, "123", 2222)); //因為優先隊列的特殊性,添加這個不使用的節點 49 //把字符串信息儲存到節點中,並把節點添加到arrayList中 50 for (String key : statistics.keySet()) { 51 Node leaf = new Node(statistics.get(key), key, statistics.get(key)); 52 list.add(leaf); 53 } 54 Node tree = null; //用於儲存指向huffman樹根節點的指針 55 priorQueue = new PriorityQueue<Node>(list); //以上面節點為元素,構建優先隊列 56 priorQueue.buildHeap(); 57 Node first = null; 58 Node second = null; 59 Node newNode = null; 60 do { 61 first = priorQueue.pop(); //取出隊首的元素,作為左孩子節點 62 second = priorQueue.pop(); //取出隊首的元素,作為右孩子節點 63 newNode = first.add(second); //構建父節點 64 priorQueue.add(newNode); //把父節點添加到隊列中 65 newNode.setLeftNode(first); 66 newNode.setRightNode(second); 67 tree = newNode; //把tree指向新節點 68 } while (priorQueue.size() > 2); //由於隊列中有一個元素是不使用的,所以隊列只剩最後一個元素(實際就是隊列只有2個元素)時就該退出循環了。 69 //最後剩下一個節點是根節點,把它取出來,並拼裝樹 70 Node root = priorQueue.pop(); 71 root.setCode("0"); 72 root.setLeftNode(tree.getLeftNode()); 73 root.setRightNode(tree.getRightNode()); 74 tree = null; 75 setCodeNum(root); //遍歷樹,為每個節點編碼 76 System.out.println("----------------------------"); 77 System.out.println(result); 78 } 79 80 public void buildHuffman(){ 81 getStatistics(); //收集統計信息 82 buildTree(); 83 for (String c : statistics.keySet()) { 84 System.out.println(c + ":" + statistics.get(c)); 85 } 86 } 87 88 //編碼 89 private void setCodeNum(Node tree){ 90 if(null == tree){ 91 return; 92 } 93 Node left = tree.getLeftNode(); 94 Node right = tree.getRightNode(); 95 if (left !=null){ 96 left.setCode("0" + tree.getCode()); //左孩子的碼為0 97 if (statistics.containsKey(left.getStr())){ 98 //如果節點在統計表裏,把它添加到result中 99 result.put(left.getStr(), left.getCode()); 100 } 101 } 102 if (right != null){ 103 right.setCode("1" + tree.getCode()); //右孩子的碼為1 104 if (statistics.containsKey(right.getStr())){ 105 //如果節點在統計表裏,把它添加到result中 106 107 result.put(right.getStr(), right.getCode()); 108 } 109 } 110 setCodeNum(left); //遞歸 111 setCodeNum(right); //遞歸 112 113 } 114 115 }
註意:代碼實現的提供主要是為了概要介紹哈夫曼編碼的實現過程,部分代碼邏輯並為深思,效率也略低,請大家只做參考,並多參考其他人的代碼。
參考文章:
維基百科
http://people.cs.pitt.edu/~kirk/cs1501/animations/Huffman.html
http://www.hightechdreams.com/weaver.php?topic=huffmancoding
延伸閱讀:
http://www.hightechdreams.com/weaver.php?topic=huffmancoding
這個網站是由一群用它們的話說是為“對編程狂熱或者有興趣的人建立的”,提供了很多算法有關的互動的例子,以及一些小程序
http://www.siggraph.org/education/materials/HyperGraph/video/mpeg/mpegfaq/huffman_tutorial.html
這是一個建立哈夫曼樹的簡明教程
http://rosettacode.org/wiki/Huffman_codes
提供了多語言的哈夫曼樹實現,包括c, c++, java, c#, python, lisp等。有興趣的朋友可以參考下,看看國外的碼農是怎麽玩哈夫曼樹的。
哈夫曼編碼(Huffman coding)的那些事,(編碼技術介紹和程序實現)