Huffman编码和文件压缩与解压缩
引言
根据维基百科:在计算机科学和信息论中,数据压缩和源编码是按照特定的编码用比未经编码少的数据比特表示信息的过程。
压缩算法分为无损数据压缩和有损数据压缩
无损数据压缩主要有字典编码、局部匹配预测、熵编码、Spepian-Wolf编码
下面我们会用到熵编码中的哈夫曼编码(简单的熵编码,通常用于压缩的最后一步)来对文件进行压缩与解压缩
Huffman编码与Huffman树
Huffman Coding,是一种用于无损数据压缩的熵编码算法,由美国计算机科学家David Albert Huffman在1952年发明。
在计算机数据处理中,huffman编码使用变长编码表对源符号(如文件中的一个字母)进行编码,其中边长编码表是通过一种评估来源符号出现几率的方法得到的,出现几率高的字母使用较短编码,反之出现几率低的字母使用较长编码,这便使得编码之后字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。
简而言之,
Huffman编码:通过更少的比特对经常出现的字符编码来压缩数据
Huffman树:字符的编码是基于字符在文本中出现的次数使用二叉树来构建的,该树称为Huffman树。
在之后的程序设计中,我们需要用到二叉树、堆排序、文件I/O流的知识。
压缩原理
压缩过程也可以称之为对文件的加密过程
计算机的世界是由0和1组成的,那么我们平时接触到的字母,符号,汉字是如何在计算机存储的呢,这就要介绍到编码,ASCII码用一个字节编码,覆盖到所有字母和常见符号,也就是2^8=256个。但我们博大精深的汉字肯定不止那么256个,这就要用到两个字节编码,也就是UTF-8码,可以对我们的所有字符进行编码。
比如在一个文件中存储有abaabc;那么计算机中存储的应该是
01100001(a)
01100010(b)
01100001(a)
01100001(a)
01100010(b)
01100011(c),
一共占6个字节;如何利用哈夫曼树将这个文件进行压缩呢?构造哈夫曼树,首先的有权值,那么拿什么东西作为权值呢?当然是该字符在文件中的出现次数。
/*=====================文件压缩======================= */
/**
* 主要流程:
* 1,遍历输入流,统计字符出现次数,生成字符计数数组
* 2,根据字符计数数组,构建哈夫曼树
* 3,由哈夫曼树生成字符编码数组
* 4,将字符编码数组作为对象写入输出流
* 5,再次遍历输入流,将文本转换为编码输出到输出流
*/
import java.io.*;
import java.util.ArrayList;
public class FileCompress{
//全局变量
private static final int SIZE = 2*128; //ASCII码的数目
private static BufferedInputStream inFile; //被压缩的文件的输入流
private static ObjectOutputStream outKey; //压缩文件的输出流(写入编码表)
private static BitOutputStream outFile; //压缩文件的输出流(写入编码后的文本)
private static int[] charCounts; //字符计数数组,统计每个字符的出现次数
private static String[] charCodes; //字符编码数组,统计每个字符对应的Huffman编码
private static Tree huffmanTree; //Huffman树,用于生成每个字符的编码
private static Long originalSize,keySize;
/**主函数 */
public static void main(String[] args) {
if(args.length != 2){
System.out.println("Usage:java FileCompress source target");
System.exit(1);
}
//刚好对应五个步骤
setCharacterFrequency(args[0]);
setHuffmanTree();
setCode();
writeHuffmanCode(args[1]);
writeFileData(args[0], args[1]);
System.out.println("Done!");
System.out.println(originalSize);
}
/**1,遍历输入流,统计字符出现次数,生成字符计数数组 */
public static void setCharacterFrequency(String inputFile){
charCounts = new int[SIZE];
originalSize = (long)0;
try{
inFile = new BufferedInputStream(new FileInputStream(inputFile));
int r;
while((r = inFile.read()) != -1){
charCounts[r] ++;
originalSize ++;
}
inFile.close();
}
catch(FileNotFoundException ex){
System.out.println(inputFile + " file cannot be found in the folder");
ex.printStackTrace(System.out);
System.exit(2);
}
catch(IOException ex){
ex.printStackTrace(System.out);
}
}
/**2,根据字符计数数组,构建哈夫曼树 */
public static void setHuffmanTree(){
Heap heap = new Heap<>();
for(int i=0; i<256; i++){
if(charCounts[i]!=0){
heap.add(new Tree(charCounts[i],(char)i));
}
}
while(heap.getSize() > 1){
Tree t1 = heap.remove();
Tree t2 = heap.remove();
heap.add(new Tree(t1,t2));
}
huffmanTree = heap.remove();
}
/**内部类:Tree
* 用于构建Huffman树
* 要求实现能通过权重比较树的大小
* 实现Comparable接口
* 改写compareTo方法
*/
public static class Tree implements Comparable{
Node root;
/**构造方法 */
public Tree(){ //空树
}
public Tree(int weight, char element){ //单个节点的树
root = new Node(weight, element);
}
public Tree(Tree t1,Tree t2){ //两个子树合并成一个树
root = new Node(); //空的根节点
root.left = t1.root; //连接子树
root.right = t2.root;
root.weight = t1.root.weight + t2.root.weight; //权重相加
}
/**改写compareTo方法
* 比较树根节点的权值
*/
@Override
public int compareTo(Tree t){
if(root.weight < t.root.weight) return 1;
else if(root.weight == t.root.weight) return 0;
else return -1;
}
/**定义Tree的内部类Node
* 用于定义树的节点
*/
public static class Node{
char element; //存储叶节点的字符
int weight; //该字符的权重
Node left; //左子树
Node right; //右子树
String code = ""; //该字符的编码
public Node(){
}
public Node(int weight,char element){
this.weight = weight;
this.element = element;
}
}
}
/**
* 内部类:Heap
* 用于:将有权重的树排序
* 堆排序
*/
public static class Heap>{
/**数组线性表用于存储堆中的元素 */
private ArrayList list = new ArrayList<>();
/**构造一个空的二叉堆 */
public Heap(){
}
/**用指定的对象数组构造二叉堆 */
public Heap(E[] objects){
for(int i=0; i 0){
/**得到父节点的下标 */
indexOfParent = (indexOfCurrent -1 ) / 2;
if(list.get(indexOfCurrent).compareTo(list.get(indexOfParent)) > 0){
/**将当前节点和父节点交换 */
E temp = list.get(indexOfCurrent);
list.set(indexOfCurrent, list.get(indexOfParent));
list.set(indexOfParent, temp);
}
else{
break; //当当前节点小于等于父节点退出循环
}
/**更新当前节点的下标 */
indexOfCurrent = indexOfParent;
}
}
/**删除根节点 并返回该值 */
public E remove(){
if(list.size() == 0) return null;
/**末尾元素赋值给根节点元素之后,删除堆的末尾元素 */
E root = list.get(0);//暂存根节点的元素
list.set(0, list.get(list.size() - 1) );
list.remove(list.size() - 1);
/**根节点为当前节点 */
int indexOfCurrent = 0;
int indexOfSon;
/**重构二叉堆 当当前节点为叶节点时退出循环*/
while(indexOfCurrent < list.size()){
int indexOfLeft = indexOfCurrent*2 + 1;
int indexOfRight = indexOfCurrent*2 + 2;
/**选出左右孩子中较大的一个作为子节点 */
if(indexOfLeft == list.size() - 1){ //只有左孩子
indexOfSon = indexOfLeft;
}
else if(indexOfLeft > list.size() - 1 ){ //无孩子
break;
}
else{ //有左右孩子
if(list.get(indexOfLeft).compareTo(list.get(indexOfRight)) > 0){
indexOfSon = indexOfLeft;
}
else{
indexOfSon = indexOfRight;
}
}
if(list.get(indexOfCurrent).compareTo(list.get(indexOfSon)) < 0){
/**将当前节点和子节点交换 */
E temp = list.get(indexOfSon);
list.set(indexOfSon, list.get(indexOfCurrent));
list.set(indexOfCurrent, temp );
}
else{
break;//当前节点大于等于子节点退出循环
}
/**更新当前节点的下标 */
indexOfCurrent = indexOfSon;
}
return root;
}
/**返回堆的大小 */
public int getSize(){
return list.size();
}
}
/**3,由哈夫曼树生成字符编码数组 */
public static void setCode(){
charCodes = new String[SIZE];
if(huffmanTree.root == null)return;
assignCode(huffmanTree.root);
}
public static void assignCode(Tree.Node root){
if(root.left != null){
root.left.code = root.code + "0";
assignCode(root.left);
root.right.code = root.code + "1";
assignCode(root.right);
}
else{
charCodes[(int)root.element] = root.code;
}
}
/**4,将字符编码数组作为对象写入输出流 */
public static void writeHuffmanCode(String outputFile){
try{
outKey = new ObjectOutputStream(new FileOutputStream(outputFile));
outKey.writeObject(charCodes);
keySize = new File(outputFile).length(); //记录编码表的大小(单位:字节) long:8个字节
outKey.writeLong(keySize);
outKey.writeLong(originalSize);
outKey.close();
}
catch(IOException ex){
ex.printStackTrace(System.out);
}
}
/**5,再次遍历输入流,将文本转换为编码写入到输出流 */
public static void writeFileData(String inputFile,String outputFile){
try{
inFile = new BufferedInputStream(new FileInputStream(inputFile));
outFile = new BitOutputStream(new FileOutputStream(outputFile,true));
int r = 0;
while((r = inFile.read()) != -1){
outFile.writeBit(charCodes[r]);
}
inFile.close();
outFile.close();
}
catch(FileNotFoundException ex){
System.out.println(inputFile + " file cannot be found in the folder");
ex.printStackTrace(System.out);
System.exit(4);
}
catch(IOException ex){
ex.printStackTrace(System.out);
}
}
/**
* 内部类:
* 用于:实现将字符二进制比特流
* 并且每次一个字节写入输出流
*/
public static class BitOutputStream{
private FileOutputStream output;
private int value = 0;
private int mask = 1;
private int count = 0; //标记填充比特的个数
/**构造方法 */
public BitOutputStream(FileOutputStream outputStream)throws IOException{
output = outputStream;
}
/**存储一个字节变量形式的比特
* 每次只能写入一个字节的数据
* 所以每存满一个字节的比特,就写入
*/
public void writeBit(char bit)throws IOException{
count ++;
value = value << 1;
if(bit == '1'){
value = value | mask;
}
if(count == 8){
output.write(value);
count = 0; //在写入一个字节后 填充比特数置为0
}
}
/**调用writeBit(char bit)字符串划分成单个字符 */
public void writeBit(String bit)throws IOException{
for(int i=0; i 0){
value = value << (8 - count);
output.write(value);//写入低位一个字节
}
output.close();
}
}
}
解压缩原理
/*=====================文件解压缩======================= */
/**
* 主要流程:
* 1,读取输入流中的码表,
* 2,跳过输入流的码表,读取编码内容
* 根据码表,进行译码,将译码后的内容写入到输出流
*/
import java.io.*;
public class FileDecompress{
//全局变量
private static final int SIZE = 2*128; //ASCII码的数目
private static ObjectInputStream inKey; //压缩文件的输入流(读取编码表)
private static BufferedInputStream inFile;//压缩文件的输入流(读取编码后的文本)
private static FileOutputStream outFile;//解锁后文件的输出流
private static String[] charCodes;//字符编码数组,统计每个字符对应的Huffman编码
private static Long originalSize,keySize;
private static Long skipSize; //需要跳过的字节数,
private static String encodeText;//读取到的编码文本
private static Long readSize = (long) 0;
/**主函数 */
public static void main(String[] args) {
if(args.length != 2){
System.out.println("Usage:java FileDecompress source target");
System.exit(1);
}
readHuffmanCode(args[0]);
readFileData(args[0], args[1]);
System.out.println("Done!");
System.out.println(skipSize);
System.out.println(originalSize);
System.out.println(encodeText);
System.out.println(encodeText.length());
}
/**1,读取输入流中的码表, */
public static void readHuffmanCode(String inputFile){
charCodes = new String[SIZE];
try{
inKey = new ObjectInputStream(new FileInputStream(inputFile));
charCodes = (String[])inKey.readObject();
keySize = inKey.readLong();
originalSize = inKey.readLong();
skipSize = keySize + 2*Long.SIZE/8 + 2;
inKey.close();
}
catch(ClassNotFoundException ex) {
ex.printStackTrace(System.out);
System.exit(2);
}
catch(FileNotFoundException ex){
System.out.println(inputFile + " file cannot be found in the folder");
ex.printStackTrace(System.out);
System.exit(3);
}
catch(IOException ex){
ex.printStackTrace(System.out);
}
}
/**2,跳过输入流的码表,读取编码内容
* 根据码表,进行译码,将译码后的内容写入到输出流
*/
public static void readFileData(String inputFile,String outputFile){
encodeText = "";
try{
inFile = new BufferedInputStream(new FileInputStream(inputFile));
outFile = new FileOutputStream(outputFile);
inFile.skip(skipSize);
int r = 0;
while( (r=inFile.read()) != -1){
encodeText += getBits(r);
}
String temp = "";
for(int i=0; i=0; i--){
int temp = value >> i;
int bit = temp & mask;
bits = bits + String.valueOf(bit);
}
return bits;
}
}