自己動手寫壓縮軟件


        想吃項記的燴面了……這小地方的可難吃。

        看完了《裸婚時代》,我覺得冬瓜說得對,劉易陽不敢面對自己的真實感受。
        女生都是感性的,工作永遠不如生活重要。

        不是坐在一起就叫團隊,不是不吵架就叫態度。

        昨晚一哥們說求k小直接可以進行k次冒泡,我都想不起來,我想到的是區間快拍,說明基礎很重要。

        對原版本的算法有很大修改,個人認為原版代碼的命名不太規范,讀起來比較累,本程序主要是界面參考了參考資料,讀寫文件完全是自己搞的(原文字節流,我的字符流),不過原文不可壓縮漢字(原版壓縮效率高一些),我的可以,壓縮比40%左右(文件大小做除)。

一.概念引入

        優先級隊列(Priority Queue)又叫最小堆。Huffman( 哈夫曼 ) 算法在上世紀五十年代初提出來了,它是一種無損壓縮方法,在壓縮過程中不會丟失信息熵,而且可以證明Huffman 算法在無損壓縮算法中是最優的。Hufmann coding 是最古老,以及最優雅的數據壓縮方法之一。它是以最小冗余編碼為基礎的,即如果我們知道數據中的不同符號在數據中的出現頻率,我們就可以對它用一種占用空間最少的編碼方式進行編碼,這種方法是,對於最頻繁出現的符號制定最短長度的編碼,而對於較少出現的符號給較長長度的編碼。哈夫曼編碼可以對各種類型的數據進行壓縮,但在本文中我們僅僅針對字符進行編碼。

        哈夫曼編碼是一種前綴碼,即任一個字符的編碼都不是其他字符編碼的前綴。從我們的編碼過程中可以很容易看到這一點,因為所有字符都是哈夫曼樹中的葉子節點,這個特征能夠保證解碼的唯一性,不會產生歧義。筆者認為這樣說或許更好理解:任意字符的編碼的所有前綴都不是其它字符的編碼,而且字符和編碼是滿單射,為后面value反查key打基礎。可以看出,出現頻率最高的字符,使用最短的編碼,字符出現頻率越低,編碼逐漸增長。這樣不同字符在文檔中出現的頻率差異越大,則壓縮效果將會越好。因此字符出現頻率越大,我們希望給它的編碼越短(在哈夫曼樹中的深度越淺),即我們希望更晚的將它所在的樹進行合並。反之,字符頻率越低,我們希望給他的編碼最長(在哈夫曼樹中的深度越深),因此我們希望越早的將它所在的樹進行合並。因此,哈夫曼編碼的貪心策略就體現在合並樹的過程中,我們每一次總是選擇根節點頻率最小的兩個樹先合並,這樣就能達到我們所希望的編碼結果。

        例:假設一個文本文件中只包含7個字符{A,B,C,D,E,F,G},這7個字符在文本中出現的次數為{5,24,7,17,34,5,13},求這些字符的哈夫曼編碼。

                                image

         仔細看上圖,發現樹的深度不一定是n(節點數),內節點個數是(n-1)。右子樹的頻率為17的內節點並沒有和字符節點的D(頻率17)合並,先和G(頻率13)合並了(用的是內節點頻率為17的,不是字符節點,數據結構課本上也是這么搞的),然后左子樹的D、B合並。

        為什么能壓縮?壓縮的時候當我們遇到了文本中的1 、 2 、 3 、 4 幾個字符的時候,我們不用原來的存儲,而是轉化為用它們的 01 串來存儲不久是能減小了空間占用了嗎。(什么 01 串不是比原來的字符還多了嗎?怎么減少?)大家應該知道的,計算機中我們存儲一個 int 型數據的時候一般式占用了32 個 01 位,因為計算機中所有的數據都是最后轉化為二進制位去存儲的。所以,想想我們的編碼不就是只含有 0 和 1 嘛,因此我們就直接將編碼按照計算機的存儲規則用位的方法寫入進去就能實現壓縮了。比如:1這個數字,用整數寫進計算機硬盤去存儲,占用了32個二進制位, 而如果用它的哈弗曼編碼去存儲,只有幾個二進制位,效果顯而易見。不過筆者認為這是采用字節流,那為什么我用字符流也能實現壓縮呢?因為寫入的時候都是一個字節,原來可能1、2、4字節。

