哈夫曼(Huffman)树创建及其带权路径长度(WPL)、哈夫曼编码、哈夫曼解码

  • 哈夫曼(Huffman)树创建及其带权路径长度(WPL)、哈夫曼编码、哈夫曼解码
package ccnu.offer.tree;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Stack;

// HuffmanNode中的data表示这个节点存储的数据,而weight则表示该节点的权值(表示赋予该节点的某种意义,如对该节点访问的频度等)
// 任意一棵树中任意节点的带权路径长度为该节点的权值与该节点到该树根节点的路径长度(经过的边数)的乘积
// 任意一棵树的带权路径长度为这棵树中所有叶子结点的带权路径长度之和
// 在含有n个带权叶子节点的二叉树中,其中带权路径长度(WPL)最小的二叉树为哈夫曼树(Huffman tree),也称为最优二叉树

// 哈夫曼算法:
// 第一步:将n个带权叶子结点分别看作为n棵仅含有一个节点的二叉树,这n棵树构成森林F
// 第二步:构造一个新节点,从森林F中选出权值最小的两棵树作为新节点的左右子树,并且将新节点的权值置为左右子树根节点的权值之和
// 第三步:将第二步中选取的两棵树从森林F中删除,并且将新节点为根的树加入到森林F中
// 第四步:重复第二、三步,直到森林F中只有一棵树为止

// 我们从这个构造过程中可以看出Huffman树有如下几个特点:
// 1、每个初始节点最终将成为哈夫曼树的叶节点,并且权值越小的叶子结点到根节点的路径长度越大
// 2、整个构造过程会额外产生n-1个新的节点(均为双分支节点),因此哈夫曼树的总的节点个数是n+(n-1)=2*n-1个
// 3、由于哈夫曼树的构造过程中总是从当前的森林中选择两个权值最小的两个根节点树作为新节点的左右子树,所以哈夫曼树中不存在度为1的节点

