数据结构与算法之霍夫曼树

1、霍夫曼编码

计算机里每个字符在没有压缩的文本文件中都由一个字节(如ASCII码)或两个字节(如Unicode码)表示。这些方案中,每个字符需要相同的位数。

字母对应的ASCII码类似这样:

字母 十进制 二进制
A 65 01000000
B 66 01000001
C 67 01000001
... ... ...
X 88 01001100
Y 89 01001100
Z 90 01011010

在英文中,字母E的使用频率最高,而字母Z的使用频率最低,但是,无论使用频率高低,一律使用相同位数的编码来存储,是不是有些浪费空间呢?试想,如果我们对使用频率高的字母用较少的位数来存储,而对使用频率低的字母用较多的位数来存储,会大大提升存储效率。

霍夫曼编码就是根据以上的设想来处理数据压缩的。

例如,我们要发送一句消息:

SUSIE SAYS IT IS EASY

统计所有符号出现的频率:

符号 频次
换行符 1
T 1
U 1
A 2
E 2
Y 2
I 3
空格 4
S 6

S出现的频率最多,我们可以将它的编码设为10,其次是空格,我们将它设置为00,接下来的编码必须增加位数了。

三位码我们有八种选择:000、001、010、011、100、101、110、111。

但是,以10或00开头的是不能使用的,只能从010,011,110,111中选择。因为如果我们用101表示I,用010表示Y,101010既可以分解为101、010两个有意义的编码,也能分解为10,10,10三个有意义的编码,这显然是不允许的。

我们必须保证,任何信息编码之后,解码时都不会出现歧义。借助霍夫曼树就能便捷的实现编码。

2、霍夫曼树

霍夫曼树是最优二叉树

树的带权路径长度规定为所有叶子结点的带权路径长度之和,带权路径长度最短的树,即为最优二叉树

在最优二叉树中,权值较大的结点离根较近。

首先就需要构建一个霍夫曼树,一般利用优先级队列来够贱霍夫曼树。

以上面的消息为例,我们先来分析一下构建霍夫曼树的过程,下图中,if代表换行符,sp代表空格。

数据结构与算法之霍夫曼树_第1张图片
1.png

首先,将字符按照频率插入一个优先级队列,频率越低越靠近队头,然后循环执行下面的操作:

  • 取出队头的两个树
  • 以它们为左右子节点构建一棵新树,新树的权值是两者之和
  • 将这棵新树插入队列

直到队列中只有一棵树时,这棵树就是我们需要的霍夫曼树。
接下来我们用java来实现上面的过程。

节点类:

public class Node {
    private String key;         //树节点存储的关键字,如果是非叶子节点为空
    private int frequency;      //关键字词频
    private Node left;      //左子节点
    private Node right;     //右子节点
    private Node next;      //优先级队列中指向下一个节点的引用
    
    public Node(int fre,String str){  //构造方法1
        frequency = fre;
        key = str;
    }
    public Node(int fre){   //构造方法2
        frequency = fre;
    }
    
    public String getKey() {
        return key;
    }
    public void setKey(String key) {
        this.key = key;
    }
    public Node getLeft() {
        return left;
    }
    public void setLeft(Node left) {
        this.left = left;
    }
    public Node getRight() {
        return right;
    }
    public void setRight(Node right) {
        this.right = right;
    }
    public Node getNext() {
        return next;
    }
    public void setNext(Node next) {
        this.next = next;
    }
    public int getFrequency() {
        return frequency;
    }
    public void setFrequency(int frequency) {
        this.frequency = frequency;
    }
}

用于辅助创建霍夫曼树的优先级队列:

public class PriorityQueue {
    private Node first;
    private int length;
    
    public PriorityQueue(){
        length = 0;
        first = null;
    }
    
    //插入节点
    public void insert(Node node){
        if(first == null){  //队列为空
            first = node;
        }else{
            Node cur = first;
            Node previous = null;
            while(cur.getFrequency() < node.getFrequency()){  
                //定位要插入位置的前一个节点和后一个节点
                previous = cur;
                if(cur.getNext() == null){  //已到达队尾
                    cur = null;
                    break;
                }else{
                    cur = cur.getNext();
                }
                
            }
            if(previous == null){  //要插入第一个节点之前
                node.setNext(first);
                first = node;
            }else if(cur == null){  //要插入最后一个节点之后
                previous.setNext(node);
            }else{  //插入到两个节点之间
                previous.setNext(node);
                node.setNext(cur);
            }
        }
        length++;
    }
    
    //删除队头元素
    public Node delete(){
        Node temp = first;
        first = first.getNext();
        length--;
        return temp;
    }
    
    //获取队列长度
    public int getLength(){
        return length;
    }
    
    //按顺序打印队列
    public void display(){
        Node cur = first;
        System.out.print("优先级队列:\t");
        while(cur != null){
            System.out.print(cur.getKey()+":"+cur.getFrequency()+"\t");
            cur = cur.getNext();
        }
        System.out.println();
    }
    
    //构造霍夫曼树
    public HuffmanTree buildHuffmanTree(){
        while(length > 1){
            Node hLeft = delete();  //取出队列的第一个节点作为新节点的左子节点
            Node hRight = delete(); //取出队列的第二个节点作为新节点的右子节点
            //新节点的权值等于左右子节点的权值之和
            Node hRoot = new Node(hLeft.getFrequency()+hRight.getFrequency());
            hRoot.setLeft(hLeft);
            hRoot.setRight(hRight);
            insert(hRoot);
        }
        //最后队列中只剩一个节点,即为霍夫曼树的根节点
        return new HuffmanTree(first);
    }
    
}

