Java數據結構與算法 day09 樹結構實際應用(二)

第10章 樹結構的實際應用

本章源碼:https://github.com/name365/Java-Data-structure

赫夫曼編碼

霍夫曼編碼基本介紹

基本介紹:

1.赫夫曼編碼也翻譯爲 哈夫曼編碼(Huffman Coding),又稱霍夫曼編碼,是一種編碼方式, 屬於一種程序算法
2.赫夫曼編碼是赫哈夫曼樹在電訊通信中的經典的應用之一。
3.赫夫曼編碼廣泛地用於數據文件壓縮。其壓縮率通常在20%90%之間
4.赫夫曼碼是可變字長編碼(VLC)的一種。Huffman於1952年提出一種編碼方法,稱之爲最佳編碼

赫夫曼編碼的原理圖解

  • 變長編碼的舉例說明
  1. 通信領域中信息的處理方式1-定長編碼
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/ 
  1. 通信領域中信息的處理方式2-變長編碼
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...  
字符的編碼都不能是其他字符編碼的前綴,符合此要求的編碼叫做前綴編碼, 即不能匹配到重複的編碼(這個在赫夫曼編碼中,還要進行舉例說明,看不懂,不捉急)
  • 赫夫曼編碼的原理圖解

要傳輸的字符串

  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"   字符串對應的編碼爲 (注意這裏我們使用的無損壓縮)
1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110 
通過赫夫曼編碼處理  長度爲  133

6) 長度爲 : 133 
說明:
原來長度是  359 , 壓縮了  (359-133) / 359 = 62.9%
此編碼滿足前綴編碼, 即字符的編碼都不能是其他字符編碼的前綴。不會造成匹配的多義性
赫夫曼編碼是無損處理方案
  • 注意事項

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

在這裏插入圖片描述

數據壓縮-創建赫夫曼樹思路及實現

將給出的一段文本,比如 "i like like like java do you like a java" , 根據前面敘述的赫夫曼編碼原理,對其進行數據壓縮處理 ,形式如 "1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
" 

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

  • 具體思路:

    • 創建Node { data (存放數據), weight (權值), left 和 right }
    • 得到 “i like like like java do you like a java” 對應的 byte[] 數組
    • 編寫一個方法,將準備構建赫夫曼樹的Node 節點放到 List , 形式 [Node[date=97 ,weight = 5], Node[date=32,weight = 9]…], 體現 d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 :9
    • 可以通過List 創建對應的赫夫曼樹
  • 代碼實現如下:

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 str = "i like like like java do you like a java";
		byte[] bytes = str.getBytes();
		System.out.println("字符串長度:" + bytes.length);	//40
		
		List<Node> nodes = getNodes(bytes);
		System.out.println("nodes = " + nodes);
		
		//創建的二叉樹
		System.out.println("赫夫曼樹");
		Node hfTree = createHFTree(nodes);
		System.out.println("前序遍歷赫夫曼樹:");
		hfTree.preOrder();
	}
	
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午4:56:13
	  * @param bytes 接受字符數組
	  * @return 返回的就是List形式  [Node[date=97 ,weight = 5], Node[date=32,weight = 9]......]
	 */
	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集合
		//遍歷map
		for(Map.Entry<Byte, Integer> entry : counts.entrySet()){
			nodes.add(new Node(entry.getKey(),entry.getValue()));
		}
		
		return nodes;
	}
	
	//通過List 創建對應的赫夫曼樹
	private static Node createHFTree(List<Node> nodes){
		while(nodes.size() > 1){
			//排序:從小到大
			Collections.sort(nodes);
			//取出第一顆最小的二叉樹
			Node leftNode = nodes.get(0);
			//取出第二顆最小的二叉樹
			Node rightNode = nodes.get(1);
			//創建一顆新的二叉樹,它的根節點沒有data,只有權值
			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);
	}

}
//創建Node,存放數據和權值
class Node implements Comparable<Node>{
	Byte date;	//存放數據(字符)本身,比如‘a’ =》 97 ; ' ' =》 32
	int weight;	//權值,表示字符
	Node left;	//指向左子樹
	Node right;	//指向右子樹
	
	public Node(Byte date, int weight) {
		super();
		this.date = date;
		this.weight = weight;
	}

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

	@Override
	public String toString() {
		return "Node [date=" + date + ", weight=" + weight + "]";
	}
	
	//前序遍歷方法
	public void preOrder(){
		System.out.println(this);
		if(this.left != null){
			this.left.preOrder();
		}
		if(this.right != null){
			this.right.preOrder();
		}
	}
}

數據壓縮-生成赫夫曼編碼表

我們已經生成了赫夫曼樹, 下面將繼續完成任務:
1.生成赫夫曼樹對應的赫夫曼編碼, 如下表:
=01 a=100 d=11000 u=11001 e=1110 v=11011 i=101 y=11010 j=0010 k=1111 l=000 o=0011

具體代碼如下(相關思路看註釋!!!):

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 str = "i like like like java do you like a java";
		byte[] bytes = str.getBytes();
		System.out.println("字符串長度:" + bytes.length);	//40
		
		List<Node> nodes = getNodes(bytes);
		System.out.println("nodes = " + nodes);
		
		//創建的二叉樹
		System.out.println("赫夫曼樹");
		Node hfTree = createHFTree(nodes);
		System.out.println("前序遍歷赫夫曼樹:");
		hfTree.preOrder();
		
		//驗證是否生成對應的赫夫曼編碼
		Map<Byte, String> hfCodes = getCodes(hfTree);
		System.out.println("生成的赫夫曼編碼表:" + hfCodes);
	}
	
	//生成赫夫曼樹對應的赫夫曼編碼
	//思路:
	//1.將赫夫曼編碼表存放在 Map<Byte,String> 形式:
	//  生成的赫夫曼編碼表{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
	static Map<Byte, String> huffCodes = new HashMap<Byte,String>();
	//2.在生成赫夫曼編碼表示,需要去拼接路徑,定義一個StringBuilder 存儲某個葉子結點的路徑
	static StringBuilder stringBuilder = new StringBuilder();
	
	/**
	  * 
	  * @Description 功能:將傳入的node結點的所有葉子結點的赫夫曼編碼得到,並放入到huffCodes集合
	  * @author subei
	  * @date 2020年6月9日下午5:28:28
	  * @param nodes 傳入結點
	  * @param code 路徑:左子結點是 0,右子結點 1
	  * @param stringBuilder 用於拼接路徑
	 */
	private static void getCodes(Node nodes,String code,StringBuilder stringBuilder){
		StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
		
		//將code 加入到 stringBuilder2
		stringBuilder2.append(code);
		
		if(nodes != null){	//如果nodes == null不處理
			//判斷當前nodes是葉子結點,還是非葉子結點
			if(nodes.date == null) { //非葉子結點
				//遞歸處理,即先向左遞歸處理
				getCodes(nodes.left,"0",stringBuilder2);
				//再向右遞歸
				getCodes(nodes.right,"1",stringBuilder2);
			}else{	//否則,說明是一個葉子結點,就表示找到某個葉子結點的最後
				huffCodes.put(nodes.date, stringBuilder2.toString());
			}
		}
	}
	
	//爲了調用方便,進行重載 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 huffCodes;
	}
	
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午4:56:13
	  * @param bytes 接受字符數組
	  * @return 返回的就是List形式  [Node[date=97 ,weight = 5], Node[date=32,weight = 9]......]
	 */
	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集合
		//遍歷map
		for(Map.Entry<Byte, Integer> entry : counts.entrySet()){
			nodes.add(new Node(entry.getKey(),entry.getValue()));
		}
		
		return nodes;
	}
	
	//通過List 創建對應的赫夫曼樹
	private static Node createHFTree(List<Node> nodes){
		while(nodes.size() > 1){
			//排序:從小到大
			Collections.sort(nodes);
			//取出第一顆最小的二叉樹
			Node leftNode = nodes.get(0);
			//取出第二顆最小的二叉樹
			Node rightNode = nodes.get(1);
			//創建一顆新的二叉樹,它的根節點沒有data,只有權值
			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);
	}

}
//創建Node,存放數據和權值
class Node implements Comparable<Node>{
	Byte date;	//存放數據(字符)本身,比如‘a’ =》 97 ; ' ' =》 32
	int weight;	//權值,表示字符
	Node left;	//指向左子樹
	Node right;	//指向右子樹
	
	public Node(Byte date, int weight) {
		super();
		this.date = date;
		this.weight = weight;
	}

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

	@Override
	public String toString() {
		return "Node [date=" + date + ", weight=" + weight + "]";
	}
	
	//前序遍歷方法
	public void preOrder(){
		System.out.println(this);
		if(this.left != null){
			this.left.preOrder();
		}
		if(this.right != null){
			this.right.preOrder();
		}
	}
}

數據壓縮-赫夫曼編碼字節數組

2.使用赫夫曼編碼來生成赫夫曼編碼數據 ,即按照上面的赫夫曼編碼,將"i like like like java do you like a java"   字符串生成對應的編碼數據, 形式如下.
   1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
  • 此處運用到原碼反碼補碼相關知識點,對此知識有所遺忘的讀者,可以先了解一下相關知識,再進行閱讀!!!當然,筆者也對此處知識也做了總結,可以參考:《Java基礎》一文中的《進制與進制間的轉換》

具體代碼實現:

import java.util.ArrayList;
import java.util.Arrays;
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 str = "i like like like java do you like a java";
		byte[] bytes = str.getBytes();
		System.out.println("字符串長度:" + bytes.length);	//40
		
		//如何將 數據進行解壓(解碼)  
		//分步過程
		List<Node> nodes = getNodes(bytes);
		System.out.println("nodes = " + nodes);
		
		//創建的二叉樹
		System.out.println("赫夫曼樹");
		Node hfTree = createHFTree(nodes);
		System.out.println("前序遍歷赫夫曼樹:");
		hfTree.preOrder();
		
		//驗證是否生成對應的赫夫曼編碼
		Map<Byte, String> hfCodes = getCodes(hfTree);
		System.out.println("生成的赫夫曼編碼表:" + hfCodes);
		
		//測試
		byte[] hfCodeBytes = zip(bytes, hfCodes);
		System.out.println("hfCodeBytes=" + Arrays.toString(hfCodeBytes));//17
	}
	
	//編寫一個方法,將字符串對應的byte[]數組,通過生成的赫夫曼編碼表,返回一個赫夫曼編碼壓縮後的byte[]
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午5:55:21
	  * @param bytes 原始的字符串對應的byte[]
	  * @param hfCodes 生成的赫夫曼編碼map
	  * @return 返回赫夫曼編碼處理後的 byte[] 
	  * 
	  * 舉例: String content = "i like like like java do you like a java"; 
	  * 	 =》 byte[] contentBytes = content.getBytes();
	  * 
	  * 返回的是字符串 "1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
	  * 	 => 對應的 byte[] hfCodeBytes ,即 8位數字對應一個 byte,放入到 hfCodeBytes
	  * 
	  * hfCodeBytes[0] =  10101000(補碼) 
	  * 	 => byte  [推導  10101000=> 10101000 - 1 => 10100111(反碼)=> 11011000= -88 ]
	  * hfCodeBytes[1] = -88
	 */
	private static byte[] zip(byte[] bytes,Map<Byte,String> hfCodes){
		
		//1.利用 hfCodes 將  bytes 轉成赫夫曼編碼對應的字符串
		StringBuilder stringBuilder = new StringBuilder();
		//遍歷bytes數組
		for(byte b : bytes){
			stringBuilder.append(hfCodes.get(b));
		}
		
		//將 "1010100010111111110..." 轉成 byte[]
		//統計返回  byte[] hfCodeBytes 長度
		//即 int len = (stringBuilder.length() + 7) / 8;
		int len;
		if(stringBuilder.length() % 8 == 0){	
			len = stringBuilder.length() / 8;
		}else{
			len = stringBuilder.length() / 8 + 1;
		}
		
		//創建存儲壓縮後的 byte數組
		byte[] hfCodeBytes = new byte[len];
		int index = 0;//記錄是第幾個byte
		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,放入到 hfCodeBytes
			hfCodeBytes[index] = (byte)Integer.parseInt(strByte, 2);
			index++;
		}
		
		return hfCodeBytes;
	}
	
	//生成赫夫曼樹對應的赫夫曼編碼
	//思路:
	//1.將赫夫曼編碼表存放在 Map<Byte,String> 形式:
	//  生成的赫夫曼編碼表{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
	static Map<Byte, String> huffCodes = new HashMap<Byte,String>();
	//2.在生成赫夫曼編碼表示,需要去拼接路徑,定義一個StringBuilder 存儲某個葉子結點的路徑
	static StringBuilder stringBuilder = new StringBuilder();
	
	/**
	  * 
	  * @Description 功能:將傳入的node結點的所有葉子結點的赫夫曼編碼得到,並放入到huffCodes集合
	  * @author subei
	  * @date 2020年6月9日下午5:28:28
	  * @param nodes 傳入結點
	  * @param code 路徑:左子結點是 0,右子結點 1
	  * @param stringBuilder 用於拼接路徑
	 */
	private static void getCodes(Node nodes,String code,StringBuilder stringBuilder){
		StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
		
		//將code 加入到 stringBuilder2
		stringBuilder2.append(code);
		
		if(nodes != null){	//如果nodes == null不處理
			//判斷當前nodes是葉子結點,還是非葉子結點
			if(nodes.date == null) { //非葉子結點
				//遞歸處理,即先向左遞歸處理
				getCodes(nodes.left,"0",stringBuilder2);
				//再向右遞歸
				getCodes(nodes.right,"1",stringBuilder2);
			}else{	//否則,說明是一個葉子結點,就表示找到某個葉子結點的最後
				huffCodes.put(nodes.date, stringBuilder2.toString());
			}
		}
	}
	
	//爲了調用方便,進行重載 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 huffCodes;
	}
	
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午4:56:13
	  * @param bytes 接受字符數組
	  * @return 返回的就是List形式  [Node[date=97 ,weight = 5], Node[date=32,weight = 9]......]
	 */
	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集合
		//遍歷map
		for(Map.Entry<Byte, Integer> entry : counts.entrySet()){
			nodes.add(new Node(entry.getKey(),entry.getValue()));
		}
		
		return nodes;
	}
	
	//通過List 創建對應的赫夫曼樹
	private static Node createHFTree(List<Node> nodes){
		while(nodes.size() > 1){
			//排序:從小到大
			Collections.sort(nodes);
			//取出第一顆最小的二叉樹
			Node leftNode = nodes.get(0);
			//取出第二顆最小的二叉樹
			Node rightNode = nodes.get(1);
			//創建一顆新的二叉樹,它的根節點沒有data,只有權值
			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);
	}

}
//創建Node,存放數據和權值
class Node implements Comparable<Node>{
	Byte date;	//存放數據(字符)本身,比如‘a’ =》 97 ; ' ' =》 32
	int weight;	//權值,表示字符
	Node left;	//指向左子樹
	Node right;	//指向右子樹
	
	public Node(Byte date, int weight) {
		super();
		this.date = date;
		this.weight = weight;
	}

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

	@Override
	public String toString() {
		return "Node [date=" + date + ", weight=" + weight + "]";
	}
	
	//前序遍歷方法
	public void preOrder(){
		System.out.println(this);
		if(this.left != null){
			this.left.preOrder();
		}
		if(this.right != null){
			this.right.preOrder();
		}
	}
}

數據壓縮-赫夫曼字節數組封裝

使用赫夫曼編碼來解碼數據,具體要求是
1.前面我們得到了赫夫曼編碼和對應的編碼
byte[] ,:[-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"

即對上述的方法進行封裝

具體代碼如下:

import java.util.ArrayList;
import java.util.Arrays;
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 str = "i like like like java do you like a java";
		byte[] bytes = str.getBytes();
		System.out.println("字符串長度:" + bytes.length);	//40
		
		byte[] hfCoBytes = hfZip(bytes);
		System.out.println("壓縮後的結果=" + Arrays.toString(hfCoBytes) + "長度是:" + hfCoBytes.length);
	}
	
	//使用一個方法,將前面的方法封裝起來,便於我們的調用.
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午6:50:14
	  * @param bytes 原始的字符串對應的字節數組
	  * @return 經過赫夫曼編碼處理後的字節數組(即壓縮後的數組)
	 */
	private static byte[] hfZip(byte[] bytes){
		List<Node> nodes = getNodes(bytes);
		//根據 nodes 創建的赫夫曼樹
		Node hfTree = createHFTree(nodes);
		//對應的赫夫曼編碼(根據 赫夫曼樹)
		Map<Byte, String> hfCodes = getCodes(hfTree);
		//根據生成的赫夫曼編碼,壓縮得到壓縮後的赫夫曼編碼字節數組
		byte[] hfCodeBytes = zip(bytes, hfCodes);
		return hfCodeBytes;
	}
	
	//編寫一個方法,將字符串對應的byte[]數組,通過生成的赫夫曼編碼表,返回一個赫夫曼編碼壓縮後的byte[]
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午5:55:21
	  * @param bytes 原始的字符串對應的byte[]
	  * @param hfCodes 生成的赫夫曼編碼map
	  * @return 返回赫夫曼編碼處理後的 byte[] 
	  * 
	  * 舉例: String content = "i like like like java do you like a java"; 
	  * 	 =》 byte[] contentBytes = content.getBytes();
	  * 
	  * 返回的是字符串 "1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
	  * 	 => 對應的 byte[] hfCodeBytes ,即 8位數字對應一個 byte,放入到 hfCodeBytes
	  * 
	  * hfCodeBytes[0] =  10101000(補碼) 
	  * 	 => byte  [推導  10101000=> 10101000 - 1 => 10100111(反碼)=> 11011000= -88 ]
	  * hfCodeBytes[1] = -88
	 */
	private static byte[] zip(byte[] bytes,Map<Byte,String> hfCodes){
		
		//1.利用 hfCodes 將  bytes 轉成赫夫曼編碼對應的字符串
		StringBuilder stringBuilder = new StringBuilder();
		//遍歷bytes數組
		for(byte b : bytes){
			stringBuilder.append(hfCodes.get(b));
		}
		
		//將 "1010100010111111110..." 轉成 byte[]
		//統計返回  byte[] hfCodeBytes 長度
		//即 int len = (stringBuilder.length() + 7) / 8;
		int len;
		if(stringBuilder.length() % 8 == 0){	
			len = stringBuilder.length() / 8;
		}else{
			len = stringBuilder.length() / 8 + 1;
		}
		
		//創建存儲壓縮後的 byte數組
		byte[] hfCodeBytes = new byte[len];
		int index = 0;//記錄是第幾個byte
		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,放入到 hfCodeBytes
			hfCodeBytes[index] = (byte)Integer.parseInt(strByte, 2);
			index++;
		}
		
		return hfCodeBytes;
	}
	
	//生成赫夫曼樹對應的赫夫曼編碼
	//思路:
	//1.將赫夫曼編碼表存放在 Map<Byte,String> 形式:
	//  生成的赫夫曼編碼表{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
	static Map<Byte, String> huffCodes = new HashMap<Byte,String>();
	//2.在生成赫夫曼編碼表示,需要去拼接路徑,定義一個StringBuilder 存儲某個葉子結點的路徑
	static StringBuilder stringBuilder = new StringBuilder();
	
	/**
	  * 
	  * @Description 功能:將傳入的node結點的所有葉子結點的赫夫曼編碼得到,並放入到huffCodes集合
	  * @author subei
	  * @date 2020年6月9日下午5:28:28
	  * @param nodes 傳入結點
	  * @param code 路徑:左子結點是 0,右子結點 1
	  * @param stringBuilder 用於拼接路徑
	 */
	private static void getCodes(Node nodes,String code,StringBuilder stringBuilder){
		StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
		
		//將code 加入到 stringBuilder2
		stringBuilder2.append(code);
		
		if(nodes != null){	//如果nodes == null不處理
			//判斷當前nodes是葉子結點,還是非葉子結點
			if(nodes.date == null) { //非葉子結點
				//遞歸處理,即先向左遞歸處理
				getCodes(nodes.left,"0",stringBuilder2);
				//再向右遞歸
				getCodes(nodes.right,"1",stringBuilder2);
			}else{	//否則,說明是一個葉子結點,就表示找到某個葉子結點的最後
				huffCodes.put(nodes.date, stringBuilder2.toString());
			}
		}
	}
	
	//爲了調用方便,進行重載 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 huffCodes;
	}
	
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午4:56:13
	  * @param bytes 接受字符數組
	  * @return 返回的就是List形式  [Node[date=97 ,weight = 5], Node[date=32,weight = 9]......]
	 */
	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集合
		//遍歷map
		for(Map.Entry<Byte, Integer> entry : counts.entrySet()){
			nodes.add(new Node(entry.getKey(),entry.getValue()));
		}
		
		return nodes;
	}
	
	//通過List 創建對應的赫夫曼樹
	private static Node createHFTree(List<Node> nodes){
		while(nodes.size() > 1){
			//排序:從小到大
			Collections.sort(nodes);
			//取出第一顆最小的二叉樹
			Node leftNode = nodes.get(0);
			//取出第二顆最小的二叉樹
			Node rightNode = nodes.get(1);
			//創建一顆新的二叉樹,它的根節點沒有data,只有權值
			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);
	}

}
//創建Node,存放數據和權值
class Node implements Comparable<Node>{
	Byte date;	//存放數據(字符)本身,比如‘a’ =》 97 ; ' ' =》 32
	int weight;	//權值,表示字符
	Node left;	//指向左子樹
	Node right;	//指向右子樹
	
	public Node(Byte date, int weight) {
		super();
		this.date = date;
		this.weight = weight;
	}

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

	@Override
	public String toString() {
		return "Node [date=" + date + ", weight=" + weight + "]";
	}
	
	//前序遍歷方法
	public void preOrder(){
		System.out.println(this);
		if(this.left != null){
			this.left.preOrder();
		}
		if(this.right != null){
			this.right.preOrder();
		}
	}
}

至此,赫夫曼編碼的字節壓縮方法過程的敘述就結束了,它的解壓方法敘述如下:

數據解壓-字節轉二進制字符串

使用赫夫曼編碼來解碼數據,具體要求是
1.前面我們得到了赫夫曼編碼和對應的編碼
   byte[] ,:[-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.將hfCoBytes[-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"

具體代碼實現:

import java.util.ArrayList;
import java.util.Arrays;
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 str = "i like like like java do you like a java";
		byte[] bytes = str.getBytes();
		System.out.println("字符串長度:" + bytes.length);	//40
		
		byte[] hfCoBytes = hfZip(bytes);
		System.out.println("壓縮後的結果=" + Arrays.toString(hfCoBytes) + "長度是:" + hfCoBytes.length);
		
		//測試byToBitString方法
		System.out.println(byToBitString(false, (byte)-1));
	}
	
	//完成數據的解壓
	//思路
	//1.將hfCoBytes[-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"
	
	/**
	  * 將一個byte 轉成一個二進制的字符串, 如果看不懂,可以參考我的筆記:Java基礎二進制的原碼,反碼,補碼
	  * @Description 
	  * @author subei
	  * @date 2020年6月10日上午8:31:31
	  * @param b 傳入的 byte
	  * @param flag 標誌是否需要補高位如果是true ,表示需要補高位,如果是false表示不補, 如果是最後一個字節,無需補高位
	  * @return 是該b 對應的二進制的字符串(注意是按補碼返回)
	 */
	private static String byToBitString(boolean flag,byte b){
		//使用變量保存
		int temp = b;	//將b轉成int
		
		//如果是正數我們還存在補高位
		if(flag){
			temp |= 256;	//按位與256 1 0000 0000 | 0000 0001 ==》 1 0000 0001
		}
		String str = Integer.toBinaryString(temp);	//返回temp對應的二進制補碼
		if(flag){
			return str.substring(str.length() - 8);				
		}else{
			return str;
		}
			
	}
	
	//使用一個方法,將前面的方法封裝起來,便於我們的調用.
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午6:50:14
	  * @param bytes 原始的字符串對應的字節數組
	  * @return 經過赫夫曼編碼處理後的字節數組(即壓縮後的數組)
	 */
	private static byte[] hfZip(byte[] bytes){
		List<Node> nodes = getNodes(bytes);
		//根據 nodes 創建的赫夫曼樹
		Node hfTree = createHFTree(nodes);
		//對應的赫夫曼編碼(根據 赫夫曼樹)
		Map<Byte, String> hfCodes = getCodes(hfTree);
		//根據生成的赫夫曼編碼,壓縮得到壓縮後的赫夫曼編碼字節數組
		byte[] hfCodeBytes = zip(bytes, hfCodes);
		return hfCodeBytes;
	}
	
	//編寫一個方法,將字符串對應的byte[]數組,通過生成的赫夫曼編碼表,返回一個赫夫曼編碼壓縮後的byte[]
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午5:55:21
	  * @param bytes 原始的字符串對應的byte[]
	  * @param hfCodes 生成的赫夫曼編碼map
	  * @return 返回赫夫曼編碼處理後的 byte[] 
	  * 
	  * 舉例: String content = "i like like like java do you like a java"; 
	  * 	 =》 byte[] contentBytes = content.getBytes();
	  * 
	  * 返回的是字符串 "1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
	  * 	 => 對應的 byte[] hfCodeBytes ,即 8位數字對應一個 byte,放入到 hfCodeBytes
	  * 
	  * hfCodeBytes[0] =  10101000(補碼) 
	  * 	 => byte  [推導  10101000=> 10101000 - 1 => 10100111(反碼)=> 11011000= -88 ]
	  * hfCodeBytes[1] = -88
	 */
	private static byte[] zip(byte[] bytes,Map<Byte,String> hfCodes){
		
		//1.利用 hfCodes 將  bytes 轉成赫夫曼編碼對應的字符串
		StringBuilder stringBuilder = new StringBuilder();
		//遍歷bytes數組
		for(byte b : bytes){
			stringBuilder.append(hfCodes.get(b));
		}
		
		//將 "1010100010111111110..." 轉成 byte[]
		//統計返回  byte[] hfCodeBytes 長度
		//即 int len = (stringBuilder.length() + 7) / 8;
		int len;
		if(stringBuilder.length() % 8 == 0){	
			len = stringBuilder.length() / 8;
		}else{
			len = stringBuilder.length() / 8 + 1;
		}
		
		//創建存儲壓縮後的 byte數組
		byte[] hfCodeBytes = new byte[len];
		int index = 0;//記錄是第幾個byte
		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,放入到 hfCodeBytes
			hfCodeBytes[index] = (byte)Integer.parseInt(strByte, 2);
			index++;
		}
		
		return hfCodeBytes;
	}
	
	//生成赫夫曼樹對應的赫夫曼編碼
	//思路:
	//1.將赫夫曼編碼表存放在 Map<Byte,String> 形式:
	//  生成的赫夫曼編碼表{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
	static Map<Byte, String> huffCodes = new HashMap<Byte,String>();
	//2.在生成赫夫曼編碼表示,需要去拼接路徑,定義一個StringBuilder 存儲某個葉子結點的路徑
	static StringBuilder stringBuilder = new StringBuilder();
	
	/**
	  * 
	  * @Description 功能:將傳入的node結點的所有葉子結點的赫夫曼編碼得到,並放入到huffCodes集合
	  * @author subei
	  * @date 2020年6月9日下午5:28:28
	  * @param nodes 傳入結點
	  * @param code 路徑:左子結點是 0,右子結點 1
	  * @param stringBuilder 用於拼接路徑
	 */
	private static void getCodes(Node nodes,String code,StringBuilder stringBuilder){
		StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
		
		//將code 加入到 stringBuilder2
		stringBuilder2.append(code);
		
		if(nodes != null){	//如果nodes == null不處理
			//判斷當前nodes是葉子結點,還是非葉子結點
			if(nodes.date == null) { //非葉子結點
				//遞歸處理,即先向左遞歸處理
				getCodes(nodes.left,"0",stringBuilder2);
				//再向右遞歸
				getCodes(nodes.right,"1",stringBuilder2);
			}else{	//否則,說明是一個葉子結點,就表示找到某個葉子結點的最後
				huffCodes.put(nodes.date, stringBuilder2.toString());
			}
		}
	}
	
	//爲了調用方便,進行重載 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 huffCodes;
	}
	
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午4:56:13
	  * @param bytes 接受字符數組
	  * @return 返回的就是List形式  [Node[date=97 ,weight = 5], Node[date=32,weight = 9]......]
	 */
	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集合
		//遍歷map
		for(Map.Entry<Byte, Integer> entry : counts.entrySet()){
			nodes.add(new Node(entry.getKey(),entry.getValue()));
		}
		
		return nodes;
	}
	
	//通過List 創建對應的赫夫曼樹
	private static Node createHFTree(List<Node> nodes){
		while(nodes.size() > 1){
			//排序:從小到大
			Collections.sort(nodes);
			//取出第一顆最小的二叉樹
			Node leftNode = nodes.get(0);
			//取出第二顆最小的二叉樹
			Node rightNode = nodes.get(1);
			//創建一顆新的二叉樹,它的根節點沒有data,只有權值
			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);
	}

}
//創建Node,存放數據和權值
class Node implements Comparable<Node>{
	Byte date;	//存放數據(字符)本身,比如‘a’ =》 97 ; ' ' =》 32
	int weight;	//權值,表示字符
	Node left;	//指向左子樹
	Node right;	//指向右子樹
	