二.Java實現

import java.awt.Container;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
public class HuffmanMain extends javax.swing.JFrame{
	
	static javax.swing.JTextArea textarea=null;
	public static void main(String[] args) {
		new HuffmanMain().display();
	}
	
	public void display(){
		this.setSize(400,400);
		this.setTitle("火星十一郎解壓壓縮軟件");
		this.setLayout(new java.awt.FlowLayout());
		//添加菜單欄
		this.setJMenuBar(getMenu());
		this.setVisible(true);
	}
	
	public JMenuBar getMenu(){
		//菜單欄、菜單項、菜單條目
		JMenuBar mb = new JMenuBar();
		
		//菜單項
		JMenu file = new JMenu("File");
		JMenu contact = new JMenu("Contact");
		//菜單條目
		JMenuItem Code = new JMenuItem("Code");
		JMenuItem Decode = new JMenuItem("Decode");
		JMenuItem exit = new JMenuItem ("Exit");
		JMenuItem qq = new JMenuItem ("About");
		
		//加入文件菜單項中
		file.add(Code);
		file.addSeparator();
		file.add(Decode);
		//在這加了個分割線
		file.addSeparator();
		file.add(exit);
		contact.add(qq);
		
		//添加菜單事件
		Action action = new Action();
		Code.addActionListener(action);
		Decode.addActionListener(action);
		exit.addActionListener(action);
		qq.addActionListener(action);
		
		//放上去
		mb.add(file);
		mb.add(contact);
		return mb;
	}
}
class Node implements Comparable<Node>{
	
   public char data;
   public int times;
   public Node lchild;
   public Node rchild;
   
   public Node(char data,int times){
	   this.data=data;
	   this.times=times;
   }
	   
	public int getData() {
		return data;
	}
	public void setData(char data) {
		this.data = data;
	}
	public int getTimes() {
		return times;
	}
	public void setTimes(int times) {
		this.times = times;
	}
	public Node getLchild() {
		return lchild;
	}
	public void setLchild(Node lchild) {
		this.lchild = lchild;
	}
	public Node getRchild() {
		return rchild;
	}
	public void setRchild(Node rchild) {
		this.rchild = rchild;
	}
	
	public int compareTo(Node o) {
		Node other = o;
		int temp = times-other.times;
		if(temp>0){
			return 1;
		}else {		
			return 0;
		}
	}
}

---------------------------------------------------------------------------------------------------------

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.filechooser.FileNameExtensionFilter;
public class Action implements ActionListener {
	public void actionPerformed(ActionEvent e) {
		String command = e.getActionCommand();
		//3個if是並列的,因為可能多次壓縮、解壓
		if ("Code".equals(command)) {
			
			System.out.println("進行壓縮");
			// 文件選擇
			JFileChooser Chooser = new JFileChooser();
			int t = Chooser.showOpenDialog(null);// 彈出文件選擇框
			if (t == Chooser.APPROVE_OPTION) {// 如果點擊的是確定
				// 得到文件的絕對路徑
				String path = Chooser.getSelectedFile().getAbsolutePath();
				HuffmanCode code = new HuffmanCode(path);
				code.readFile();// 讀取文件
				code.writeFile();// 寫出壓縮文件
			}
		}
		if ("Decode".equals(command)) {
			
			System.out.println("解壓縮");
			// 顯示打開的窗口
			JFileChooser chooser = new JFileChooser();
			//參數:文件描述(顯示的)和文件類型
			FileNameExtensionFilter filter = new FileNameExtensionFilter(
					"hxsyl壓縮文件", "hxsyl");
			chooser.setFileFilter(filter);
			int returnVal = chooser.showOpenDialog(null);
			if (returnVal == chooser.APPROVE_OPTION) {
				String path = chooser.getSelectedFile().getAbsolutePath();// 得到壓縮文件的路徑
				HuffmanDecode decode = new HuffmanDecode(path);
				decode.decode();// 解壓縮文件
			}
		}
		if ("Exit".equals(command)) {
			
			System.exit(0);
		}
		if ("About".equals(command)) {
			
			JOptionPane.showConfirmDialog(null, "By 791909235@qq.com", "作者",
					JOptionPane.YES_OPTION, JOptionPane.QUESTION_MESSAGE);
		}
	}
}