霍夫曼树类:

import java.util.HashMap;
import java.util.Map;


public class HuffmanTree {
    private Node root;
    private Map codeSet = new HashMap();  //该霍夫曼树对应的字符编码集
    
    public HuffmanTree(Node root){
        this.root = root;
        buildCodeSet(root,"");  //初始化编码集
    }
    
    //生成编码集的私有方法,运用了迭代的思想
    //参数currentNode表示当前节点,参数currentCode代表当前节点对应的代码
    private void buildCodeSet(Node currentNode,String currentCode){
        if(currentNode.getKey() != null){
            //霍夫曼树中,如果当前节点包含关键字,则该节点肯定是叶子节点,将该关键字和代码放入代码集
            codeSet.put(currentNode.getKey(), currentCode);
        }else{//如果不是叶子节点,必定同时包含左右子节点,这种节点没有对应关键字
            //转向左子节点需要将当前代码追加0
            buildCodeSet(currentNode.getLeft(),currentCode+"0");
            //转向右子节点需要将当前代码追加1
            buildCodeSet(currentNode.getRight(),currentCode+"1");
        }
    }
    
    //获取编码集
    public Map getCodeSet(){
        return codeSet;
    }
    
}

测试类:

public static void main(String[] args) throws Exception{
        PriorityQueue queue = new PriorityQueue();
        Node n1 = new Node(1,"if");
        Node n2 = new Node(1,"U");
        Node n3 = new Node(1,"T");
        Node n4 = new Node(2,"Y");
        Node n5 = new Node(2,"E");
        Node n6 = new Node(2,"A");
        Node n7 = new Node(3,"I");
        Node n8 = new Node(4,"sp");
        Node n9 = new Node(5,"S");
        queue.insert(n3);
        queue.insert(n2);
        queue.insert(n1);
        queue.insert(n6);
        queue.insert(n5);
        queue.insert(n4);
        queue.insert(n7);
        queue.insert(n8);
        queue.insert(n9);
        queue.display();
        
        HuffmanTree tree = queue.buildHuffmanTree();
        Map map = tree.getCodeSet();
        Iterator it = map.entrySet().iterator();
        System.out.println("霍夫曼编码结果:");
        while(it.hasNext()){
            Entry entry = (Entry)it.next();
            System.out.println(entry.getKey()+"——>"+entry.getValue());
        }
    }

控制台打印结果如下图所示:

数据结构与算法之霍夫曼树_第2张图片
2.png

可见,达到了预期的效果。

3、更进一步

秉承没有最好,只有更好的精神,我们不应该就此止步。现在我们做到的只是得到某个字符对应的霍夫曼编码,但是这个字符的词频仍然需要我们手工去统计、输入,更深入的思考一下,能不能更智能化一点,只要我们输入完整的一段消息,就能给出对应的霍夫曼编码,而且能对编码进行解码呢?

就是说,我们要利用上面已经写出的代码来封装一个编码类,这个类的一个方法接受一个字符串消息,返回霍夫曼编码。此外,还有一个解码类,接受一段完整的霍夫曼编码,返回解码后的消息内容。这实际上就是压缩与解压的模拟过程。

霍夫曼编码器:

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;


public class HuffmanEncoder {
    private PriorityQueue queue;    //辅助建立霍夫曼树的优先级队列
    private HuffmanTree tree;       //霍夫曼树
    private String [] message;      //以数组的形式存储消息文本
    private Map keyMap;             //存储字符以及词频的对应关系
    private Map codeSet;            //存储字符以及代码的对应关系
    
    public HuffmanEncoder(){
        queue = new PriorityQueue();
        keyMap = new HashMap();
    }
    
    //获取指定字符串的霍夫曼编码
    public String encode(String msg){
        resolveMassage(msg);
        buildCodeSet();
        String code = "";
        for(int i=0;i"+entry.getValue());
        }
        System.out.println();
    }
    
    //获取该段消息的代码集
    public Map getCodeSet(){
        return codeSet;
    }
}

霍夫曼解码器:

import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;


public class HuffmanDecoder {
    private Map codeSet;  //代码段对应的代码集
    
    public HuffmanDecoder(Map map){
        codeSet = map;
    }
    
    //将代码段解析成消息文本
    public String decode(String code){
        String message = "";
        String key = "";
        char [] chars = code.toCharArray();
        for(int i=0;i

测试类:

public static void main(String[] args){
        
        String message = "chen long fei is hero !";
        HuffmanEncoder encoder = new HuffmanEncoder();
        String code = encoder.encode(message);
        
        encoder.printCodeSet();
        System.out.print("编码结果:");
        System.out.println(code);
        
        HuffmanDecoder decoder = new HuffmanDecoder(encoder.getCodeSet());
        String message2 = decoder.decode(code);
        System.out.print("解码结果:");
        System.out.println(message);
    }

运行结果如下图所示:

数据结构与算法之霍夫曼树_第3张图片
3.png

你可能感兴趣的:(数据结构与算法之霍夫曼树)