	public Node(Byte date, int weight) {
		super();
		this.date = date;
		this.weight = weight;
	}

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

	@Override
	public String toString() {
		return "Node [date=" + date + ", 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.util.ArrayList;
import java.util.Arrays;
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 str = "i like like like java do you like a java";
		byte[] bytes = str.getBytes();
		System.out.println("字符串長度:" + bytes.length);	//40
		
		byte[] hfCoBytes = hfZip(bytes);
		System.out.println("壓縮後的結果=" + Arrays.toString(hfCoBytes) + "長度是:" + hfCoBytes.length);
		
		//測試byToBitString方法
//		System.out.println(byToBitString(false, (byte)-1));
		byte[] decode = decode(huffCodes, hfCoBytes);
		System.out.println("原來的字符串=" + new String(decode));
		
	}
	
	//編寫一個方法,完成對壓縮數據的解碼
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月10日上午8:54:55
	  * @param hfCodes 赫夫曼編碼表 map
	  * @param hfBytes 赫夫曼編碼得到的字節數組
	  * @return 就是原來的字符串對應的數組
	 */
	private static byte[] decode(Map<Byte,String> hfCodes, byte[] hfBytes) {
		//1.先得到 hfCoBytes對應的 二進制的字符串 , 形式 1010100010111...
		StringBuilder builder = new StringBuilder();
		
		//將byte數組轉成二進制的字符串
		for(int i = 0; i < hfBytes.length; i++) {
			byte b = hfBytes[i];
			//判斷是不是最後一個字節
			boolean flag = (i == hfBytes.length - 1);
			builder.append(byToBitString(!flag, b));
		}
//		System.out.println("二進制字符串="+builder.toString());
		
		//把字符串安裝指定的赫夫曼編碼進行解碼
		//把赫夫曼編碼表進行調換,因爲反向查詢 a->100 100->a
		Map<String, Byte>  map = new HashMap<String,Byte>();
		for(Map.Entry<Byte, String> entry: hfCodes.entrySet()) {
			map.put(entry.getValue(), entry.getKey());
		}
		
		//創建要給集合,存放byte
		List<Byte> list = new ArrayList<>();
		//i 可以理解成就是索引,掃描 builder 
		for(int  i = 0; i < builder.length(); ) {
			int count = 1; 	//小的計數器
			boolean flag = true;
			Byte b = null;
			
			while(flag) {
				//1010100010111... 遞增的取出 key 1 
				String key = builder.substring(i, i+count);	//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;
//		return null;
	}
	
	//完成數據的解壓
	//思路
	//1.將hfCoBytes[-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"
	
	/**
	  * 將一個byte 轉成一個二進制的字符串, 如果看不懂,可以參考我的筆記:Java基礎二進制的原碼,反碼,補碼
	  * @Description 
	  * @author subei
	  * @date 2020年6月10日上午8:31:31
	  * @param b 傳入的 byte
	  * @param flag 標誌是否需要補高位如果是true ,表示需要補高位,如果是false表示不補, 如果是最後一個字節,無需補高位
	  * @return 是該b 對應的二進制的字符串(注意是按補碼返回)
	 */
	private static String byToBitString(boolean flag,byte b){
		//使用變量保存
		int temp = b;	//將b轉成int
		
		//如果是正數我們還存在補高位
		if(flag){
			temp |= 256;	//按位與256 1 0000 0000 | 0000 0001 ==》 1 0000 0001
		}
		String str = Integer.toBinaryString(temp);	//返回temp對應的二進制補碼
		if(flag){
			return str.substring(str.length() - 8);				
		}else{
			return str;
		}
			
	}
	
	//使用一個方法,將前面的方法封裝起來,便於我們的調用.
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午6:50:14
	  * @param bytes 原始的字符串對應的字節數組
	  * @return 經過赫夫曼編碼處理後的字節數組(即壓縮後的數組)
	 */
	private static byte[] hfZip(byte[] bytes){
		List<Node> nodes = getNodes(bytes);
		//根據 nodes 創建的赫夫曼樹
		Node hfTree = createHFTree(nodes);
		//對應的赫夫曼編碼(根據 赫夫曼樹)
		Map<Byte, String> hfCodes = getCodes(hfTree);
		//根據生成的赫夫曼編碼,壓縮得到壓縮後的赫夫曼編碼字節數組
		byte[] hfCodeBytes = zip(bytes, hfCodes);
		return hfCodeBytes;
	}
	
	//編寫一個方法,將字符串對應的byte[]數組,通過生成的赫夫曼編碼表,返回一個赫夫曼編碼壓縮後的byte[]
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午5:55:21
	  * @param bytes 原始的字符串對應的byte[]
	  * @param hfCodes 生成的赫夫曼編碼map
	  * @return 返回赫夫曼編碼處理後的 byte[] 
	  * 
	  * 舉例: String content = "i like like like java do you like a java"; 
	  * 	 =》 byte[] contentBytes = content.getBytes();
	  * 
	  * 返回的是字符串 "1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
	  * 	 => 對應的 byte[] hfCodeBytes ,即 8位數字對應一個 byte,放入到 hfCodeBytes
	  * 
	  * hfCodeBytes[0] =  10101000(補碼) 
	  * 	 => byte  [推導  10101000=> 10101000 - 1 => 10100111(反碼)=> 11011000= -88 ]
	  * hfCodeBytes[1] = -88
	 */
	private static byte[] zip(byte[] bytes,Map<Byte,String> hfCodes){
		
		//1.利用 hfCodes 將  bytes 轉成赫夫曼編碼對應的字符串
		StringBuilder stringBuilder = new StringBuilder();
		//遍歷bytes數組
		for(byte b : bytes){
			stringBuilder.append(hfCodes.get(b));
		}
		
//		System.out.println("測試 stringBuilder=" + stringBuilder.toString());
		
		//將 "1010100010111111110..." 轉成 byte[]
		//統計返回  byte[] hfCodeBytes 長度
		//即 int len = (stringBuilder.length() + 7) / 8;
		int len;
		if(stringBuilder.length() % 8 == 0){	
			len = stringBuilder.length() / 8;
		}else{
			len = stringBuilder.length() / 8 + 1;
		}
		
		//創建存儲壓縮後的 byte數組
		byte[] hfCodeBytes = new byte[len];
		int index = 0;//記錄是第幾個byte
		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,放入到 hfCodeBytes
			hfCodeBytes[index] = (byte)Integer.parseInt(strByte, 2);
			index++;
		}
		
		return hfCodeBytes;
	}
	
	//生成赫夫曼樹對應的赫夫曼編碼
	//思路:
	//1.將赫夫曼編碼表存放在 Map<Byte,String> 形式:
	//  生成的赫夫曼編碼表{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
	static Map<Byte, String> huffCodes = new HashMap<Byte,String>();
	//2.在生成赫夫曼編碼表示,需要去拼接路徑,定義一個StringBuilder 存儲某個葉子結點的路徑
	static StringBuilder stringBuilder = new StringBuilder();
	
	/**
	  * 
	  * @Description 功能:將傳入的node結點的所有葉子結點的赫夫曼編碼得到,並放入到huffCodes集合
	  * @author subei
	  * @date 2020年6月9日下午5:28:28
	  * @param nodes 傳入結點
	  * @param code 路徑:左子結點是 0,右子結點 1
	  * @param stringBuilder 用於拼接路徑
	 */
	private static void getCodes(Node nodes,String code,StringBuilder stringBuilder){
		StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
		
		//將code 加入到 stringBuilder2
		stringBuilder2.append(code);
		
		if(nodes != null){	//如果nodes == null不處理
			//判斷當前nodes是葉子結點,還是非葉子結點
			if(nodes.date == null) { //非葉子結點
				//遞歸處理,即先向左遞歸處理
				getCodes(nodes.left,"0",stringBuilder2);
				//再向右遞歸
				getCodes(nodes.right,"1",stringBuilder2);
			}else{	//否則,說明是一個葉子結點,就表示找到某個葉子結點的最後
				huffCodes.put(nodes.date, stringBuilder2.toString());
			}
		}
	}
	
	//爲了調用方便,進行重載 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 huffCodes;
	}
	
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午4:56:13
	  * @param bytes 接受字符數組
	  * @return 返回的就是List形式  [Node[date=97 ,weight = 5], Node[date=32,weight = 9]......]
	 */
	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集合
		//遍歷map
		for(Map.Entry<Byte, Integer> entry : counts.entrySet()){
			nodes.add(new Node(entry.getKey(),entry.getValue()));
		}
		
		return nodes;
	}
	
	//通過List 創建對應的赫夫曼樹
	private static Node createHFTree(List<Node> nodes){
		while(nodes.size() > 1){
			//排序:從小到大
			Collections.sort(nodes);
			//取出第一顆最小的二叉樹
			Node leftNode = nodes.get(0);
			//取出第二顆最小的二叉樹
			Node rightNode = nodes.get(1);
			//創建一顆新的二叉樹,它的根節點沒有data,只有權值
			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);
	}

}
//創建Node,存放數據和權值
class Node implements Comparable<Node>{
	Byte date;	//存放數據(字符)本身,比如‘a’ =》 97 ; ' ' =》 32
	int weight;	//權值,表示字符
	Node left;	//指向左子樹
	Node right;	//指向右子樹
	
	public Node(Byte date, int weight) {
		super();
		this.date = date;
		this.weight = weight;
	}

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

	@Override
	public String toString() {
		return "Node [date=" + date + ", 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.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
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 srcFile = "D://subei.xml";
		String dstFile = "D://Uninstall.zip";
		
		zipFile(srcFile, dstFile);
		System.out.println("壓縮文件ok~~");
				
	}
	
	//編寫一個方法進行文件壓縮
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月10日上午9:44:35
	  * @param srcFile 傳入需要壓縮文件的路徑
	  * @param dstFile 壓縮後文件的保存路徑
	 */
	public static void zipFile(String srcFile,String dstFile){
		//創建輸入流
		FileInputStream is = null;
		//創建輸出流
		OutputStream os = null;
		ObjectOutputStream oos = null;
		try {			
			//創建文件的輸入流
			is = new FileInputStream(srcFile);
			//創建一個和源文件大小一樣的byte[]
			byte[] b = new byte[is.available()];
			//讀取文件
			is.read(b);
			//直接對源文件壓縮
			byte[] hfBytes = hfZip(b);
			//創建文件的輸出流
			os = new FileOutputStream(dstFile);
			//創建一個和文件輸出流關聯的ObjectOutputStream
			oos = new ObjectOutputStream(os);
			//把赫夫曼編碼後的字節數組寫入壓縮文件
			oos.writeObject(hfBytes); 
			//這裏我們以對象流的方式寫入赫夫曼編碼,是爲了以後我們恢復源文件時使用
			//注意一定要把赫夫曼編碼 寫入壓縮文件
			oos.writeObject(huffCodes);
			
		} catch (Exception e) {
			System.out.println(e.getMessage());
		} finally{
			try {
				is.close();
				oos.close();
				os.close();
			}catch (Exception e) {
				System.out.println(e.getMessage());
			}
		}
	}
	
	//使用一個方法,將前面的方法封裝起來,便於我們的調用.
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午6:50:14
	  * @param bytes 原始的字符串對應的字節數組
	  * @return 經過赫夫曼編碼處理後的字節數組(即壓縮後的數組)
	 */
	private static byte[] hfZip(byte[] bytes){
		List<Node> nodes = getNodes(bytes);
		//根據 nodes 創建的赫夫曼樹
		Node hfTree = createHFTree(nodes);
		//對應的赫夫曼編碼(根據 赫夫曼樹)
		Map<Byte, String> hfCodes = getCodes(hfTree);
		//根據生成的赫夫曼編碼,壓縮得到壓縮後的赫夫曼編碼字節數組
		byte[] hfCodeBytes = zip(bytes, hfCodes);
		return hfCodeBytes;
	}
	
	//編寫一個方法,將字符串對應的byte[]數組,通過生成的赫夫曼編碼表,返回一個赫夫曼編碼壓縮後的byte[]
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午5:55:21
	  * @param bytes 原始的字符串對應的byte[]
	  * @param hfCodes 生成的赫夫曼編碼map
	  * @return 返回赫夫曼編碼處理後的 byte[] 
	  * 
	  * 舉例: String content = "i like like like java do you like a java"; 
	  * 	 =》 byte[] contentBytes = content.getBytes();
	  * 
	  * 返回的是字符串 "1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
	  * 	 => 對應的 byte[] hfCodeBytes ,即 8位數字對應一個 byte,放入到 hfCodeBytes
	  * 
	  * hfCodeBytes[0] =  10101000(補碼) 
	  * 	 => byte  [推導  10101000=> 10101000 - 1 => 10100111(反碼)=> 11011000= -88 ]
	  * hfCodeBytes[1] = -88
	 */
	private static byte[] zip(byte[] bytes,Map<Byte,String> hfCodes){
		
		//1.利用 hfCodes 將  bytes 轉成赫夫曼編碼對應的字符串
		StringBuilder stringBuilder = new StringBuilder();
		//遍歷bytes數組
		for(byte b : bytes){
			stringBuilder.append(hfCodes.get(b));
		}
		
//		System.out.println("測試 stringBuilder=" + stringBuilder.toString());
		
		//將 "1010100010111111110..." 轉成 byte[]
		//統計返回  byte[] hfCodeBytes 長度
		//即 int len = (stringBuilder.length() + 7) / 8;
		int len;
		if(stringBuilder.length() % 8 == 0){	
			len = stringBuilder.length() / 8;
		}else{
			len = stringBuilder.length() / 8 + 1;
		}
		
		//創建存儲壓縮後的 byte數組
		byte[] hfCodeBytes = new byte[len];
		int index = 0;//記錄是第幾個byte
		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,放入到 hfCodeBytes
			hfCodeBytes[index] = (byte)Integer.parseInt(strByte, 2);
			index++;
		}
		
