算法系列(二):贪心算法--Huffman编码

算法系列(二):贪心算法--Huffman编码

一、分析

问题描述:

     哈夫曼编码是广泛地用于数据文件压缩的十分有效的编码方法。其压缩率通常在20%~90%之间。哈夫曼编码算法使用字符在文件中出现的频率表来建立一个用0,1串表示各字符的最优表示方式。假设有一个数据文件包含100,000个字符,要用压缩的方法存储它。该文件中各字符出现的频率如表1-1所示。文件中共有6个不同字符出现,字符a出现45,000次,字符b出现13,000次等。

                                         表1-1     字符出现的频率表

                     

      有多种方式表示文件中的信息。若用0,1码串表示字符的方法,即每个字符用唯一的一个0,1串表示。若采用定长编码表示,则表示6个不同的字符需要3位(6<2^3):a=000,b=001,......,f=101。用这种方法对整个文件编码需要300,000位;若采用变长编码表示,给频率高的字符较短的编码;频率低的字符较长的编码,达到整体编码减少的目的,编码如表所示,则整个文件编码需要(45×1+13×3+12×3+16×3+9×4+5×4)×1000=224,000位,由此可见,变长码比定长码方案好,总码长减小约25%。事实上,这是该文件的最优编码方案。

    1.前缀码:对每一个字符规定一个0,1串作为其代码,并要求任一字符的代码都不是其他字符代码的前缀。这种编码称为前缀码。编码的前缀性质可以使译码方法非常简单;例如采用上例的编码,对于给定的0,1串001011101可唯一的分解为0,0,101,1101,因而其译码为aabe。

      译码过程需要方便的取出编码的前缀,因此需要表示前缀码的合适的数据结构。为此,可以用二叉树作为前缀码的数据结构:树叶表示给定字符;从树根到树叶的路径当作该字符的前缀码;代码中每一位的0或1分别作为指示某节点到左儿子或右儿子的“路标”。

                       算法系列(二):贪心算法--Huffman编码_第1张图片

                                                                                           图1-2 前缀码的二叉树表示

      容易看出,表示最优前缀码的二叉树总是一棵完全二叉树,即树中任一节点都有2个儿子。从图1-2a可以看出,定长编码方案不是最优的,其编码的二叉树不是一棵完全二叉树。在一般情况下,若C是编码字符集,表示其最优前缀码的二叉树中恰有|C|个叶子。每个叶子对应于字符集中的一个字符,该二叉树有 |C|-1 个内部节点(二叉树结论:n个节点的完全二叉树,叶子数为:⌈n/2⌉;分支节点数:⌊n/2⌋)。

      给定编码字符集C及频率分布f,即C中任一字符c以频率f(c)在数据文件中出现。C的一个前缀码编码方案对应于一棵二叉树T。字符c在树T中的深度记为dT(c)。dT(c)也是字符c的前缀码长。则平均码长定义为:使平均码长达到最小的前缀码编码方案称为C的最优前缀码。

      哈夫曼提出构造最优前缀码的贪心算法,由此产生的编码方案称为哈夫曼编码

      在哈夫曼编码分析之前先简单回顾贪心算法,贪心算法通过一系列的选择来得到问题的解。它所做的每一个选择都是当前状态下局部最好选择,即贪心选择。这种“选择就目前来看最好的”启发式的策略并不总能奏效,但也可能是最优的。

      基本模式:

                   算法系列(二):贪心算法--Huffman编码_第2张图片

二、求解

     2.构造哈夫曼编码哈夫曼提出构造最优前缀码的贪心算法,由此产生的编码方案称为哈夫曼编码。

      哈夫曼编码构造步骤如下:

      (1)哈夫曼算法以自底向上的方式构造表示最优前缀码的二叉树T。

      (2)算法以|C|个叶结点开始,执行|C|-1次的“合并”运算后产生最终所要求的树T。

      (3)假设编码字符集中每一字符c的频率是f(c)。以f为键值的优先队列Q用在贪心选择时有效地确定算法当前要合并的2棵具有最小频率的树。一旦2棵具有最小频率的树合并后,产生一棵新的树,其频率为合并的2棵树的频率之和,并将新树插入优先队列Q。经过n-1次的合并后,优先队列中只剩下一棵树,即所要求的树T。

      构造过程如图所示:

                                  算法系列(二):贪心算法--Huffman编码_第3张图片

