在咱們開始介紹赫夫曼樹以前,咱們先帶入一個情景。你想發送一個文件給你朋友,可是文件太大,因此你決定將文件壓縮,變小再發送。你有沒有考慮文件是怎麼壓縮呢?做爲程序員,沒有考慮過這裏使用的什麼算法呢?赫夫曼編碼就是其中的一種解決方法。java
在介紹赫夫曼編碼以前,咱們先介紹先導知識——赫夫曼樹node
赫夫曼樹,又稱最優樹,是一類帶權路徑長度最短的樹。程序員
定義: 假設有n個權值{$W_1,W_2...W_n$},試構造一個有n個葉子結點的二叉樹,每一個葉子結點帶權爲w,則其中帶權路徑長度WPL最小的二叉樹稱作最優二叉樹或赫夫曼樹
光看着定義,大部分人,確定雲裏霧裏的。接下來咱們就好好解釋下,什麼叫作赫夫曼樹。
首先咱們瞭解兩個基本概念:算法
畫圖描述,更加直觀
如今有3棵二叉樹,都有4個結點,分別帶權13, 7, 8, 3
第二棵樹的WPL= 59,最小,故,爲赫夫曼樹。數組
舉例:給你一個數列 {13, 7, 8, 3, 29, 6, 1},要求轉成一顆赫夫曼樹.數據結構
{13, 7, 8, 3, 29, 6, 1}app
構成赫夫曼樹的步驟ide
接下來咱們就開始寫程序學習
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(); } } }
以上便是構成赫夫曼樹的程序。測試
接下來咱們就開始介紹赫夫曼編碼
按照二進制來傳遞信息,總的長度是 359 (包括空格)
字符的編碼都不能是其餘字符編碼的前綴,符合此要求的編碼叫作前綴編碼, 即不能匹配到重複的編碼
按照上面的赫夫曼編碼,咱們的"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
具體代碼實現
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(); } } }
赫夫曼編碼壓縮文件注意事項
此文是看過韓順平老師的《數據結構與算法》(java版)以後在寫的,一是爲了之後複習,二是爲了方便你們。若是有人須要韓順平老師的課件和代碼能夠私信我
以上全部程序都在IDEA中運行過,沒有任何問題。謝謝你們!共勉! PS:剛開始寫博客,不少內容本想寫的詳細一些,奈何自己實力和寫做能力欠缺沒法完成。若是寫的不對的地方,但願你們能夠提出寶貴的意見。謝謝!