		return hfCodeBytes;
	}
	
	//生成赫夫曼樹對應的赫夫曼編碼
	//思路:
	//1.將赫夫曼編碼表存放在 Map<Byte,String> 形式:
	//  生成的赫夫曼編碼表{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
	static Map<Byte, String> huffCodes = new HashMap<Byte,String>();
	//2.在生成赫夫曼編碼表示,需要去拼接路徑,定義一個StringBuilder 存儲某個葉子結點的路徑
	static StringBuilder stringBuilder = new StringBuilder();
	
	/**
	  * 
	  * @Description 功能:將傳入的node結點的所有葉子結點的赫夫曼編碼得到,並放入到huffCodes集合
	  * @author subei
	  * @date 2020年6月9日下午5:28:28
	  * @param nodes 傳入結點
	  * @param code 路徑:左子結點是 0,右子結點 1
	  * @param stringBuilder 用於拼接路徑
	 */
	private static void getCodes(Node nodes,String code,StringBuilder stringBuilder){
		StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
		
		//將code 加入到 stringBuilder2
		stringBuilder2.append(code);
		
		if(nodes != null){	//如果nodes == null不處理
			//判斷當前nodes是葉子結點,還是非葉子結點
			if(nodes.date == null) { //非葉子結點
				//遞歸處理,即先向左遞歸處理
				getCodes(nodes.left,"0",stringBuilder2);
				//再向右遞歸
				getCodes(nodes.right,"1",stringBuilder2);
			}else{	//否則,說明是一個葉子結點,就表示找到某個葉子結點的最後
				huffCodes.put(nodes.date, stringBuilder2.toString());
			}
		}
	}
	
	//爲了調用方便,進行重載 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 huffCodes;
	}
	
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午4:56:13
	  * @param bytes 接受字符數組
	  * @return 返回的就是List形式  [Node[date=97 ,weight = 5], Node[date=32,weight = 9]......]
	 */
	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集合
		//遍歷map
		for(Map.Entry<Byte, Integer> entry : counts.entrySet()){
			nodes.add(new Node(entry.getKey(),entry.getValue()));
		}
		
		return nodes;
	}
	
	//通過List 創建對應的赫夫曼樹
	private static Node createHFTree(List<Node> nodes){
		while(nodes.size() > 1){
			//排序:從小到大
			Collections.sort(nodes);
			//取出第一顆最小的二叉樹
			Node leftNode = nodes.get(0);
			//取出第二顆最小的二叉樹
			Node rightNode = nodes.get(1);
			//創建一顆新的二叉樹,它的根節點沒有data,只有權值
			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);
	}

}
//創建Node,存放數據和權值
class Node implements Comparable<Node>{
	Byte date;	//存放數據(字符)本身,比如‘a’ =》 97 ; ' ' =》 32
	int weight;	//權值,表示字符
	Node left;	//指向左子樹
	Node right;	//指向右子樹
	
	public Node(Byte date, int weight) {
		super();
		this.date = date;
		this.weight = weight;
	}

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

	@Override
	public String toString() {
		return "Node [date=" + date + ", 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.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
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 srcFile = "D://subei.xml";
		String dstFile = "D://Uninstall.zip";
		
		zipFile(srcFile, dstFile);
		System.out.println("壓縮文件ok~~");
		
		String zipFile = "D://Uninstall.zip";
		String dstFile2 = "D://subei2.xml";
		unZipFile(zipFile, dstFile2);
		System.out.println("解壓成功!");
	}
	
	//編寫一個方法,完成對壓縮文件的解壓
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月10日上午11:41:42
	  * @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數組  hfBytes
			byte[] hfBytes = (byte[])ois.readObject();
			//讀取赫夫曼編碼表
			@SuppressWarnings("unchecked")
			Map<Byte,String> huffCodes = (Map<Byte,String>)ois.readObject();
			
			//解碼
			byte[] bytes = decode(huffCodes, hfBytes);
			//將bytes 數組寫入到目標文件
			os = new FileOutputStream(dstFile);
			//寫數據到 dstFile 文件
			os.write(bytes);
		} catch (Exception e) {
			System.out.println(e.getMessage());
		} finally {
			try {
				os.close();
				ois.close();
				is.close();
			} catch (Exception e2) {
				System.out.println(e2.getMessage());
			}
		}
	}
	
	//編寫一個方法進行文件壓縮
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月10日上午9:44:35
	  * @param srcFile 傳入需要壓縮文件的路徑
	  * @param dstFile 壓縮後文件的保存路徑
	 */
	public static void zipFile(String srcFile,String dstFile){
		//創建輸入流
		FileInputStream is = null;
		//創建輸出流
		OutputStream os = null;
		ObjectOutputStream oos = null;
		try {			
			//創建文件的輸入流
			is = new FileInputStream(srcFile);
			//創建一個和源文件大小一樣的byte[]
			byte[] b = new byte[is.available()];
			//讀取文件
			is.read(b);
			//直接對源文件壓縮
			byte[] hfBytes = hfZip(b);
			//創建文件的輸出流
			os = new FileOutputStream(dstFile);
			//創建一個和文件輸出流關聯的ObjectOutputStream
			oos = new ObjectOutputStream(os);
			//把赫夫曼編碼後的字節數組寫入壓縮文件
			oos.writeObject(hfBytes); 
			//這裏我們以對象流的方式寫入赫夫曼編碼,是爲了以後我們恢復源文件時使用
			//注意一定要把赫夫曼編碼 寫入壓縮文件
			oos.writeObject(huffCodes);
			
		} catch (Exception e) {
			System.out.println(e.getMessage());
		} finally{
			try {
				is.close();
				oos.close();
				os.close();
			}catch (Exception e) {
				System.out.println(e.getMessage());
			}
		}
	}
	
	//編寫一個方法,完成對壓縮數據的解碼
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月10日上午8:54:55
	  * @param hfCodes 赫夫曼編碼表 map
	  * @param hfBytes 赫夫曼編碼得到的字節數組
	  * @return 就是原來的字符串對應的數組
	 */
	private static byte[] decode(Map<Byte,String> hfCodes, byte[] hfBytes) {
		//1.先得到 hfCoBytes對應的 二進制的字符串 , 形式 1010100010111...
		StringBuilder builder = new StringBuilder();
		
		//將byte數組轉成二進制的字符串
		for(int i = 0; i < hfBytes.length; i++) {
			byte b = hfBytes[i];
			//判斷是不是最後一個字節
			boolean flag = (i == hfBytes.length - 1);
			builder.append(byToBitString(!flag, b));
		}
//		System.out.println("二進制字符串="+builder.toString());
		
		//把字符串安裝指定的赫夫曼編碼進行解碼
		//把赫夫曼編碼表進行調換,因爲反向查詢 a->100 100->a
		Map<String, Byte>  map = new HashMap<String,Byte>();
		for(Map.Entry<Byte, String> entry: hfCodes.entrySet()) {
			map.put(entry.getValue(), entry.getKey());
		}
		
		//創建要給集合,存放byte
		List<Byte> list = new ArrayList<>();
		//i 可以理解成就是索引,掃描 builder 
		for(int  i = 0; i < builder.length(); ) {
			int count = 1; 	//小的計數器
			boolean flag = true;
			Byte b = null;
			
			while(flag) {
				//1010100010111... 遞增的取出 key 1 
				String key = builder.substring(i, i+count);	//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;
//		return null;
	}
	
	//完成數據的解壓
	//思路
	//1.將hfCoBytes[-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"
	
	/**
	  * 將一個byte 轉成一個二進制的字符串, 如果看不懂,可以參考我的筆記:Java基礎二進制的原碼,反碼,補碼
	  * @Description 
	  * @author subei
	  * @date 2020年6月10日上午8:31:31
	  * @param b 傳入的 byte
	  * @param flag 標誌是否需要補高位如果是true ,表示需要補高位,如果是false表示不補, 如果是最後一個字節,無需補高位
	  * @return 是該b 對應的二進制的字符串(注意是按補碼返回)
	 */
	private static String byToBitString(boolean flag,byte b){
		//使用變量保存
		int temp = b;	//將b轉成int
		
		//如果是正數我們還存在補高位
		if(flag){
			temp |= 256;	//按位與256 1 0000 0000 | 0000 0001 ==》 1 0000 0001
		}
		String str = Integer.toBinaryString(temp);	//返回temp對應的二進制補碼
		if(flag){
			return str.substring(str.length() - 8);				
		}else{
			return str;
		}
			
	}
	
	//使用一個方法,將前面的方法封裝起來,便於我們的調用.
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午6:50:14
	  * @param bytes 原始的字符串對應的字節數組
	  * @return 經過赫夫曼編碼處理後的字節數組(即壓縮後的數組)
	 */
	private static byte[] hfZip(byte[] bytes){
		List<Node> nodes = getNodes(bytes);
		//根據 nodes 創建的赫夫曼樹
		Node hfTree = createHFTree(nodes);
		//對應的赫夫曼編碼(根據 赫夫曼樹)
		Map<Byte, String> hfCodes = getCodes(hfTree);
		//根據生成的赫夫曼編碼,壓縮得到壓縮後的赫夫曼編碼字節數組
		byte[] hfCodeBytes = zip(bytes, hfCodes);
		return hfCodeBytes;
	}
	
	//編寫一個方法,將字符串對應的byte[]數組,通過生成的赫夫曼編碼表,返回一個赫夫曼編碼壓縮後的byte[]
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午5:55:21
	  * @param bytes 原始的字符串對應的byte[]
	  * @param hfCodes 生成的赫夫曼編碼map
	  * @return 返回赫夫曼編碼處理後的 byte[] 
	  * 
	  * 舉例: String content = "i like like like java do you like a java"; 
	  * 	 =》 byte[] contentBytes = content.getBytes();
	  * 
	  * 返回的是字符串 "1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100"
	  * 	 => 對應的 byte[] hfCodeBytes ,即 8位數字對應一個 byte,放入到 hfCodeBytes
	  * 
	  * hfCodeBytes[0] =  10101000(補碼) 
	  * 	 => byte  [推導  10101000=> 10101000 - 1 => 10100111(反碼)=> 11011000= -88 ]
	  * hfCodeBytes[1] = -88
	 */
	private static byte[] zip(byte[] bytes,Map<Byte,String> hfCodes){
		
		//1.利用 hfCodes 將  bytes 轉成赫夫曼編碼對應的字符串
		StringBuilder stringBuilder = new StringBuilder();
		//遍歷bytes數組
		for(byte b : bytes){
			stringBuilder.append(hfCodes.get(b));
		}
		
//		System.out.println("測試 stringBuilder=" + stringBuilder.toString());
		
		//將 "1010100010111111110..." 轉成 byte[]
		//統計返回  byte[] hfCodeBytes 長度
		//即 int len = (stringBuilder.length() + 7) / 8;
		int len;
		if(stringBuilder.length() % 8 == 0){	
			len = stringBuilder.length() / 8;
		}else{
			len = stringBuilder.length() / 8 + 1;
		}
		
		//創建存儲壓縮後的 byte數組
		byte[] hfCodeBytes = new byte[len];
		int index = 0;//記錄是第幾個byte
		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,放入到 hfCodeBytes
			hfCodeBytes[index] = (byte)Integer.parseInt(strByte, 2);
			index++;
		}
		
		return hfCodeBytes;
	}
	
	//生成赫夫曼樹對應的赫夫曼編碼
	//思路:
	//1.將赫夫曼編碼表存放在 Map<Byte,String> 形式:
	//  生成的赫夫曼編碼表{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
	static Map<Byte, String> huffCodes = new HashMap<Byte,String>();
	//2.在生成赫夫曼編碼表示,需要去拼接路徑,定義一個StringBuilder 存儲某個葉子結點的路徑
	static StringBuilder stringBuilder = new StringBuilder();
	
	/**
	  * 
	  * @Description 功能:將傳入的node結點的所有葉子結點的赫夫曼編碼得到,並放入到huffCodes集合
	  * @author subei
	  * @date 2020年6月9日下午5:28:28
	  * @param nodes 傳入結點
	  * @param code 路徑:左子結點是 0,右子結點 1
	  * @param stringBuilder 用於拼接路徑
	 */
	private static void getCodes(Node nodes,String code,StringBuilder stringBuilder){
		StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
		
		//將code 加入到 stringBuilder2
		stringBuilder2.append(code);
		
		if(nodes != null){	//如果nodes == null不處理
			//判斷當前nodes是葉子結點,還是非葉子結點
			if(nodes.date == null) { //非葉子結點
				//遞歸處理,即先向左遞歸處理
				getCodes(nodes.left,"0",stringBuilder2);
				//再向右遞歸
				getCodes(nodes.right,"1",stringBuilder2);
			}else{	//否則,說明是一個葉子結點,就表示找到某個葉子結點的最後
				huffCodes.put(nodes.date, stringBuilder2.toString());
			}
		}
	}
	
	//爲了調用方便,進行重載 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 huffCodes;
	}
	
	/**
	  * 
	  * @Description 
	  * @author subei
	  * @date 2020年6月9日下午4:56:13
	  * @param bytes 接受字符數組
	  * @return 返回的就是List形式  [Node[date=97 ,weight = 5], Node[date=32,weight = 9]......]
	 */
	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集合
		//遍歷map
		for(Map.Entry<Byte, Integer> entry : counts.entrySet()){
			nodes.add(new Node(entry.getKey(),entry.getValue()));
		}
		
		return nodes;
	}
	
	//通過List 創建對應的赫夫曼樹
	private static Node createHFTree(List<Node> nodes){
		while(nodes.size() > 1){
			//排序:從小到大
			Collections.sort(nodes);
			//取出第一顆最小的二叉樹
			Node leftNode = nodes.get(0);
			//取出第二顆最小的二叉樹
			Node rightNode = nodes.get(1);
			//創建一顆新的二叉樹,它的根節點沒有data,只有權值
			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);
	}

}
//創建Node,存放數據和權值
class Node implements Comparable<Node>{
	Byte date;	//存放數據(字符)本身,比如‘a’ =》 97 ; ' ' =》 32
	int weight;	//權值,表示字符
	Node left;	//指向左子樹
	Node right;	//指向右子樹
	
	public Node(Byte date, int weight) {
		super();
		this.date = date;
		this.weight = weight;
	}

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

	@Override
	public String toString() {
		return "Node [date=" + date + ", weight=" + weight + "]";
	}
	
	//前序遍歷方法
	public void preOrder(){
		System.out.println(this);
		if(this.left != null){
			this.left.preOrder();
		}
		if(this.right != null){
			this.right.preOrder();
		}
	}
}

在這裏插入圖片描述

赫夫曼編碼的注意事項

  • 如果文件本身就是經過壓縮處理的,那麼使用赫夫曼編碼再壓縮效率不會有明顯變化, 比如視頻、pptx等等文件 [舉例壓一個.pptx]。
  • 赫夫曼編碼是按字節來處理的,因此可以處理所有的文件(二進制文件、文本文件) [舉例壓一個.xml文件]。
  • 如果一個文件中的內容,重複的數據不多,壓縮效果也不會很明顯。

本章部分導圖

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章