三、程序

      设有文件有n个符号(即叶子节点数为n),则整个二叉树有(2n-1)个节点,组织在一个线性表中(如:数组)

             算法系列(二):贪心算法--Huffman编码_第4张图片

      每个哈夫曼树节点的数据结构:< c,f,p,l,r >   (c:符号,f:频率,p:父亲节点,l:左孩子节点,r:右孩子节点)

     例:        字符频率表:                                                             

              构造二叉树:算法系列(二):贪心算法--Huffman编码_第5张图片

                                                                              

              存储节点的线性表:

     程序分析及代码(Java):

      (1)数据结构之优先队列PriorityQueue(详细请看Java API:http://www.yq1012.com/api/java/util/PriorityQueue.html)

       类 PriorityQueue:一个基于优先级堆的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的Comparator 进行排序,具体取决于所使用的构造方法。 此队列的 队头 是按指定排序方式确定的最小/最大元素。队列获取操作 poll、remove、peek 和 element 访问处于队头的元素。

           构造方法:

      public PriorityQueue(int initialCapacity,
                     Comparator comparator)
使用指定的初始容量创建一个PriorityQueue,并根据指定的比较器对元素进行排序。
参数:
initialCapacity - 此优先级队列的初始容量
comparator - 用于对此优先级队列进行排序的比较器。如果该参数为null,则将使用元素的自然顺序
抛出:
IllegalArgumentException - 如果initialCapacity 小于 1
      (2)版本(1.0):绘制程序窗体:(包括菜单条,左边的 哈夫曼树 面板 和 右边的 哈夫曼编码 面板)
算法系列(二):贪心算法--Huffman编码_第6张图片
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Scanner;

import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;

/**
 * @author Sherly-Liu
 * @version 1.0(绘制窗体)
 */
class WindowsFrame extends JFrame implements ActionListener {
	File sourseFile;

	Container contentPane = this.getContentPane();
	JLabel backgroundLabel = new JLabel(); // 背景
	JPanel incodePanel = new JPanel(); // 哈夫曼编码面板

	JScrollPane scrollPane = new JScrollPane(); // JTree容器
	JPanel treepanel = new JPanel(); // 树
	JPanel codePanel = new JPanel(); // 表

	JMenuBar bar = new JMenuBar();
	JMenu menu1 = new JMenu(" 文件 ");
	JMenuItem item11 = new JMenuItem("打开源文件");
	JMenuItem item12 = new JMenuItem("压缩源文件");
	JMenuItem item13 = new JMenuItem("打开压缩文件");
	JMenuItem item14 = new JMenuItem("解压压缩文件");
	JMenuItem item15 = new JMenuItem("退出");

	JMenu menu2 = new JMenu(" 帮助 ");
	JMenuItem item21 = new JMenuItem("关于HuffmanDemo");

	public WindowsFrame() {
		super("HuffmanDemo");
		initialize();

	}

	/**
	 * 主窗口元素初始化
	 */
	public void initialize() {

		// 将窗口位置放在屏幕中央
		Dimension screensize = Toolkit.getDefaultToolkit().getScreenSize();
		this.setSize(1000, 625);
		Dimension framesize = this.getSize();
		int x = (int) screensize.getWidth() / 2 - (int) framesize.getWidth()
				/ 2;
		int y = (int) screensize.getHeight() / 2 - (int) framesize.getHeight()
				/ 2;
		this.setLocation(x, y);

		// 设置背景图片,把Lable加进LayoutPane
		try {
			backgroundLabel.setIcon(new ImageIcon(ImageIO.read(new File(
					"drawable", "background2.jpg"))));
		} catch (IOException e) {
			e.printStackTrace();
		}

		this.getLayeredPane().add(backgroundLabel,
				new Integer(Integer.MIN_VALUE));
		backgroundLabel.setBounds(0, 0, 1000, 625);

		this.setJMenuBar(bar);
		menu1.add(item11);
		menu1.addSeparator();// 分割线
		menu1.add(item12);
		menu1.addSeparator();// 分割线
		menu1.add(item13);
		menu1.addSeparator();// 分割线
		menu1.add(item14);
		menu1.addSeparator();// 分割线
		menu1.add(item15);

		menu2.add(item21);

		bar.add(menu1);
		bar.add(menu2);

		item11.addActionListener(this);
		item12.addActionListener(this);
		item13.addActionListener(this);
		item14.addActionListener(this);
		item15.addActionListener(this);
		item21.addActionListener(this);

		contentPane.setLayout(null);

		((JPanel) contentPane).setOpaque(false);// 内容面板设置为透明,LayoutPane面板背景才显现
		this.setResizable(false);
		this.setVisible(true);

		// JTree
		scrollPane
				.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
		scrollPane.setBounds(0, 0, 300, 570);
		treepanel.setBounds(0, 0, 300, 570);

		scrollPane.add(treepanel);
		scrollPane.setBackground(new Color(255, 255, 255, 68));
		treepanel.setVisible(true);

		codePanel.setBounds(300, 0, 700, 600); // 数据表
		codePanel.setVisible(true);
		codePanel.setBackground(new Color(255, 255, 255, 0));

		incodePanel.setBounds(0, 0, 1000, 600);
		incodePanel.setBackground(new Color(255, 255, 255, 0));
		incodePanel.setLayout(null);
		incodePanel.add(scrollPane);
		incodePanel.add(codePanel);
		incodePanel.setVisible(false);
		contentPane.add(incodePanel);

		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

	}

	/**
	 * 菜单响应事件
	 */
	@Override
	public void actionPerformed(ActionEvent e) {
		String command = e.getActionCommand();
		// 打开源文件
		// ********************************************
		if (command.equals("打开源文件")) {
			JFileChooser fc = new JFileChooser();
			int val = fc.showOpenDialog(null); // 问件选择器
			if (val == fc.APPROVE_OPTION) {
				// 数据表
				sourseFile = fc.getSelectedFile();
				Scanner readFile = null;
				try {
					readFile = new Scanner(sourseFile);
				} catch (FileNotFoundException e1) {
					e1.printStackTrace();
				}// Scanner对象读取文件
				String buff = "";
				while (readFile.hasNext()) { // 如果文件读取没有到结尾,一直循环
					buff += readFile.nextLine();// 读取一行,并加入到buff后面
				}
				readFile.close();// 关闭对文件的通道,此时buff就是txt文件的所有内容了。
				String str = buff.toString();

				int n = 1;// 节点总数
				Object[][] rowData = new Object[2 * n - 1][9];
				for (int i = 0; i < n; i++) {
					rowData[i][0] = i;
					rowData[i][1] = null;
					rowData[i][2] = null;
					rowData[i][3] = null;
					rowData[i][4] = null;
					rowData[i][5] = null;
					rowData[i][6] = null;
					rowData[i][7] = null;
					rowData[i][8] = null;
				}
				// 列名最好用final修饰
				final Object[] columnNames = { "Index", "Char", "Hex", "Left",
						"Right", "Parent", "Frequency", "Code", "Code-len" };

				JTable hTable = new JTable(rowData, columnNames);
				hTable.setPreferredScrollableViewportSize(new Dimension(700,
						100)); // 设置表格的大小
				hTable.setRowHeight(20);// 设置每行的高度为20
				hTable.setRowMargin(5); // 设置相邻两行单元格的距离
				hTable.setRowSelectionAllowed(true);// 设置可否被选择.默认为false
				hTable.setSelectionBackground(Color.white);// 设置所选择行的背景色
				hTable.setSelectionForeground(Color.red); // 设置所选择行的前景色
				hTable.setGridColor(Color.black);// 设置网格线的颜色
				hTable.setShowGrid(true); // 是否显示网格线
				hTable.setShowHorizontalLines(true);// 是否显示水平的网格线
				hTable.setShowVerticalLines(true); // 是否显示垂直的网格线
				hTable.doLayout();
				hTable.setBackground(Color.LIGHT_GRAY);

				ScrollPane pane = new JScrollPane(hTable);
				pane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
				pane.setPreferredSize(new Dimension(680, 500));
				pane.setBackground(Color.black);
				codePanel.add(pane);
				incodePanel.setVisible(true);

			} else {
				JOptionPane.showMessageDialog(null, "未选取文件", "警告",
						JOptionPane.WARNING_MESSAGE, null);
			}

		}

		// 压缩源文件******************************************
		if (command.equals("压缩源文件")) {

		}

		// 打开压缩文件******************************************
		if (command.equals("打开压缩文件")) {

		}

		// 解压压缩文件******************************************
		if (command.equals("解压压缩文件")) {

		}

		// 退出******************************************
		if (command.equals("退出")) {
			int result = JOptionPane.showConfirmDialog(null, "是否真的退出程序?",
					"提示信息", JOptionPane.YES_NO_OPTION,
					JOptionPane.QUESTION_MESSAGE);
			if (result == 0) {
				System.exit(0);
			}

		}
		if (command.equals("关于HuffmanDemo")) {
			JOptionPane.showMessageDialog(null,
					"Huffman Compress & DeCompress Demo 1.0" + "\n"
							+ "Author:刘晓玲" + "\n" + "2015.12", "关于HuffmanDemo",
					JOptionPane.INFORMATION_MESSAGE, null);

		}
	}
}

// ***************************************

public class Main {
	public static void main(String[] args) {
		new WindowsFrame();
	}
}


(3)版本(1.1):哈夫曼树节点 数据结构:< c,f,p,l,r >

/**
 * @author Sherly-Liu 哈夫曼树节点 数据结构:< c,f,p,l,r >
 *         (c:符号,f:频率,p:父亲节点,l:左孩子节点,r:右孩子节点)
 */
public class HuffmanNode {
	private int index; // 索引,结点在数组里的标号
	private char c; // 字符,当为叶子结点时才有意义
	private float f; // 频率
	private int l, r, p; // p为父节点,若为-1则为根
							// l,r若为-1则为叶子
	private int nums; // 各个字符字符数

	public HuffmanNode() {
		f = 0; // 初始化
		l = -1;
		r = -1;
		p = -1;
		nums = 0;
	}

	public int getIndex() {
		return index;
	}

	public void setIndex(int idx) {
		this.index = idx;
	}

	public char getC() {
		return c;
	}

	public void setC(char c) {
		this.c = c;
	}

	public float getF() {
		return f;
	}

	public void setF(float f) {
		this.f = f;
	}

	public int getL() {
		return l;
	}

	public void setL(int l) {
		this.l = l;
	}

	public int getR() {
		return r;
	}

	public void setR(int r) {
		this.r = r;
	}

	public int getP() {
		return p;
	}

	public void setP(int p) {
		this.p = p;
	}

	public int getNums() {
		return nums;
	}

	public void setNums(int num) {
		this.nums = num;
	}
}
(4)版本(1.2):构造哈夫曼树

import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Stack;
import java.util.TreeMap;
import java.util.Map.Entry;

/**
 * @author Sherly-Liu 
 * 哈夫曼树 :n个叶子节点,n-1个内节点,
 *        利用优先队列每次取队列里频率最低(贪心选择)的两个节点组成新的父节点(频率值为原两节点的频率之和),
 *        并将新的节点加入优先队列
 */
public class HuffmanTree {
	HuffmanNode[] t; //节点总数为2n-1,n为叶子节点数数(字符数)
	int i = 0;
	Map map;
	int n;

	/**
	 * 构造方法
	 * @param str待编码字符串
	 */
	public HuffmanTree(String str) {
		char[] ch = str.toCharArray();
		map = new TreeMap();

		for (i = 0; i < ch.length; i++) {
			// 统计字符频率
			Integer value = map.get(ch[i]);
			int count = 1;
			if (value != null) {
				count = value + 1;
			}
			map.put(ch[i], count);
		}

		Iterator> it = map.entrySet().iterator();
		// 分配存储空间
		n = map.size();
		t = new HuffmanNode[2 * n - 1];
		for (i = 0; i < t.length; i++) {
			t[i] = new HuffmanNode();
		}
		// 初始化
		int j = 0;
		while (it.hasNext()) {
			Entry en = it.next();
			char cha = en.getKey();
			int value = en.getValue();
			t[j].setIndex(j);
			t[j].setC(cha);
			t[j].setNums(value);
			t[j].setF((float) value / str.length());
			j++;
		}

		// 自定义优先队列的优先级(队尾到队头降序):频率值低的在队头
		Comparator OrderIsdn = new Comparator() {
			public int compare(HuffmanNode node1, HuffmanNode node2) {
				float f1 = node1.getF();
				float f2 = node2.getF();
				if (f2 < f1) {
					return 1;
				} else if (f2 > f1) {
					return -1;
				} else {
					return 0;
				}
			}
		};
		// 采用优先队列构造哈夫曼树
		Queue priorityQueue = new PriorityQueue(11,
				OrderIsdn);
        //将n个叶子节点加入优先队列
		for (i = 0; i < n; i++) {
			priorityQueue.add(t[i]);
		}

		int nNext = n;// 下一个要生成的父结点的标号
		while (priorityQueue.size() > 1) {
			// 左孩子
			HuffmanNode L = priorityQueue.poll();// 具有最小f的结点出队
			// 右孩子
			HuffmanNode R = priorityQueue.poll();// f最小的两个结点出队
			// 构造父结点
			HuffmanNode P = t[nNext];
			P.setL(L.getIndex());
			P.setR(R.getIndex());
			P.setIndex(nNext);
			P.setF(L.getF() + R.getF());

			t[L.getIndex()].setP(nNext);
			t[R.getIndex()].setP(nNext);
			priorityQueue.add(P);
			nNext++;
		}
	}

	/**
	 * 编码的构造:由叶子自底向上直到根节点进行编码,左孩子编码为0,右孩子编码为1,
	 *         编码先进后出,所以用栈存储编码结果
	 * @param k:节点编号
	 * @return
	 */
	public String Incode(int k) {
		if (k >= n) {
			return null; // 非叶子结点
		} else {
			Stack S = new Stack();
			StringBuilder sb = new StringBuilder();
			while (t[k].getP() != -1) {
				int p = t[k].getP();
				if (k == t[p].getL()) {
					S.push(0);
				} else {
					S.push(1);
				}
				k = p;
			}
			// 出栈得到该叶子节点字符的编码
			while (!S.isEmpty()) {
				sb.append(S.pop());
			}
			return sb.toString();
		}
	}
}
(5)版本(1.3):实现各菜单项的监听事件

1.打来源文件:读取文件字串,统计各字符个数,构造哈夫曼树,并打印存储节点的线性表以及哈夫曼树。

    /**
     * 菜单响应事件
     */
    @Override
    public void actionPerformed(ActionEvent e){
        String command = e.getActionCommand();
        // 打开源文件
        // ********************************************
        if (command.equals("打开源文件")) {
            JFileChooser fc = new JFileChooser();
            int val = fc.showOpenDialog(null); // 问件选择器
            if (val == fc.APPROVE_OPTION) {
                unCompressSuccess = false;
                // 数据表
                File sourceFile=null;
                sourceFile = fc.getSelectedFile();
                FileInputStream fis=null;
                InputStreamReader isr = null;
                BufferedReader br = null;
                String buff = "";
                sourceStr = "";//清空原数据
                try {
                    fis = new FileInputStream(sourceFile);
                } catch (FileNotFoundException e1) {
                    e1.printStackTrace();
                }
                try {
                    isr = new InputStreamReader(fis,"gbk");
                } catch (UnsupportedEncodingException e1) {
                    e1.printStackTrace();
                }
                br = new BufferedReader(isr);
                String line = null;
                try {
                    while ((line=br.readLine())!=null) {// 如果文件读取没有到结尾,一直循环
                        buff += line;// 读取一行,并加入到buff后面
                        buff +="\r\n";//补上换行符
                    }
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
                if(br!=null){
                    //注意流用完要关闭
                    try {
                        br.close();
                    } catch (IOException e1) {
                        e1.printStackTrace();
                    }
                    // 关闭对文件的通道,此时buff就是txt文件的所有内容了。
                    
                }
                //去掉最后一个补上的换行符
                sourceStr = buff.substring(0, buff.length()-2); // 源文件字串
                try {
                    sourceStr = new String(sourceStr.getBytes("utf-8"), "utf-8");// 转化成utf-8编码
                } catch (UnsupportedEncodingException e1) {
                    e1.printStackTrace();
                }
                
                HuffmanTree hftree = new HuffmanTree(sourceStr);
                int n = hftree.map.size();
                Object[][] rowData = new Object[2 * n - 1][9];
                codeMap.clear();//清空脏数据
                // 叶子节点
                for (int i = 0; i < n; i++) {
                    String cString = String.valueOf(hftree.t[i].getC());
                    String unicode="";
                    byte[] b_utf8=null;
                    try {
                        b_utf8=cString.getBytes("UTF-8");//获得字节数组
                    } catch (UnsupportedEncodingException e2) {
                        e2.printStackTrace();
                    }
                    unicode = Bytes2HexString(b_utf8);
                    
                    rowData[i][0] = i;
                    rowData[i][1] = hftree.t[i].getC();
                    rowData[i][2] = unicode;
                    rowData[i][3] = hftree.t[i].getL();
                    rowData[i][4] = hftree.t[i].getR();
                    rowData[i][5] = hftree.t[i].getP();
                    rowData[i][6] = hftree.t[i].getNums();
                    rowData[i][7] = hftree.Incode(i);
                    rowData[i][8] = hftree.Incode(i).length();

                    // 编码信息存进编码表
                    codeMap.put(rowData[i][2], rowData[i][7]);
                }
                // 内节点
                for (int i = n; i < 2 * n - 1; i++) {
                    rowData[i][0] = i;
                    rowData[i][1] = null;
                    rowData[i][2] = null;
                    rowData[i][3] = hftree.t[i].getL();
                    rowData[i][4] = hftree.t[i].getR();
                    rowData[i][5] = hftree.t[i].getP();
                    rowData[i][6] = null;
                    rowData[i][7] = null;
                    rowData[i][8] = null;
                }
                readFileSuccess = true;// 成功读取文件

                // 列名最好用final修饰
                final Object[] columnNames = { "Index", "Char", "UTF-8", "Left",
                        "Right", "Parent", "Frequency", "Code", "Code-len" };
                DefaultTableModel model;
                if (hTable != null) {
                    model = (DefaultTableModel) hTable.getModel();
                    model = new DefaultTableModel(rowData, columnNames);
                    hTable.setModel(model);
                    hTable.repaint();
                    hTable.updateUI();
                } else {
                    model = new DefaultTableModel(rowData, columnNames);
                    hTable = new JTable(model);
                    hTable.setPreferredScrollableViewportSize(new Dimension(
                            700, 100)); // 设置表格的大小
                    hTable.setRowHeight(20);// 设置每行的高度为20
                    hTable.setRowMargin(5); // 设置相邻两行单元格的距离
                    hTable.setRowSelectionAllowed(true);// 设置可否被选择.默认为false
                    hTable.setSelectionBackground(Color.white);// 设置所选择行的背景色
                    hTable.setSelectionForeground(Color.red); // 设置所选择行的前景色
                    hTable.setGridColor(Color.black);// 设置网格线的颜色
                    hTable.setShowGrid(true); // 是否显示网格线
                    hTable.setShowHorizontalLines(true);// 是否显示水平的网格线
                    hTable.setShowVerticalLines(true); // 是否显示垂直的网格线
                    hTable.doLayout();
                    hTable.setBackground(Color.LIGHT_GRAY);

                    // TokenPanel
                    codeScrollPane = new JScrollPane(hTable);
                    codeScrollPane
                            .setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
                    codeScrollPane.setPreferredSize(new Dimension(680, 555));
                    codeScrollPane.setBackground(Color.black);
                    codePanel.add(codeScrollPane);
                    incodePanel.setVisible(true);
                }
                // #########################
                // HuffmanTree
                // top:根结点
                final JTree tree = new JTree(CreateTree(hftree, 2 * n - 2));
                treeScrollPane.setViewportView(tree);
            } else {
                JOptionPane.showMessageDialog(null, "未选取文件", "警告",
                        JOptionPane.WARNING_MESSAGE, null);
            }
        }

        // 压缩源文件******************************************
        if (command.equals("压缩源文件")) {
            if (!readFileSuccess) {
                JOptionPane.showMessageDialog(null, "请先打开源文件!", "警告",
                        JOptionPane.WARNING_MESSAGE, null);
            } else {
                readFileSuccess = false;
                unCompressSuccess = false;
                inString = Compress(sourceStr, codeMap);// 压缩
                JFileChooser fc = new JFileChooser();
                int selectFile = fc.showSaveDialog(this);
                if (selectFile == JFileChooser.APPROVE_OPTION) {
                    File f = fc.getSelectedFile();        
                    // 向文件里写压缩后的字符串.
                    try {
                         // 要写入的数据转换成字节数组
                        byte[] context = new byte[inString.length()/2];
                        
                        for(int i=0;i
打开source.txt文件,进行哈夫曼编码:
                              算法系列(二):贪心算法--Huffman编码_第7张图片


算法系列(二):贪心算法--Huffman编码_第8张图片

2.压缩源文件:

        压缩原理:经过哈夫曼编码后,每一个在源文件里出现过的字符根据出现的频率大小进行编码,成了长度各异的0,1串,频率大的0,1串长度短,频率小的编码长度长,压缩过程使用字符的0,1编码替代原字符,用0,1串之所以能节省存储空间是因为例如压缩的时候我们文本中存的是1234这几个字符,我们不用原来的存储,而是转化为用它们的01串来存储。(疑问:01串不是比原来的字符还多了吗?怎么减少?)大家应该知道的,计算机中我们存储一个int型(4字节)数据的时候存储空间一般占用32比特位(0,1位),由于计算机中所有的数据最终都转化为二进制位去存储,而我们的编码就只含有01因此我们就直接将编码按照计算机的存储规则用位的方法写入进去就能实现压缩了。比如:1这个数字,用整数写进计算机硬盘去存储,占用了32个二进制位,而如果用它的哈弗曼编码(假如为110)去存储,只有110三个二进制位,压缩效果显而易见。

	/**
	 * 压缩(PS:将目前待压缩的文件以utf-8编码,可支持中文汉字)
	 * 
	 * @param source源文件字串
	 * @param cordMap编码表
	 * @return compressStr 压缩后的字串 
	 * 压缩规则:1.用三个字节表示文件里存在的字符类型的个数(最大:2^24-1=16777215个,注意Java不支持无符号数),
	 * 2.每个字符用相应的UTF-8码表示(注意:UTF-8是变长的编码),
	 * 3.写入每个字符的Huffman编码(用一个字节表示编码的长度,HuffmanCode不足8位的补0),
	 * 4.四个字节表示文章的字符总数(最大:2^31-1个,注意Java是不支持无符号数的,所以int型最大表示为2^31-1)
	 * 5.以Huffman编码记录源文件
	 */
	public String Compress(String source, HashMap cordMap) {
		String compressStr = "";
		int num = cordMap.size();//字符类型的总数
		String numHexString="";//字符类型的总数
		String codeCharStr="";//字符以UTF-8码表示
		String codeLen="";//Huffman编码长度
		String code="";//Huffman编码
		String sourChsSum="";//源文件字符总数
		String sourToHuff="";//以Huffman编码记录源文件
		if(num<256){//注意边界,包含255(0xFF=255)
			numHexString = "0000"+Dec2HexString(num);//最大:0x0000FF
		}
		if(num>255&&num<65536){//注意边界,包含256到65535(0xFFFF=65535)
			numHexString = "00"+Dec2HexString(num);//最大:0x00FFFF
		}
		if(num>65535&&num<16777216){//注意边界,包含65536到16777215(0xFFFFFF=16777215)
			numHexString = "00"+Dec2HexString(num);//最大:0xFFFFFF
		}
		Iterator iter = codeMap.entrySet().iterator();
		while (iter.hasNext()) {
			Map.Entry entry = (Map.Entry) iter.next();
		    String key = entry.getKey().toString();//字符UTF-8表示
			codeCharStr += key;
		}
		
		compressStr += numHexString; //1.用三个字节表示文件里存在的字符类型的个数
		compressStr += codeCharStr;  //2.每个字符用相应的UTF-8码表示
		//3.写入每个字符的Huffman编码(用一个字节表示编码的长度,HuffmanCode不足8位的补0)
		Iterator iter1 = codeMap.entrySet().iterator();
		while (iter1.hasNext()) {
			Map.Entry entry = (Map.Entry) iter1.next();
		    String huffmanCode = entry.getValue().toString();//HuffmanCode
			codeLen =Dec2HexString(huffmanCode.length());
			compressStr += codeLen;
			
			// 将0,1串压缩,每8个二进制数(一个字节)转2个十六进制数,对8取余,余数单独存(补0)
			int quotient = huffmanCode.length() / 8;// 商
			int remainder = huffmanCode.length() % 8;// 余数
			for (int i = 0; i < quotient; i++) {
				// 每4个步长摘取(4个二进制数表示一个16进制数)
				String subStr1 = huffmanCode.substring(8 * i, 8 * i + 4);
				String subStr2 = huffmanCode.substring(8 * i + 4,
						8 * i + 8);// 每4个步长摘取
				String subHex1 = Integer.toHexString(Integer.parseInt(
						subStr1, 2));// 转十六进制
				String subHex2 = Integer.toHexString(Integer.parseInt(
						subStr2, 2));// 转十六进制
				String subHex = subHex1 + subHex2;
				subHex = subHex.toUpperCase();
				compressStr += subHex;
			}
			//不足8位的补0
			if(remainder!=0){
				int addZeroNum = 8-remainder;//需要补全的0的个数
				String addZeroCode = huffmanCode.substring(8 * quotient, 8 * quotient
						+ remainder);
				for(int j=0;j255&&sourceCharsSum<65536){//注意边界,包含256到65535(0xFFFF=65535)
			sourChsSum = "0000"+Dec2HexString(sourceCharsSum);//最大:0x0000FFFF
		}
		if(sourceCharsSum>65535&&sourceCharsSum<16777216){//注意边界,包含65536到16777215(0xFFFFFF=16777215)
			sourChsSum = "00"+Dec2HexString(sourceCharsSum);//最大:0x00FFFFFF
		}
		if(sourceCharsSum>16777215&&sourceCharsSum<=2147483647){//注意边界,包含16777216到2147483647(0x7FFFFFFF=2147483647)
			sourChsSum = "00"+Dec2HexString(sourceCharsSum);//最大:0x7FFFFFFF
		}
		sourChsSum = sourChsSum.toUpperCase();
		compressStr += sourChsSum;		
		
		//5.以Huffman编码记录源文件(末位不足八位补0)
		//5.1将源文件字符逐个转HuffmanCode
		char sourceChars[] = new char[sourceCharsSum];
		sourceChars = source.toCharArray();
		for(int i=0;i
			sourToHuff+=huffCode;//二进制:10101010这样的01串
		}
		//5.2将暂存在String sourToHuff的0,1串转化成HexString,
		//转化方式依旧是每8个二进制数(一个字节)转2个十六进制数,对8取余,余数单独存(补0)
		int quotient = sourToHuff.length() / 8;// 商
		int remainder = sourToHuff.length() % 8;// 余数
		for (int i = 0; i < quotient; i++) {
			// 每4个步长摘取(4个二进制数表示一个16进制数)
			String subStr1 = sourToHuff.substring(8 * i, 8 * i + 4);
			String subStr2 = sourToHuff.substring(8 * i + 4,
					8 * i + 8);// 每4个步长摘取
			String subHex1 = Integer.toHexString(Integer.parseInt(
					subStr1, 2));// 转十六进制
			String subHex2 = Integer.toHexString(Integer.parseInt(
					subStr2, 2));// 转十六进制
			String subHex = subHex1 + subHex2;
			subHex = subHex.toUpperCase();
			compressStr += subHex;
		}
		//不足8位的补0
		if(remainder!=0){
			int addZeroNum = 8-remainder;//需要补全的0的个数
			String addZeroCode = sourToHuff.substring(8 * quotient, 8 * quotient
					+ remainder);
			for(int j=0;j
3.解压缩:

	/**
	 * 解压缩(PS:按压缩规则解压缩,虽然utf-8编码为变长编码,其实也很好解码,
	 * UTF-8码解码规则:如果一个字节的第一位是0,则这个字节单独就是一个字符;
	 * 如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节)
	 * 
	 * @param encodingStr 压缩文件字符串
	 * @return decodingStr 解压缩(解码后)的字符串==源文件字符串  (因为Huffman编码是无损压缩的)
	 * 
	 * 压缩规则:1.用三个字节表示文件里存在的字符类型的个数(最大:2^24-1=16777215个,注意Java不支持无符号数),
	 * 2.每个字符用相应的UTF-8码表示(注意:UTF-8是变长的编码),
	 * 3.写入每个字符的Huffman编码(用一个字节表示编码的长度,HuffmanCode不足8位的补0),
	 * 4.四个字节表示文章的字符总数(最大:2^31-1个,注意Java是不支持无符号数的,所以int型最大表示为2^31-1)
	 * 5.以Huffman编码记录源文件
	 */
	public String Uncompress(String encodingStr) {
		String decodingStr = "";
		int num = 0;//字符类型的总数
		String numHexString="";//字符类型的总数
		String codeCharStr[];//字符以UTF-8码表示
		int codeLen=0;//Huffman编码长度
		String code="";//Huffman编码
		String sourChsSum="";//源文件字符总数
		int charsSum = 0;//源文件字符总数
		String huffToUTF8="";//以HUTF8编码记录压缩文件
		String codesStr = "";//压缩文件中以HuffCode编码记录的源文件字串
		
		int pointer=0;//遍历encodingStr的指针
		
		//1.用三个字节表示文件里存在的字符类型的个数
		numHexString = encodingStr.substring(pointer, 6);
		num = HexString2Dec(numHexString);
		pointer = 6;//遍历到第六个字符
		codeCharStr = new String[num];
		//2.每个字符用相应的UTF-8码表示
		for(int i=0;i>>1;//带符号右移1位,高空位始终补0
					tab = HexString2Dec(temp) & check;
					numOf1+=1;//当前指向的二进制位为1时,numOf1加1
				}//循环结束时求得连续的1的个数,表示当前字符占用多少个字节
				unicodeKey = encodingStr.substring(pointer, pointer+numOf1*2);//当前字符占用numOf1个字节
				codeMapCMP1.put(unicodeKey, "0");//取得一个UTF-8码加入编码表
				codeCharStr[i] = unicodeKey;
				pointer = pointer+numOf1*2;//遍历指针右移numOf1个字节
			}
		}//循环结束时取得所有字符的UTF-8码
		//3.读取每个字符的Huffman编码(用一个字节表示编码的长度,HuffmanCode不足8位的补0)
		for(int i=0;i的编码表构造编码表
		Iterator iter = codeMapCMP1.entrySet().iterator();
		while (iter.hasNext()) {
			Map.Entry entry = (Map.Entry) iter.next();
		    String key = entry.getKey().toString();//字符UTF-8表示
		    String value = entry.getValue().toString();//字符HuffmanCode表示
			codeMapCMP2.put(value, key);
		}
		//4.四个字节表示源文件的字符总数
		sourChsSum = encodingStr.substring(pointer, pointer+8);
		charsSum = HexString2Dec(sourChsSum);
		pointer = pointer+8;//遍历指针右移四个字节
		//5.解析以Huffman编码记录的压缩文件
		//5.1将压缩文件HuffmanCode逐个转UTF-8字符(例如:11001-->41(A))
		codesStr = encodingStr.substring(pointer, encodingStr.length());//压缩文件中以HuffCode编码记录的源文件字串
		//注意此时codesStr是用十六进制表示的
		String codesBinary = HexString2BinaryString(codesStr);//将codesStr转成二进制的01串
		int count=0;//记录已经转换的字符数,最终count==charsSum
		code = "";
		for(int i=0;i
				String value = codeMapCMP2.get(code).toString();
				huffToUTF8 += value;
				count++;
				code = "";
				if(count==charsSum)
					break;
			}	
		}//循环结束时压缩文件完成转码
		//5.2将暂存在String huffToUTF8的UTF-8码转换成相应的字符
	    //格式化:先在每个字节前加上%,转成如下格式:"%E6%98%9F%E6%9C%9F%E5%87%A0"
		String formatStr = "";
		for(int i=0;i
4.一些转码方法:

/**
	 * 转化字符为十六进制编码(a-->61)
	 * 
	 * @param s
	 * @return
	 */
	public static String Char2Hex(char c) {
		String retStr = "";
		retStr = Integer.toHexString(c).toUpperCase();
		if(retStr.length()==1)
			retStr ="0"+retStr;
		return retStr;
	}

	/**
	 * 转化十六进制编码为字符串(61-->a)
	 * 
	 * @param s
	 * @return
	 */
	public static String Hex2String(String s) {
		byte[] byte1 = new byte[s.length() / 2];
		for (int i = 0; i < byte1.length; i++) {
			try {
				byte1[i] = (byte) (0xff & Integer.parseInt(
						s.substring(i * 2, i * 2 + 2), 16));
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		try {
			s = new String(byte1, "utf-8");
		} catch (Exception e1) {
			e1.printStackTrace();
		}
		return s;
	}
	
	/**
	 * 将byte[]转换成十六进制的字符串
	 * 
	 * @param b
	 * @return
	 */
	public static String Bytes2HexString(byte[] b) {
		String string = "";
		for (int i = 0; i < b.length; i++) {
			String hex = Integer.toHexString(b[i] & 0xFF);
		    if (hex.length() == 1) {
		    	hex = '0' + hex;
		    	}
		    string += hex.toUpperCase();
		    }
		return string;
		}

	/**
	 * 将十六进制字符串转换成byte[]
	 * @param string
	 * @return
	 */
	public static byte[] HexString2Bytes(String string) {
		//字节数组(要写入的数据转换成字节数组)
		byte[] context = new byte[string.length()/2];
		for(int i=0;i0A)
	 * @param string
	 * @return
	 */
	public static String Dec2HexString(int integer) {
		String string = "";
		string = Integer.toHexString(integer);
		if (string.length()%2 == 1) {
			string = "0" + string;
			}
		string = string.toUpperCase();
		return string;
	}
	
	/**
	 * 将HexString转换成十进制数(0A-->10)
	 * @param hexString
	 * @return
	 */
	public static int HexString2Dec(String hexString) {
		int sum = 0, temp = 0;
		for (int i = hexString.length()-1,j=0; i >= 0; i--,j++) {
			char c = hexString.charAt(i);
			if (c >= '0' && c <= '9')
				temp = c - '0';
			if (c >= 'A' && c <= 'F')
				temp = c - 'A' + 10;
			sum += Math.pow(16, j) * temp;
			}
		return sum;
	}
	
	/**
	 * 将HexString转换成二进制数(0A-->00001010)
	 * @param hexString
	 * @return
	 */
	public static String HexString2BinaryString(String hexString) {
		if (hexString == null || hexString.length() % 2 != 0)
			return null;
		String bString = "", temp;
		for (int i = 0; i < hexString.length(); i++) {
			temp = "0000"
					+ Integer.toBinaryString(Integer.parseInt(
							hexString.substring(i, i + 1), 16));
			bString += temp.substring(temp.length() - 4);
		}
		return bString;
	}

  在这解释一下编码的问题,程序一再强调了用UTF-8编码,我们知道ASCII编码是一个字节的编码,其使用了低7位(bit位)表示128(2^7)个最基本的字符,所以ASCII字符集是一个很小的字符集,只能用来显示现代英语和其他西欧语言。然而,不同国家、语种对其他字符的需求产生了各种其他字符集和编码系统,因此Unicode出现以前,编码和字符集不统一,不同编码系统存在冲突。Unicode又称为统一码,其目的是为给全世界各个国家、各种语言的字符提供一个统一的编码,那么我们为什么用UTF-8码而不用Unicode码呢?Unicode码和UTF-8码又有什么联系呢?

   我们之所以不用Unicode码是因为我们自己解析压缩文件时有难度:Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。比如,汉字"严"的unicode是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。

这里就有两个严重的问题,第一个问题是,如何才能区别Unicode和ASCII?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果Unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。

  到这我们基本能解析清楚我们为什么要用UTF-8编码了,一、为了节省存储的开销我们需要一种变长的编码,像数字英文字母这些用一个字节表示就够了,而汉字、韩文等复杂的字符用两个或者三个字节表示;二、为了识别三个字节到底是代表3个单字节的字符 还是1个单字节的字符+1个双字节的字符 或者就只代表1个三字节的字符 。而UTF-8码是如何解决这两个问题的呢?

 UTF-8码:

互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种Unicode的实现方式。其他实现方式还包括UTF-16(字符用两个字节或四个字节表示)和UTF-32(字符用四个字节表示),不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8是Unicode的实现方式之一。

UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。

UTF-8的编码规则很简单,只有二条:

1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。

2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。

下表总结了编码规则,字母x表示可用编码的位。

Unicode符号范围 | UTF-8编码方式
(十六进制) | (二进制)
--------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

跟据上表,解读UTF-8编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。

下面,还是以汉字"严"为例,演示如何实现UTF-8编码。

已知"严"的unicode是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此"严"的UTF-8编码需要三个字节,即格式是"1110xxxx 10xxxxxx 10xxxxxx"。然后,从"严"的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,"严"的UTF-8编码是"11100100 10111000 10100101",转换成十六进制就是E4B8A5。

关于各种编码详细的请参考这篇博文:字符编码笔记:ASCII,Unicode和UTF-8

哈哈,请原谅我文章又一次把代码分析得那么细了,以后不会写那么罗嗦的了。保证。

下面给大家演示一下程序:

1.准备一个待压缩的文本,我们命名为source.txt

     算法系列(二):贪心算法--Huffman编码_第9张图片

2.run application,点击“打开源文件”菜单命令,选中我们要压缩的文件。算法系列(二):贪心算法--Huffman编码_第10张图片


算法系列(二):贪心算法--Huffman编码_第11张图片

我们看到程序面板上打印了HuffmanTree和编码表,并且可以看到是支持中文的,中文一般是用三个字节表示

3.点击菜单命令“压缩源文件”

算法系列(二):贪心算法--Huffman编码_第12张图片

将压缩的文件命名为“coding.txt”或者其他什么名字都行,压缩后打开看看压缩后的文件:                                                    

                                              算法系列(二):贪心算法--Huffman编码_第13张图片     

       乱码!很好,正是我想要的结果,我们再看看文件的大小

            算法系列(二):贪心算法--Huffman编码_第14张图片               算法系列(二):贪心算法--Huffman编码_第15张图片

        我们可以看到源文件有1653个字节,而压缩后的文件是1156个字节,1156/1653=0.699,也就是将源文件压缩到了69.9%,压缩率还是很高的。是不是即实现了文件加密又实现了文件压缩呢!接下来用 FileViewPro 软件打开压缩后的文件看看(FileViewPro是一款很好的文件查看软件,能打开几百种格式的软件,推荐给大家):

         算法系列(二):贪心算法--Huffman编码_第16张图片

   前三个字节"000043"表示总共的字符类型数,即该文件有67种字符;接下来"e4b8ad"三个字节是"中"的UTF-8码,接下来"e4bbac"三个字节是"们"的UTF-8码,e8af95->"试",接下来依次是各个字符的UTF-8码,共有67个;在地址 00000070 处第5第6第7个字节"0adf00"表示"中"的HuffmanCode的长度为0a,即长度为10,所以"中"的HuffmanCode为1101111100(0xdf00=1101111100000000),依次类推;在地址 00000110 处最后两个字节和下一地址的头两个字节"0000065b"表示文件总共有1627个字符,接下来就是各个字符的HuttmanCode了,比如"c73cc398ee"(1100 0111 0011 1100 1100 0011 1001 1000 1110 1110):11000111001表示'我',11100110000表示'们',11100110001表示'加',依次类推。

4.点击菜单命令“打开压缩文件”,然后我们看到压缩文件被解析,程序面板打印相应的HuffmanTree和编码表,我就不上图了。最后点击菜单命令保存解压缩文件,我将解压缩后的文件保存为“decoding.txt”,最后打开decoding.txt 看看

        算法系列(二):贪心算法--Huffman编码_第17张图片

   完美!解码后的文件一字不差,因为我们也知道采用Huffman编码压缩文件是无损压缩的,自然一字不差!

   最后,附上程序源码:http://download.csdn.net/detail/qq_22145801/9714895。

   参考:《计算机算法设计与分析》 电子工业出版社 王晓东编著




你可能感兴趣的:(算法)