public class Demo06 {
    public static void test1(){
        List nodes = new ArrayList();
        nodes.add(new HuffmanNode("A", 40.0));
        nodes.add(new HuffmanNode("B", 8.0));
        nodes.add(new HuffmanNode("C", 10.0));
        nodes.add(new HuffmanNode("D", 30.0));
        nodes.add(new HuffmanNode("E", 10.0));
        nodes.add(new HuffmanNode("F", 2.0));
        HuffmanNode root = createHuffmanTree(nodes);
        inOrder(root);
        System.out.println();
        System.out.println("WPL=" + getTreeWPL(root));
        System.out.println(getLeavesPL(root, 0));
        HashMap h = getCharactersHuffmanEncoding(root);
        Set> entrys = h.entrySet();
        for(Entry entry : entrys){
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        System.out.println(textToNodesList("C:\\Users\\" + System.getenv("username") + "\\Desktop\\huffmanIn.txt"));
    }

    public static void test2(){
        String src = "C:\\Users\\" + System.getenv("username") + "\\Desktop\\huffmanIn.txt"; // 待编码的文本
        String dest = "C:\\Users\\" + System.getenv("username") + "\\Desktop\\huffmanOut.txt"; // 保存经过哈夫曼编码的文本
        String dest2 = "C:\\Users\\" + System.getenv("username") + "\\Desktop\\copyOfHuffmanIn.txt"; // 用来保存对文本哈夫曼编码后进行哈夫曼解码的文本
        HuffmanNode root = createHuffmanTree(textToNodesList(src));
        System.out.println("WPL=" + getTreeWPL(root));
    /*  inOrder(root);
        for(Entry entry : getCharactersHuffmanEncoding(root).entrySet()){
            System.out.println(entry.getKey() + "-->" + entry.getValue());
        }*/
        getTextHuffmanEncoding(src, dest);
        getTextHuffmanDecoding(getCharactersHuffmanEncoding(root), dest, dest2);
    }

    public static void main(String[] args) {
        test1();
        test2();
        String src = "C:\\Users\\" + System.getenv("username") + "\\Desktop\\huffmanIn.txt"; // 待编码的文本
        System.out.println(textToNodesList(src));
    }

    // 创建一棵哈夫曼树
    public static HuffmanNode createHuffmanTree(List f) { // f为当前森林
        if (f == null) {
            return null;
        }
        if (f.size() == 0) { // 没有元素
            return null; // 返回一个空哈夫曼树
        }
        while (f.size() > 1) {
            f.sort(null); // 将森林f按照权值进行升序排序
            // 取出当前森林中权值最小的两个节点
            HuffmanNode l = f.get(0);
            HuffmanNode r = f.get(1);
            HuffmanNode newNode = new HuffmanNode(l.data + r.data, l.weight + r.weight, l, r); // 新节点的数据令其为两个子树根节点的数据域的合并
            // 移除刚取出的两个节点
            f.remove(0);
            f.remove(0);
            f.add(newNode);
        }
        return f.get(0); // 森林中仅存的一棵树的根节点
    }

    // 得到解码器(字符与其哈夫曼编码的对应表)
    public static HashMap getCharactersHuffmanEncoding(HuffmanNode root){
        return getCharactersHuffmanEncoding(root, "");
    }

    // 返回Huffman Tree中所有叶节点的哈夫曼编码(一种前缀编码:所有字符编码中没有一个编码是其他字符编码的前缀)
    private static HashMap getCharactersHuffmanEncoding(HuffmanNode root, String encoding){
        Map characterEncodingResultSet = new HashMap();
        if(root == null){
            return null;
        }
        if(root.lchild == null && root.rchild == null){ // 如果为叶节点,则将它的编码放入Map中
            characterEncodingResultSet.put(root.data, encoding);
        }else{ // 此处规定转向左孩子为0,转向右孩子为1
            characterEncodingResultSet.putAll(getCharactersHuffmanEncoding(root.lchild, encoding + "0")); // 将root的左子树上的所有叶节点放入Map中,由于当前root节点的编码为encoding,因此其左子树根节点的编码为encoding+"0"
            characterEncodingResultSet.putAll(getCharactersHuffmanEncoding(root.rchild, encoding + "1")); // 将root的右子树上的所有叶节点放入Map中,由于当前root节点的编码为encoding,因此其右子树根节点的编码为encoding+"1"
        }
        return (HashMap)characterEncodingResultSet;
    }

    // 将指定文本中的每一个不同字符封装为HuffmanNode(note:当一行文本结束后到下一行时的行末包含两个字符('\r'和'\n'))
    public static ArrayList textToNodesList(String pathname) {
        if (pathname == null || pathname.length() == 0) {
            return null;
        }
        ArrayList huffmanNodes = new ArrayList();
        HashMap dwMap = new HashMap(); // 每一个data与weight的对应表
        BufferedReader br = null;
        try {
            br = new BufferedReader(new InputStreamReader(new FileInputStream(new File(pathname))));
            char ch;
            while (true) {
                int i = br.read(); // 读取一个字符
                if (i == -1) { // -1 if the end of the stream has been reached,but not convert to character(greater than or equal to zero)
                    break;
                } else {
                    ch = (char) i;
                }
                double count = 1;
                if (dwMap.containsKey(String.valueOf(ch))) {
                    count = dwMap.get(String.valueOf(ch)) + 1; // 此字符已存在,那么它的的value增加1
                }
                dwMap.put(String.valueOf(ch), count);
            }
            br.close();
        } catch (FileNotFoundException e1) {
            e1.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        Set> entrys = dwMap.entrySet();
        for (Entry entry : entrys) { // 将每一个字符<-->字符频数(data<-->weight)封装为HuffmanNode
            huffmanNodes.add(new HuffmanNode(entry.getKey(), entry.getValue()));
        }
        return huffmanNodes;
    }

    // 将指定的文本testSrc通过哈夫曼编码到指定的文件testDest中
    public static void getTextHuffmanEncoding(String textSrc, String textDest) {
        if (textSrc == null || textDest == null) {
            return;
        }
        ArrayList huffmanNodes = textToNodesList(textSrc); // 得到指定文本中的所有不同的字符封装的节点列表
        HuffmanNode root = createHuffmanTree(huffmanNodes); // 根据提供的节点集创建哈夫曼树
        HashMap characterEncodingResultSet = getCharactersHuffmanEncoding(root); // 得到所有不同的字符的哈夫曼编码
        BufferedReader br = null;
        BufferedWriter bw = null;
        try {
            br = new BufferedReader(new InputStreamReader(new FileInputStream(new File(textSrc))));
            bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(textDest))));
            char ch;
            while (true) {
                int i = br.read();
                if (i == -1) {
                    break;
                } else {
                    ch = (char) i;
                }
                String characterEncoding = characterEncodingResultSet.get(String.valueOf(ch)); // 获取当前字符ch的哈夫曼编码
                bw.write(characterEncoding);
            }
            bw.flush();
            bw.close();
            br.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 将指定的编码文本testSrc通过哈夫曼解码到指定的文件testDest中
    public static void getTextHuffmanDecoding(Map decoder, String textSrc, String textDest){
        if(textSrc == null || textDest == null){
            return;
        }
        BufferedReader br = null;
        BufferedWriter bw = null;
        String tmp = "";
        try {
            br = new BufferedReader(new InputStreamReader(new FileInputStream(new File(textSrc))));
            bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(textDest))));
            while(true){
                int i = br.read();
                if(-1 == i){
                    break;
                }else{
                    tmp += (char)i;
                }
                // 只要当前的哈夫曼编码子串是某个字符的哈夫曼编码,那么就应该匹配成功(最短匹配),接着找到这个编码所对应的
                // 字符即可(理由:因为哈夫曼编码为前缀编码,任意字符的编码不会是其他字符的前缀,所以当找到某个编码str是某
                // 个字符的哈夫曼编码时,后面的编码str+str1(str1不为空)不可能是其他字符的哈夫曼编码)
                if(decoder.containsValue(tmp)){
                    String data = getKeysByValue(decoder, tmp)[0]; // 只会有一个key,因为每一个不同字符都有不同的编码
                    if(data.equals("")){ // 第一次会读取空白字符'\r',向输出文件写入\n\r,紧接着必定读取空白字符'\n',再次向输出文件写入\n\r,那么此时在输出文件中会是\n\r\n\r(由于windows当且仅当出现\r\n时才会换行,中间两个空白字符刚好组成\r\n,所以只会换行一次)
                        bw.write("\n\r");
                    }else{
                        bw.write(data);
                    }
                    tmp = ""; // 当前字符编码清空,查找下一个字符编码
                }
            }
            bw.flush();
            bw.close();
            br.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    // 通过Map中的值得到Map中键的集合
     public static String[] getKeysByValue(Map map, String value) {
         StringBuffer sb = new StringBuffer();
         Set> entrys = map.entrySet();
         Iterator> iter = entrys.iterator();
         while(iter.hasNext()){
             Map.Entry entry = iter.next();
             String value2 = entry.getValue();
             if(value2 != null && value2.equals(value)){ // 此处没有将value为null的键获取
                 sb.append(entry.getKey() + " ");
             }
         }
         String keys = sb.toString().trim();
         return keys.split(" +");
     }

    public static void inOrder(HuffmanNode root){
        if(root != null){
            inOrder(root.lchild);
            if(root.lchild == null && root.rchild == null){ // 叶子结点
                System.out.print(root.data + " ");
            }
            inOrder(root.rchild);
        }
    }

    public static double getTreeWPL(HuffmanNode root){ // 返回树的带权路径长度(WPL)
        Stack nodes = new Stack();
        double WPL = 0;
        while(root != null || !nodes.isEmpty()){
            if(root != null){
                nodes.push(root);
                root = root.lchild;
            }else{
                root = nodes.peek(); // 读取栈顶节点
                if(root.tag){ // 如果栈顶节点已被访问
                    nodes.pop(); // 将这个节点移除
                    root = null; // 被访问的节点引用置空,以便继续读取栈顶节点,而不是再次操作这个已访问节点
                }else{ // 栈顶节点未被访问
                    root.tag = true;
                    if(root.lchild == null && root.rchild == null){
                        WPL += (nodes.size() - 1) * root.weight;
                    }
                    root = root.rchild;
                }
            }
        }
        return WPL;
    }

    // 得到所有叶子结点的路径长度(Path Length)
    public static ArrayList getLeavesPL(HuffmanNode root, int depth) {
        ArrayList pls = new ArrayList();
        if (root == null) {
            return null;
        }
        if (root.lchild == null && root.rchild == null) {
            pls.add(depth);
            return pls;
        } else {
            pls.addAll(getLeavesPL(root.lchild, depth + 1));
            pls.addAll(getLeavesPL(root.rchild, depth + 1));
        }
        return pls;
    }

    // 哈夫曼树节点结构
    private static class HuffmanNode implements Comparable {
        private String data; // 该节点存储的数据
        private double weight; // 该节点的权值
        private HuffmanNode lchild;
        private HuffmanNode rchild;
        private boolean tag; // 用于标记该节点是否被访问过

        public HuffmanNode(String data, double weight) {
            this(data, weight, null, null);
        }

        public HuffmanNode(String data, double weight, HuffmanNode lchild, HuffmanNode rchild){
            this.data = data;
            this.weight = weight;
            this.lchild = lchild;
            this.rchild = rchild;
        }

        @Override
        public int compareTo(HuffmanNode o) {
            if (o == null) {
                return 0;
            }
            if (this.weight > o.weight) {
                return 1;
            } else if (this.weight == o.weight) {
                return 0;
            } else {
                return -1;
            }
        }

        @Override
        public String toString() {
            return data + "=" + weight;
        }
    }
}

你可能感兴趣的:(数据结构)