---------------------------------------------------------------------------------------

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Scanner;
public class HuffmanCode {
	private String path;// 文件的輸入絕對路徑
	Node root = null;// 定義的根節點
	private Map<Character,Integer> mm = new Hashtable<Character, Integer>();
	
	//大小:大小寫字母、數字、漢字,就開100吧
	private Map<Character,String> mapCode = new Hashtable<Character, String>();
	public HuffmanCode() {
		
	}
	
	public HuffmanCode(String path) {
		//頻率清零 初始化
		this.path = path;
		mapCode.clear();
	}
	//我想起了單例模式,才想起這種方法來在另一個java文件里因為一個java文件里的成員變量
	//注意不是方法,否則直接new 類調用就好
	public Map<Character,String> returnMap() {
		return this.mapCode;
	}
	public void readFile() {
		try {
			System.out.println("---------------讀入文件--------------");
			//把“\”換成“/”,實際上沒必要
			path.replaceAll("\\\\", "/");
			FileReader fr = new FileReader(path);
			BufferedReader br = new BufferedReader(fr);//有readline
			String str = "";
			mm.clear();
			while ((str=br.readLine())!=null) {
				System.out.println("讀入了:"+str);
				char[] ch = str.toCharArray();
				for(int i=0; i<ch.length; i++) {
					//加上if判斷就可以防止NullPointer異常了
					if(mm.get(ch[i])!=null) {
						mm.put(ch[i], mm.get(ch[i])+1);
					}else {
						mm.put(ch[i], 1);
					}
				}
			}
			System.out.println("---------文件讀入結束-------------");
			// 構建哈弗曼樹
			createHfmTree();
			// 遞歸生成編碼,root是成員函數
			genenateHFMCode(root, "");
			br.close();
			fr.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	public void writeFile() {
		try {
			System.out.println("------------寫入文件---------------");
			path.replaceAll("\\\\", "/");
			FileReader fr = new FileReader(path);
			BufferedReader br = new BufferedReader(fr);//有readline
			//第二個參數true表示追加
			BufferedWriter bw = new BufferedWriter(new FileWriter(path + ".hxsyl",true));
			String str = "";
			while ((str=br.readLine())!=null) {
				char[] ch = str.toCharArray();
				for(int i=0; i<ch.length; i++) {
					bw.write(mapCode.get(ch[i]));
				}
			}
			//刷新該流的緩沖,br沒有該方法
			bw.flush();
			System.out.println("------------文件寫入完畢---------------");
		} catch (Exception ef) {
			ef.printStackTrace();
		}
	}
	//廣搜建樹
	public void createHfmTree() { 
		//里面是Node
		PriorityQueue<Node> nodeQueue = new PriorityQueue<Node>();
		// 把所有的節點都加入到 隊列里面去
		Iterator<Character> iter = mm.keySet().iterator();
		while(iter.hasNext()) {
			char key = iter.next();
			if (mm.get(key)!= 0) {//實際上這個判斷沒必要,因為Map里沒的都是null
				Node node = new Node(key, mm.get(key));
				nodeQueue.add(node);// 加入節點
			}
		}
		//不是不為空,最后是一個節點
		while (nodeQueue.size() > 1) {
			Node min1 = nodeQueue.poll();
			Node min2 = nodeQueue.poll();
			/*
			 * data搞為‘$’,說明是內節點
			 * 權值和就是data值為$的節點的times值之和
			 * 使用優先隊列算權值就是基於此
			 */
			Node result = new Node('$', min1.times + min2.times);
			result.lchild = min1;
			result.rchild = min2;
			nodeQueue.add(result);
		}
		root = nodeQueue.peek(); // 得到根節點
	}
	//遞歸生成Hfm編碼
	public void genenateHFMCode(Node root, String s) {
		if (null == root) {
			return ;
		}
		//hfm節點全部是葉子節點
		if ((root.lchild == null) && (root.rchild == null)) {
			//root是Node
			System.out.println("節點" + root.data + "編碼" + s);
			mapCode.put(root.data,s);
		}
		if (root.lchild != null) {// 左0 右 1
			genenateHFMCode(root.lchild, s + '0');
		}
		if (root.rchild != null) {
			genenateHFMCode(root.rchild, s + '1');
		}
	}
}

----------------------------------------------------------------------------------------------

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/*
 * 為什么輸出漢子提示呢?
 * 因為實際上需要保存在日志里的.
 * 
 * 解碼需要全部讀入文件,不能一行一行,因為
 */
public class HuffmanDecode {
	private String path;
	Map<Character,String> mapCode = new Hashtable<Character, String>();
	
	public HuffmanDecode(String path) {
		this.path = path;
		//想了好久想到的方法,哈哈
		this.mapCode = new HuffmanCode().returnMap();
	}
	
	//通過
	private static char valueGetKey(Map<Character,String> map,StringBuffer value) {
	    Set set = map.entrySet();
	    Iterator it = set.iterator();
	    char ch='$';//表示沒有找到,實際沒必要,因為是哈夫曼編碼是單射
	    while(it.hasNext()) {
	      Map.Entry entry = (Map.Entry)it.next();
	      if(entry.getValue().equals(value)) {
	    	  //轉換為char報錯
	        ch = (Character)entry.getKey();
	        return ch;
	      }
	    }
	    //加這個是為了不讓編譯器報錯,原因如上
	    return ch;
	  }
	//解壓縮
	public void decode() {
		try {
			path.replaceAll("\\\\", "/");
			FileReader fr = new FileReader(path);
			BufferedReader br = new BufferedReader(fr);//有readline
			//第二個參數true表示追加
			int index = path.lastIndexOf(".hxsyl");
			path = path.substring(0,index);
			BufferedWriter bw = new BufferedWriter(new FileWriter(path,true));
			StringBuffer sb = new StringBuffer("");
			String str = "";
			
			while ((str=br.readLine())!=null) {
				sb.append(str);
			}
			//刷新該流的緩沖,br沒有該方法
			bw.flush();
			System.out.println(sb);
			System.out.println("------------解碼文件---------------");
			StringBuffer waitSB = new StringBuffer("");
			for(int i=0; i<sb.length(); i++) {
				if(mapCode.containsValue(waitSB)) {
					char ch = valueGetKey(mapCode, waitSB);
					bw.write(ch);
					waitSB.delete(0, waitSB.length());
					if(null==waitSB) {
						break;
					}
					
				}else {
					waitSB.append(sb.charAt(i));
				}
			}
			System.out.println("------------解碼完畢---------------");
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

        運行界面如下:

imageimage

三.結束語

         處理IO過程中我發現以Reader結尾的都是read單個字符,以stream結尾的都是read字節。如何從現有字符讀入呢?可以用StringReader或者System.in(str)。

        壓縮也能實現信息隱藏,把壓縮文件命名為常見格式比如txt,只有有該解壓軟件的才能打開。自己寫的,你懂得,bug在所難免,若您發現,還請告知,咱們共同進步。

       參考文獻:http://www.cppblog.com/biao/archive/2010/12/04/135457.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM