本篇博文將介紹什么是哈夫曼樹,並且如何在java語言中構建一棵哈夫曼樹,怎么利用哈夫曼樹實現對文件的壓縮和解壓。首先,先來了解下什么哈夫曼樹。
一、哈夫曼樹
哈夫曼樹屬於二叉樹,即樹的結點最多擁有2個孩子結點。若該二叉樹帶權路徑長度達到最小,稱這樣的二叉樹為最優二叉樹,也稱為哈夫曼樹(Huffman Tree)。哈夫曼樹是帶權路徑長度最短的樹,權值較大的結點離根較近。
(一)樹的相關概念
1.路徑和路徑長度
在一棵樹中,從一個結點往下可以達到的孩子或孫子結點之間的通路,稱為路徑。通路中分支的數目稱為路徑長度。若規定根結點的層數為1,則從跟結點到第L層結點的路徑長度為L-1。
2.結點的權和帶權路徑長度
若將樹中結點賦給一個有着某種含義的數值,則這個數值稱為該結點的權。結點的帶權路徑長度為:從根結點到該結點之間的路徑長度與該結點的權的乘積。
3.樹的帶權路徑長度
樹的帶權路徑長度規定為所有葉子結點的帶權路徑長度之和,記為WPL。
(二)哈夫曼樹的構造原理
假設有n個權值,則構造出的哈夫曼樹有n個葉子結點。 n個權值分別設為 w1、w2、…、wn,則哈夫曼樹的構造規則為:
(1) 將w1、w2、…,wn看成是有n 棵樹的森林(每棵樹僅有一個結點);
(2) 在森林中選出兩個根結點的權值最小的樹合並,作為一棵新樹的左、右子樹,且新樹的根結點權值為其左、右子樹根結點權值之和;
(3)從森林中刪除選取的兩棵樹,並將新樹加入森林;
(4)重復(2)、(3)步,直到森林中只剩一棵樹為止,該樹即為所求得的哈夫曼樹。
(三)哈夫曼編碼
在數據通信中,需要將傳送的文字轉換成二進制的字符串,用0,1碼的不同排列來表示字符。例如,需傳送的報文為“AFTER DATA EAR ARE ART AREA”,這里用到的字符集為“A,E,R,T,F,D”,各字母出現的次數為{8,4,5,3,1,1}。現要求為這些字母設計編碼。要區別6個字母,最簡單的二進制編碼方式是等長編碼,固定采用3位二進制,可分別用000、001、010、011、100、101對“A,E,R,T,F,D”進行編碼發送,當對方接收報文時再按照三位一分進行譯碼。顯然編碼的長度取決報文中不同字符的個數。若報文中可能出現26個不同字符,則固定編碼長度為5。然而,傳送報文時總是希望總長度盡可能短。在實際應用中,各個字符的出現頻度或使用次數是不相同的,如A、B、C的使用頻率遠遠高於X、Y、Z,自然會想到設計編碼時,讓使用頻率高的用短編碼,使用頻率低的用長編碼,以優化整個報文編碼。
為使不等長編碼為前綴編碼(即要求一個字符的編碼不能是另一個字符編碼的前綴),可用字符集中的每個字符作為葉子結點生成一棵編碼二叉樹,為了獲得傳送報文的最短長度,可將每個字符的出現頻率作為字符結點的權值賦予該結點上,顯然字使用頻率越小權值越小,權值越小葉子就越靠下,於是頻率小編碼長,頻率高編碼短,這樣就保證了此樹的最小帶權路徑長度效果上就是傳送報文的最短長度。因此,求傳送報文的最短長度問題轉化為求由字符集中的所有字符作為葉子結點,由字符出現頻率作為其權值所產生的哈夫曼樹的問題。利用哈夫曼樹來設計二進制的前綴編碼,既滿足前綴編碼的條件,又保證報文編碼總長最短。
二、用Java實現哈夫曼樹結構
(一)創建樹的結點結構
首先要搞清楚,我們用哈夫曼樹實現壓縮的原理是要先統計好被壓縮文件的每個字節的次數,以這個次數為依據來構建哈夫曼樹,使得出現次數多字節對應的哈夫曼編碼要短,而出現次數少的字節對應的哈夫曼編碼要長一些,所以樹結點結構中的要保存的數據就有文件中的字節(用byte類型),字節出現的次數(用int類型),表示是左孩子還是右孩子的數據,指向左孩子和右孩子的兩個結點結構對象。同時,如果希望能夠直接比較結點中的字節出現次數,可以重寫一個比較方法。
- /**
- * 二叉樹結點元素結構
- *
- * @author Bill56
- *
- */
- public class Node implements Comparable<Node> {
- // 元素內容
- public int number;
- // 元素次數對應的字節
- public byte by;
- // 表示結點是左結點還是右結點,0表示左,1表示右
- public String type = "";
- // 指向該結點的左孩子
- public Node leftChild;
- // 指向該結點的右孩子
- public Node rightChild;
- /**
- * 構造方法,需要將結點的值傳入
- *
- * @param number
- * 結點元素的值
- */
- public Node(int number) {
- this.number = number;
- }
- /**
- * 構造方法
- *
- * @param by
- * 結點元素字節值
- * @param number
- * 結點元素的字節出現次數
- *
- */
- public Node(byte by, int number) {
- super();
- this.by = by;
- this.number = number;
- }
- @Override
- public int compareTo(Node o) {
- // TODO Auto-generated method stub
- return this.number - o.number;
- }
- }
/**
* 二叉樹結點元素結構
*
* @author Bill56
*
*/
public class Node implements Comparable<Node> {
// 元素內容
public int number;
// 元素次數對應的字節
public byte by;
// 表示結點是左結點還是右結點,0表示左,1表示右
public String type = "";
// 指向該結點的左孩子
public Node leftChild;
// 指向該結點的右孩子
public Node rightChild;
/**
* 構造方法,需要將結點的值傳入
*
* @param number
* 結點元素的值
*/
public Node(int number) {
this.number = number;
}
/**
* 構造方法
*
* @param by
* 結點元素字節值
* @param number
* 結點元素的字節出現次數
*
*/
public Node(byte by, int number) {
super();
this.by = by;
this.number = number;
}
@Override
public int compareTo(Node o) {
// TODO Auto-generated method stub
return this.number - o.number;
}
}
(二)創建樹結構類
樹結構中主要包含一些列對結點對象的操作,如,通過一個隊列生成一個map對象(用來存放字節對應的次數),通過隊列生成一棵樹,通過樹的根結點對象生成一個哈夫曼map,獲得哈夫曼編碼等。
- /**
- * 由結點元素構成的二叉樹樹結構,由結點作為樹的根節點
- *
- * @author Bill56
- *
- */
- public class Tree {
- /**
- * 根據map生成一個由Node組成的優先隊列
- *
- * @param map
- * 需要生成隊列的map對象
- * @return 優先隊列對象
- */
- public PriorityQueue<Node> map2Queue(HashMap<Byte, Integer> map) {
- // 創建隊列對象
- PriorityQueue<Node> queue = new PriorityQueue<Node>();
- if (map != null) {
- // 獲取map的key
- Set<Byte> set = map.keySet();
- for (Byte b : set) {
- // 將獲取到的key中的值連同key一起保存到node結點中
- Node node = new Node(b, map.get(b));
- // 寫入到優先隊列
- queue.add(node);
- }
- }
- return queue;
- }
- /**
- * 根據優先隊列創建一顆哈夫曼樹
- *
- * @param queue
- * 優先隊列
- * @return 哈夫曼樹的根結點
- */
- public Node queue2Tree(PriorityQueue<Node> queue) {
- // 當優先隊列元素大於1的時候,取出最小的兩個元素之和相加后再放回到優先隊列,留下的最后一個元素便是根結點
- while (queue.size() > 1) {
- // poll方法獲取並移除此隊列的頭,如果此隊列為空,則返回 null
- // 取出最小的元素
- Node n1 = queue.poll();
- // 取出第二小的元素
- Node n2 = queue.poll();
- // 將兩個元素的字節次數值相加構成新的結點
- Node newNode = new Node(n1.number + n2.number);
- // 將新結點的左孩子指向最小的,而右孩子指向第二小的
- newNode.leftChild = n1;
- newNode.rightChild = n2;
- n1.type = "0";
- n2.type = "1";
- // 將新結點再放回隊列
- queue.add(newNode);
- }
- // 優先隊列中留下的最后一個元素便是根結點,將其取出返回
- return queue.poll();
- }
- /**
- * 根據傳入的結點遍歷樹
- *
- * @param node
- * 遍歷的起始結點
- */
- public void ergodicTree(Node node) {
- if (node != null) {
- System.out.println(node.number);
- // 遞歸遍歷左孩子的次數
- ergodicTree(node.leftChild);
- // 遞歸遍歷右孩子的次數
- ergodicTree(node.rightChild);
- }
- }
- /**
- * 根據哈夫曼樹生成對應葉子結點的哈夫曼編碼
- *
- * @param root
- * 樹的根結點
- * @return 保存葉子結點的哈夫曼map
- */
- public HashMap<Byte, String> tree2HfmMap(Node root) {
- HashMap<Byte, String> hfmMap = new HashMap<>();
- getHufmanCode(root, "", hfmMap);
- return hfmMap;
- }
- /**
- * 根據輸入的結點獲得哈夫曼編碼
- *
- * @param node
- * 遍歷的起始結點
- * @param code
- * 傳入結點的編碼類型
- * @param hfmMap
- * 用來保存字節對應的哈夫曼編碼的map
- */
- private void getHufmanCode(Node node, String code, HashMap<Byte, String> hfmMap) {
- if (node != null) {
- code += node.type;
- // 當node為葉子結點的時候
- if (node.leftChild == null && node.rightChild == null) {
- hfmMap.put(node.by, code);
- }
- // 遞歸遍歷左孩子的次數
- getHufmanCode(node.leftChild, code, hfmMap);
- // 遞歸遍歷右孩子的次數
- getHufmanCode(node.rightChild, code, hfmMap);
- }
- }
- }
/**
* 由結點元素構成的二叉樹樹結構,由結點作為樹的根節點
*
* @author Bill56
*
*/
public class Tree {
/**
* 根據map生成一個由Node組成的優先隊列
*
* @param map
* 需要生成隊列的map對象
* @return 優先隊列對象
*/
public PriorityQueue<Node> map2Queue(HashMap<Byte, Integer> map) {
// 創建隊列對象
PriorityQueue<Node> queue = new PriorityQueue<Node>();
if (map != null) {
// 獲取map的key
Set<Byte> set = map.keySet();
for (Byte b : set) {
// 將獲取到的key中的值連同key一起保存到node結點中
Node node = new Node(b, map.get(b));
// 寫入到優先隊列
queue.add(node);
}
}
return queue;
}
/**
* 根據優先隊列創建一顆哈夫曼樹
*
* @param queue
* 優先隊列
* @return 哈夫曼樹的根結點
*/
public Node queue2Tree(PriorityQueue<Node> queue) {
// 當優先隊列元素大於1的時候,取出最小的兩個元素之和相加后再放回到優先隊列,留下的最后一個元素便是根結點
while (queue.size() > 1) {
// poll方法獲取並移除此隊列的頭,如果此隊列為空,則返回 null
// 取出最小的元素
Node n1 = queue.poll();
// 取出第二小的元素
Node n2 = queue.poll();
// 將兩個元素的字節次數值相加構成新的結點
Node newNode = new Node(n1.number + n2.number);
// 將新結點的左孩子指向最小的,而右孩子指向第二小的
newNode.leftChild = n1;
newNode.rightChild = n2;
n1.type = "0";
n2.type = "1";
// 將新結點再放回隊列
queue.add(newNode);
}
// 優先隊列中留下的最后一個元素便是根結點,將其取出返回
return queue.poll();
}
/**
* 根據傳入的結點遍歷樹
*
* @param node
* 遍歷的起始結點
*/
public void ergodicTree(Node node) {
if (node != null) {
System.out.println(node.number);
// 遞歸遍歷左孩子的次數
ergodicTree(node.leftChild);
// 遞歸遍歷右孩子的次數
ergodicTree(node.rightChild);
}
}
/**
* 根據哈夫曼樹生成對應葉子結點的哈夫曼編碼
*
* @param root
* 樹的根結點
* @return 保存葉子結點的哈夫曼map
*/
public HashMap<Byte, String> tree2HfmMap(Node root) {
HashMap<Byte, String> hfmMap = new HashMap<>();
getHufmanCode(root, "", hfmMap);
return hfmMap;
}
/**
* 根據輸入的結點獲得哈夫曼編碼
*
* @param node
* 遍歷的起始結點
* @param code
* 傳入結點的編碼類型
* @param hfmMap
* 用來保存字節對應的哈夫曼編碼的map
*/
private void getHufmanCode(Node node, String code, HashMap<Byte, String> hfmMap) {
if (node != null) {
code += node.type;
// 當node為葉子結點的時候
if (node.leftChild == null && node.rightChild == null) {
hfmMap.put(node.by, code);
}
// 遞歸遍歷左孩子的次數
getHufmanCode(node.leftChild, code, hfmMap);
// 遞歸遍歷右孩子的次數
getHufmanCode(node.rightChild, code, hfmMap);
}
}
}
三、創建一個模型類,用來保存被壓縮文件的相關信息,包括被壓縮文件的路徑和該文件的哈夫曼樹編碼(HashMap對象),如下FileConfig.java:
- /**
- * 用來保存壓縮時的文件路徑和對應的字節哈夫曼編碼映射
- *
- * @author Bill56
- *
- */
- public class FileConfig {
- // 文件路徑
- private String filePath;
- // 文件字節的哈夫曼編碼映射
- private HashMap<Byte, String> hfmCodeMap;
- /**
- * 構造方法
- *
- * @param filePath
- * 文件路徑
- * @param hfmCodeMap
- * 文件字節的哈夫曼編碼映射
- */
- public FileConfig(String filePath, HashMap<Byte, String> hfmCodeMap) {
- super();
- this.filePath = filePath;
- this.hfmCodeMap = hfmCodeMap;
- }
- public String getFilePath() {
- return filePath;
- }
- public void setFilePath(String filePath) {
- this.filePath = filePath;
- }
- public HashMap<Byte, String> getHfmCodeMap() {
- return hfmCodeMap;
- }
- public void setHfmCodeMap(HashMap<Byte, String> hfmCodeMap) {
- this.hfmCodeMap = hfmCodeMap;
- }
- @Override
- public String toString() {
- return "FileConfig [filePath=" + filePath + ", hfmCodeMap=" + hfmCodeMap + "]";
- }
- }
/**
* 用來保存壓縮時的文件路徑和對應的字節哈夫曼編碼映射
*
* @author Bill56
*
*/
public class FileConfig {
// 文件路徑
private String filePath;
// 文件字節的哈夫曼編碼映射
private HashMap<Byte, String> hfmCodeMap;
/**
* 構造方法
*
* @param filePath
* 文件路徑
* @param hfmCodeMap
* 文件字節的哈夫曼編碼映射
*/
public FileConfig(String filePath, HashMap<Byte, String> hfmCodeMap) {
super();
this.filePath = filePath;
this.hfmCodeMap = hfmCodeMap;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public HashMap<Byte, String> getHfmCodeMap() {
return hfmCodeMap;
}
public void setHfmCodeMap(HashMap<Byte, String> hfmCodeMap) {
this.hfmCodeMap = hfmCodeMap;
}
@Override
public String toString() {
return "FileConfig [filePath=" + filePath + ", hfmCodeMap=" + hfmCodeMap + "]";
}
}
四、實現哈夫曼編碼對文件的壓縮和解壓
完成了第二部分后,接下來便可以實現對文件實現壓縮了。首先,需要掃描被壓縮的文件,統計好每個字節對應所出現的次數,然后生成哈夫曼樹,進而得到哈夫曼編碼。最后,哈夫曼編碼代替文件中的字節。可以將本部分的代碼全部封裝到一個FileUtil.java類中。以下的每一個點都是這個類的一個靜態方法。
(一)統計被壓縮文件中的字節及其出現的次數,用HashMap對象保存。
- /**
- * 根據指定的文件統計該文件中每個字節出現的次數,保存到一個HashMap對象中
- *
- * @param f
- * 要統計的文件
- * @return 保存次數的HashMap
- */
- public static HashMap<Byte, Integer> countByte(File f) {
- // 判斷文件是否存在
- if (!f.exists()) {
- // 不存在,直接返回null
- return null;
- }
- // 執行到這表示文件存在
- HashMap<Byte, Integer> byteCountMap = new HashMap<>();
- FileInputStream fis = null;
- try {
- // 創建文件輸入流
- fis = new FileInputStream(f);
- // 保存每次讀取的字節
- byte[] buf = new byte[1024];
- int size = 0;
- // 每次讀取1024個字節
- while ((size = fis.read(buf)) != -1) {
- // 循環每次讀到的真正字節
- for (int i = 0; i < size; i++) {
- // 獲取緩沖區的字節
- byte b = buf[i];
- // 如果map中包含了這個字節,則取出對應的值,自增一次
- if (byteCountMap.containsKey(b)) {
- // 獲得原值
- int old = byteCountMap.get(b);
- // 先自增后入
- byteCountMap.put(b, ++old);
- } else {
- // map中不包含這個字節,則直接放入,且出現次數為1
- byteCountMap.put(b, 1);
- }
- }
- }
- } catch (FileNotFoundException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } finally {
- if (fis != null) {
- try {
- fis.close();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- fis = null;
- }
- }
- }
- return byteCountMap;
- }
/**
* 根據指定的文件統計該文件中每個字節出現的次數,保存到一個HashMap對象中
*
* @param f
* 要統計的文件
* @return 保存次數的HashMap
*/
public static HashMap<Byte, Integer> countByte(File f) {
// 判斷文件是否存在
if (!f.exists()) {
// 不存在,直接返回null
return null;
}
// 執行到這表示文件存在
HashMap<Byte, Integer> byteCountMap = new HashMap<>();
FileInputStream fis = null;
try {
// 創建文件輸入流
fis = new FileInputStream(f);
// 保存每次讀取的字節
byte[] buf = new byte[1024];
int size = 0;
// 每次讀取1024個字節
while ((size = fis.read(buf)) != -1) {
// 循環每次讀到的真正字節
for (int i = 0; i < size; i++) {
// 獲取緩沖區的字節
byte b = buf[i];
// 如果map中包含了這個字節,則取出對應的值,自增一次
if (byteCountMap.containsKey(b)) {
// 獲得原值
int old = byteCountMap.get(b);
// 先自增后入
byteCountMap.put(b, ++old);
} else {
// map中不包含這個字節,則直接放入,且出現次數為1
byteCountMap.put(b, 1);
}
}
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
fis = null;
}
}
}
return byteCountMap;
}
(二)實現對文件的壓縮
對文件的壓縮,應該是根據一個文件的引用和對應的哈夫曼編碼來實現,並且將字節和對應的編碼一並寫入壓縮后的文件頭數據,以便之后做解壓來讀取。在實現這個方法之前,我們需要兩個方法,就是根據01字符串轉換成對應的字節,和根據字節生成對應的01字符串的方法。
1.根據01字符串生成對應的字節
- /**
- * 將字符串轉成二進制字節的方法
- *
- * @param bString
- * 待轉換的字符串
- * @return 二進制字節
- */
- private static byte bit2byte(String bString) {
- byte result = 0;
- for (int i = bString.length() - 1, j = 0; i >= 0; i--, j++) {
- result += (Byte.parseByte(bString.charAt(i) + "") * Math.pow(2, j));
- }
- return result;
- }
/**
* 將字符串轉成二進制字節的方法
*
* @param bString
* 待轉換的字符串
* @return 二進制字節
*/
private static byte bit2byte(String bString) {
byte result = 0;
for (int i = bString.length() - 1, j = 0; i >= 0; i--, j++) {
result += (Byte.parseByte(bString.charAt(i) + "") * Math.pow(2, j));
}
return result;
}
2.根據字節生成對應的01字符串
- /**
- * 將二字節轉成二進制的01字符串
- *
- * @param b
- * 待轉換的字節
- * @return 01字符串
- */
- public static String byte2bits(byte b) {
- int z = b;
- z |= 256;
- String str = Integer.toBinaryString(z);
- int len = str.length();
- return str.substring(len - 8, len);
- }
/**
* 將二字節轉成二進制的01字符串
*
* @param b
* 待轉換的字節
* @return 01字符串
*/
public static String byte2bits(byte b) {
int z = b;
z |= 256;
String str = Integer.toBinaryString(z);
int len = str.length();
return str.substring(len - 8, len);
}
3.實現壓縮的方法
由於每8個01串生成一個字節,而被壓縮文件最后的01串長度可能不是8的倍數,即不能被8整除,會出現不足8位的情況。這個時候,我們需要為其后面補0,補足8位,同時,還需要添加一個01串,該01串對應的字節應該是補0的次數(一定小於8)。
- /**
- * 將文件中的字節右字節哈夫曼map進行轉換
- *
- * @param f
- * 待轉換的文件
- * @param byteHfmMap
- * 該文件的字節哈夫曼map
- */
- public static FileConfig file2HfmCode(File f, HashMap<Byte, String> byteHfmMap) {
- // 聲明文件輸出流
- FileInputStream fis = null;
- FileOutputStream fos = null;
- try {
- System.out.println("正在壓縮~~~");
- // 創建文件輸入流
- fis = new FileInputStream(f);
- // 獲取文件后綴前的名稱
- String name = f.getName().substring(0, f.getName().indexOf("."));
- File outF = new File(f.getParent() + "\\" + name + "-壓縮.txt");
- // 創建文件輸出流
- fos = new FileOutputStream(outF);
- DataOutputStream dos = new DataOutputStream(fos);
- // 將哈夫曼編碼讀入到文件頭部,並記錄哈夫曼編碼所占的大小
- Set<Byte> set = byteHfmMap.keySet();
- long hfmSize = 0;
- for (Byte bi : set) {
- // 先統計哈夫曼編碼總共的所占的大小
- hfmSize += 1 + 4 + byteHfmMap.get(bi).length();
- }
- // 先將長度寫入
- dos.writeLong(hfmSize);
- dos.flush();
- for (Byte bi : set) {
- // // 測試是否正確
- // System.out.println(bi + "\t" + byteHfmMap.get(bi));
- // 寫入哈夫曼編碼對應的字節
- dos.writeByte(bi);
- // 先將字符串長度寫入
- dos.writeInt(byteHfmMap.get(bi).length());
- // 寫入哈夫曼字節的編碼
- dos.writeBytes(byteHfmMap.get(bi));
- dos.flush();
- }
- // 保存一次讀取文件的緩沖數組
- byte[] buf = new byte[1024];
- int size = 0;
- // 保存哈弗嗎編碼的StringBuilder
- StringBuilder strBuilder = new StringBuilder();
- while ((size = fis.read(buf)) != -1) {
- // 循環每次讀到的實際字節
- for (int i = 0; i < size; i++) {
- // 獲取字節
- byte b = buf[i];
- // 在字節哈夫曼映射中找到該值,獲得其hfm編碼
- if (byteHfmMap.containsKey(b)) {
- String hfmCode = byteHfmMap.get(b);
- strBuilder.append(hfmCode);
- }
- }
- }
- // 將保存的文件哈夫曼編碼按8個一字節進行壓縮
- int hfmLength = strBuilder.length();
- // 獲取需要循環的次數
- int byteNumber = hfmLength / 8;
- // 不足8位的數
- int restNumber = hfmLength % 8;
- for (int i = 0; i < byteNumber; i++) {
- String str = strBuilder.substring(i * 8, (i + 1) * 8);
- byte by = bit2byte(str);
- fos.write(by);
- fos.flush();
- }
- int zeroNumber = 8 - restNumber;
- if (zeroNumber < 8) {
- String str = strBuilder.substring(hfmLength - restNumber);
- for (int i = 0; i < zeroNumber; i++) {
- // 補0操作
- str += "0";
- }
- byte by = bit2byte(str);
- fos.write(by);
- fos.flush();
- }
- // 將補0的長度也記錄下來保存到文件末尾
- String zeroLenStr = Integer.toBinaryString(zeroNumber);
- // 將01串轉成字節
- byte zeroB = bit2byte(zeroLenStr);
- fos.write(zeroB);
- fos.flush();
- System.out.println("壓縮完畢~~~");
- return new FileConfig(outF.getAbsolutePath(), byteHfmMap);
- } catch (FileNotFoundException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } finally {
- // 關閉流
- if (fis != null) {
- try {
- fis.close();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- fis = null;
- }
- }
- // 關閉流
- if (fos != null) {
- try {
- fos.close();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- fos = null;
- }
- }
- }
- return null;
- }
/**
* 將文件中的字節右字節哈夫曼map進行轉換
*
* @param f
* 待轉換的文件
* @param byteHfmMap
* 該文件的字節哈夫曼map
*/
public static FileConfig file2HfmCode(File f, HashMap<Byte, String> byteHfmMap) {
// 聲明文件輸出流
FileInputStream fis = null;
FileOutputStream fos = null;
try {
System.out.println("正在壓縮~~~");
// 創建文件輸入流
fis = new FileInputStream(f);
// 獲取文件后綴前的名稱
String name = f.getName().substring(0, f.getName().indexOf("."));
File outF = new File(f.getParent() + "\\" + name + "-壓縮.txt");
// 創建文件輸出流
fos = new FileOutputStream(outF);
DataOutputStream dos = new DataOutputStream(fos);
// 將哈夫曼編碼讀入到文件頭部,並記錄哈夫曼編碼所占的大小
Set<Byte> set = byteHfmMap.keySet();
long hfmSize = 0;
for (Byte bi : set) {
// 先統計哈夫曼編碼總共的所占的大小
hfmSize += 1 + 4 + byteHfmMap.get(bi).length();
}
// 先將長度寫入
dos.writeLong(hfmSize);
dos.flush();
for (Byte bi : set) {
// // 測試是否正確
// System.out.println(bi + "\t" + byteHfmMap.get(bi));
// 寫入哈夫曼編碼對應的字節
dos.writeByte(bi);
// 先將字符串長度寫入
dos.writeInt(byteHfmMap.get(bi).length());
// 寫入哈夫曼字節的編碼
dos.writeBytes(byteHfmMap.get(bi));
dos.flush();
}
// 保存一次讀取文件的緩沖數組
byte[] buf = new byte[1024];
int size = 0;
// 保存哈弗嗎編碼的StringBuilder
StringBuilder strBuilder = new StringBuilder();
while ((size = fis.read(buf)) != -1) {
// 循環每次讀到的實際字節
for (int i = 0; i < size; i++) {
// 獲取字節
byte b = buf[i];
// 在字節哈夫曼映射中找到該值,獲得其hfm編碼
if (byteHfmMap.containsKey(b)) {
String hfmCode = byteHfmMap.get(b);
strBuilder.append(hfmCode);
}
}
}
// 將保存的文件哈夫曼編碼按8個一字節進行壓縮
int hfmLength = strBuilder.length();
// 獲取需要循環的次數
int byteNumber = hfmLength / 8;
// 不足8位的數
int restNumber = hfmLength % 8;
for (int i = 0; i < byteNumber; i++) {
String str = strBuilder.substring(i * 8, (i + 1) * 8);
byte by = bit2byte(str);
fos.write(by);
fos.flush();
}
int zeroNumber = 8 - restNumber;
if (zeroNumber < 8) {
String str = strBuilder.substring(hfmLength - restNumber);
for (int i = 0; i < zeroNumber; i++) {
// 補0操作
str += "0";
}
byte by = bit2byte(str);
fos.write(by);
fos.flush();
}
// 將補0的長度也記錄下來保存到文件末尾
String zeroLenStr = Integer.toBinaryString(zeroNumber);
// 將01串轉成字節
byte zeroB = bit2byte(zeroLenStr);
fos.write(zeroB);
fos.flush();
System.out.println("壓縮完畢~~~");
return new FileConfig(outF.getAbsolutePath(), byteHfmMap);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
// 關閉流
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
fis = null;
}
}
// 關閉流
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
fos = null;
}
}
}
return null;
}
(三)實現對已壓縮文件的解壓
首先應該讀取已壓縮文件的頭數據,以獲取其哈夫曼編碼,然后通過哈夫曼編碼來還原該文件。與壓縮一樣,在解壓的時候,需要獲取全部字節對應的01串,並將其保存到一個字符串對象中(最后8位除外),同時檢測在壓縮時候補0的個數(通過最后8位來獲取),然后在字符串對象中舍棄多添加的0的個數。
- /**
- * 將已經壓縮的文件進行解壓,把哈夫曼編碼重新轉成對應的字節文件
- *
- * @param f
- * 待解壓的文件
- * @param byteHfmMap
- * 保存字節的哈夫曼映射
- */
- public static void hfmCode2File(File f) {
- // 聲明文件輸出流
- FileInputStream fis = null;
- FileOutputStream fos = null;
- try {
- System.out.println("正在解壓~~~");
- // 創建文件輸入流
- fis = new FileInputStream(f);
- // 獲取文件后綴前的名稱
- String name = f.getName().substring(0, f.getName().indexOf("."));
- // 創建文件輸出流
- fos = new FileOutputStream(f.getParent() + "\\" + name + "-解壓.txt");
- DataInputStream dis = new DataInputStream(fis);
- long hfmSize = dis.readLong();
- // // 測試讀取到的大小是否正確
- // System.out.println(hfmSize);
- // 用來保存從文件讀到的哈夫曼編碼map
- HashMap<Byte, String> byteHfmMap = new HashMap<>();
- for (int i = 0; i < hfmSize;) {
- byte b = dis.readByte();
- int codeLength = dis.readInt();
- byte[] bys = new byte[codeLength];
- dis.read(bys);
- String code = new String(bys);
- byteHfmMap.put(b, code);
- i += 1 + 4 + codeLength;
- // // 測試讀取是否正確
- // System.out.println(b + "\t" + code + "\t" + i);
- }
- // 保存一次讀取文件的緩沖數組
- byte[] buf = new byte[1024];
- int size = 0;
- // 保存哈弗嗎編碼的StringBuilder
- StringBuilder strBuilder = new StringBuilder();
- // fis.skip(hfmSize);
- while ((size = fis.read(buf)) != -1) {
- // 循環每次讀到的實際字節
- for (int i = 0; i < size; i++) {
- // 獲取字節
- byte b = buf[i];
- // 將其轉成二進制01字符串
- String strBin = byte2bits(b);
- // System.out.printf("字節為:%d,對應的01串為:%s\n",b,strBin);
- strBuilder.append(strBin);
- }
- }
- String strTotalCode = strBuilder.toString();
- // 獲取字符串總長度
- int strLength = strTotalCode.length();
- // 截取出最后八個之外的
- String strFact1 = strTotalCode.substring(0, strLength - 8);
- // 獲取最后八個,並且轉成對應的字節
- String lastEight = strTotalCode.substring(strLength - 8);
- // 得到補0的位數
- byte zeroNumber = bit2byte(lastEight);
- // 將得到的fact1減去最后的0的位數
- String strFact2 = strFact1.substring(0, strFact1.length() - zeroNumber);
- // 循環字節哈夫曼映射中的每一個哈夫曼值,然后在所有01串種進行匹配
- Set<Byte> byteSet = byteHfmMap.keySet();
- int index = 0;
- // 從第0位開始
- String chs = strFact2.charAt(0) + "";
- while (index < strFact2.length()) {
- // 計數器,用來判斷是否匹配到了
- int count = 0;
- for (Byte bi : byteSet) {
- // 如果匹配到了,則跳出循環
- if (chs.equals(byteHfmMap.get(bi))) {
- fos.write(bi);
- fos.flush();
- break;
- }
- // 沒有匹配到則計數器累加一次
- count++;
- }
- // 如果計數器值大於或魚等map,說明沒有匹配到
- if (count >= byteSet.size()) {
- index++;
- chs += strFact2.charAt(index);
- } else {
- // 匹配到了,則匹配下一個字符串
- if (++index < strFact2.length()) {
- chs = strFact2.charAt(index) + "";
- }
- }
- }
- System.out.println("解壓完畢~~~");
- // for (Byte hfmByte : byteSet) {
- // String strHfmCode = byteHfmMap.get(hfmByte);
- // strFact2 = strFact2.replaceAll(strHfmCode,
- // String.valueOf(hfmByte));
- // }
- // fos.write(strFact2.getBytes());
- // fos.flush();
- } catch (FileNotFoundException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } finally {
- // 關閉流
- if (fis != null) {
- try {
- fis.close();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- fis = null;
- }
- }
- // 關閉流
- if (fos != null) {
- try {
- fos.close();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- fos = null;
- }
- }
- }
- }
/**
* 將已經壓縮的文件進行解壓,把哈夫曼編碼重新轉成對應的字節文件
*
* @param f
* 待解壓的文件
* @param byteHfmMap
* 保存字節的哈夫曼映射
*/
public static void hfmCode2File(File f) {
// 聲明文件輸出流
FileInputStream fis = null;
FileOutputStream fos = null;
try {
System.out.println("正在解壓~~~");
// 創建文件輸入流
fis = new FileInputStream(f);
// 獲取文件后綴前的名稱
String name = f.getName().substring(0, f.getName().indexOf("."));
// 創建文件輸出流
fos = new FileOutputStream(f.getParent() + "\\" + name + "-解壓.txt");
DataInputStream dis = new DataInputStream(fis);
long hfmSize = dis.readLong();
// // 測試讀取到的大小是否正確
// System.out.println(hfmSize);
// 用來保存從文件讀到的哈夫曼編碼map
HashMap<Byte, String> byteHfmMap = new HashMap<>();
for (int i = 0; i < hfmSize;) {
byte b = dis.readByte();
int codeLength = dis.readInt();
byte[] bys = new byte[codeLength];
dis.read(bys);
String code = new String(bys);
byteHfmMap.put(b, code);
i += 1 + 4 + codeLength;
// // 測試讀取是否正確
// System.out.println(b + "\t" + code + "\t" + i);
}
// 保存一次讀取文件的緩沖數組
byte[] buf = new byte[1024];
int size = 0;
// 保存哈弗嗎編碼的StringBuilder
StringBuilder strBuilder = new StringBuilder();
// fis.skip(hfmSize);
while ((size = fis.read(buf)) != -1) {
// 循環每次讀到的實際字節
for (int i = 0; i < size; i++) {
// 獲取字節
byte b = buf[i];
// 將其轉成二進制01字符串
String strBin = byte2bits(b);
// System.out.printf("字節為:%d,對應的01串為:%s\n",b,strBin);
strBuilder.append(strBin);
}
}
String strTotalCode = strBuilder.toString();
// 獲取字符串總長度
int strLength = strTotalCode.length();
// 截取出最后八個之外的
String strFact1 = strTotalCode.substring(0, strLength - 8);
// 獲取最后八個,並且轉成對應的字節
String lastEight = strTotalCode.substring(strLength - 8);
// 得到補0的位數
byte zeroNumber = bit2byte(lastEight);
// 將得到的fact1減去最后的0的位數
String strFact2 = strFact1.substring(0, strFact1.length() - zeroNumber);
// 循環字節哈夫曼映射中的每一個哈夫曼值,然后在所有01串種進行匹配
Set<Byte> byteSet = byteHfmMap.keySet();
int index = 0;
// 從第0位開始
String chs = strFact2.charAt(0) + "";
while (index < strFact2.length()) {
// 計數器,用來判斷是否匹配到了
int count = 0;
for (Byte bi : byteSet) {
// 如果匹配到了,則跳出循環
if (chs.equals(byteHfmMap.get(bi))) {
fos.write(bi);
fos.flush();
break;
}
// 沒有匹配到則計數器累加一次
count++;
}
// 如果計數器值大於或魚等map,說明沒有匹配到
if (count >= byteSet.size()) {
index++;
chs += strFact2.charAt(index);
} else {
// 匹配到了,則匹配下一個字符串
if (++index < strFact2.length()) {
chs = strFact2.charAt(index) + "";
}
}
}
System.out.println("解壓完畢~~~");
// for (Byte hfmByte : byteSet) {
// String strHfmCode = byteHfmMap.get(hfmByte);
// strFact2 = strFact2.replaceAll(strHfmCode,
// String.valueOf(hfmByte));
// }
// fos.write(strFact2.getBytes());
// fos.flush();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
// 關閉流
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
fis = null;
}
}
// 關閉流
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
fos = null;
}
}
}
}
(四)將第(一)中得到的字節和次數map對象生成哈夫曼樹編碼,實現壓縮,並且保存到FileConfig對象中
- public static FileConfig yasuo(File f) {
- HashMap<Byte, Integer> map = FileUtil.countByte(f);
- Tree tree = new Tree();
- // 構建優先隊列
- PriorityQueue<Node> queue = tree.map2Queue(map);
- // 構建樹
- Node root = tree.queue2Tree(queue);
- // 獲得字節的哈夫曼編碼map
- // tree.ergodicTree(root);
- HashMap<Byte, String> hfmMap = tree.tree2HfmMap(root);
- // Set<Byte> set = hfmMap.keySet();
- // for (Byte b : set) {
- // System.out.printf("字節為:%d,哈夫曼編碼為:%s\n", b, hfmMap.get(b));
- // }
- FileConfig fc = FileUtil.file2HfmCode(f, hfmMap);
- return fc;
- }
public static FileConfig yasuo(File f) {
HashMap<Byte, Integer> map = FileUtil.countByte(f);
Tree tree = new Tree();
// 構建優先隊列
PriorityQueue<Node> queue = tree.map2Queue(map);
// 構建樹
Node root = tree.queue2Tree(queue);
// 獲得字節的哈夫曼編碼map
// tree.ergodicTree(root);
HashMap<Byte, String> hfmMap = tree.tree2HfmMap(root);
// Set<Byte> set = hfmMap.keySet();
// for (Byte b : set) {
// System.out.printf("字節為:%d,哈夫曼編碼為:%s\n", b, hfmMap.get(b));
// }
FileConfig fc = FileUtil.file2HfmCode(f, hfmMap);
return fc;
}
(五)實現解壓的具體算法
- public static void jieya(String filePath) {
- File f = new File(filePath);
- FileUtil.hfmCode2File(f);
- }
public static void jieya(String filePath) {
File f = new File(filePath);
FileUtil.hfmCode2File(f);
}
五、創建一個測試類,用來壓縮一個文件,同時對被壓縮的文件再次解壓,查看耗時
- /**
- * 測試一些算法的類
- *
- * @author Bill56
- *
- */
- public class Test {
- public static void main(String[] args) {
- File f = new File("C:\\Users\\Bill56\\Desktop\\file.txt");
- long startTime = System.currentTimeMillis();
- FileConfig fc = ExeUtilFile.yasuo(f);
- ExeUtilFile.jieya(fc.getFilePath());
- long endTime = System.currentTimeMillis();
- System.out.println("壓縮和解壓共花費時間為:" + (endTime - startTime) + "ms");
- }
- }
/**
* 測試一些算法的類
*
* @author Bill56
*
*/
public class Test {
public static void main(String[] args) {
File f = new File("C:\\Users\\Bill56\\Desktop\\file.txt");
long startTime = System.currentTimeMillis();
FileConfig fc = ExeUtilFile.yasuo(f);
ExeUtilFile.jieya(fc.getFilePath());
long endTime = System.currentTimeMillis();
System.out.println("壓縮和解壓共花費時間為:" + (endTime - startTime) + "ms");
}
}
運行結果:


