数据结构与算法之二叉树(二)赫夫曼编码原理及实现

引言

上篇博客学习了二叉树的基本操作原理,今天我们在此基础上学习二叉树的典型应用:赫夫曼编码树,赫夫曼编码(Huffman Coding)是一种编码方法。它使用变长编码表对源符号(如文件中的一个字母)进行编码,出现机率高的字母使用较短的编码,反之出现机率低的则使用较长的编码,这便使编码之后的字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。JPG图片及文件压缩就是用的赫夫曼编码。赫夫曼编码的节点带有权重(一般指出现频次),而且编码的字符都是叶子节点,他们的父节点保存左右子节点的权重和。它的结构图如下:


赫夫曼编码树结构

从图中看到频次越高的元素越接近根节点,反之亦然。规定做路径代号0,右路径代号1,而各个字符的编码就是从根节点到叶子节点的路径代码,如:右图A的编码为1,D为01,J为001,左图H的编码为101等等。下面是赫夫曼树的实现。

赫夫曼树的构建

已上图中的左图为例子说明它的构建和编码流程:
1.字符节点按权重从小到大排序
2.遍历过程主要执行节点合并操作:
1>最小权重的两个节点合并为一个新的子树,它的权重为左右节点的权重和(如图中的EF节点合并为子树,根节点权重为4);
2>后面挂载新的元素的时候(如挂载B节点),比较新元素和子树的权重,前者大,则新元素节点为合并新子树的右边孩子(如上面左图中的B节点权重5比EF的根节点4大);
3.直到数组遍历完,都执行1>和2>流程
4.树构建完毕,从根节点到叶子节点的路径代码就是编码,每一层节点的编码都是基于它的父节点,因此可以通过递归实现,递归结束条件为遍历到叶子节点。

赫夫曼树数据结构描述

赫夫曼树节点元素:包含路径编码和权重,载体为前面用的二叉树节点BinaryNode.

/**
 * Created by chenming on 17/1/11.
 */
public class HuffmanModel implements Comparable<HuffmanModel>{
    public Integer weight;//权重
    public int data;//data
    public String code = "";//路径编码
    public HuffmanModel(int data){
        this.weight = this.data = data;//简化处理
    }
    @Override
    public int compareTo(HuffmanModel o) {
        return this.weight.compareTo(o.weight);
    }
}

赫夫曼树构建实现

 /**
     * 根据优先级列表创建赫夫曼二叉树:
     * 根据前两个节点,构造子树,然后与后面的节点合并成新的子树,挂载节点原则:
     * 1.从底至上构建,
     * 2.权重大的挂载右边,反之左边
     * 3.根节点的权重为左右权重的和
     *
     * @param priorityArr 元素权重递增的数组
     * @return 赫夫曼根节点
     */
    public static BinaryNode<HuffmanModel> createHuffmanTree(Integer[] priorityArr) {
        Stack<BinaryNode<HuffmanModel>> stack = new Stack<>();//这里仅做暂存合并树节点的容器
        BinaryNode<HuffmanModel> newRootNode = null;//新建节点,连接新节点和之前生成的子树
        if (priorityArr == null || priorityArr.length == 0) {
            return newRootNode;
        }

        int i = 0;
        //两两合并节点,构建子树入栈
        while (i < priorityArr.length) {
            //循环体做合并节点
            BinaryNode<HuffmanModel> newNode = new BinaryNode<>(new HuffmanModel(priorityArr[i]));//叶子节点
            if (i == 0) {//第一个节点,无法构建子树,直接入栈
                stack.push(newNode);
                i++;
                continue;
            }
            //每一次将上次构建的子树和新的节点合并成新子树入栈
            BinaryNode<HuffmanModel> tempRootNode = new BinaryNode<>(new HuffmanModel(0));//挂载叶子节点的新的根节点
            BinaryNode<HuffmanModel> currentSubTree = stack.pop();//目前已经合并的子树根节点
            //判断权重,新节点和子树节点合并
            if (newNode.data.weight > currentSubTree.data.weight) {
                tempRootNode.left = currentSubTree;
                tempRootNode.right = newNode;
            } else {
                tempRootNode.left = newNode;
                tempRootNode.right = currentSubTree;
            }

            /**
             * 更新权重,权重为节点左右
             */
            if (tempRootNode.left != null) {
                tempRootNode.data.weight += tempRootNode.left.data.weight;
            }

            if (tempRootNode.right != null) {
                tempRootNode.data.weight += tempRootNode.right.data.weight;
            }

            stack.push(tempRootNode);
            i++;
        }
        newRootNode = stack.pop();//最后取出合并完成的树
        return newRootNode;
    }

代码结合注释比较好理解,不在赘述。

赫夫曼树编码实现

从根节点到叶子节点的遍历,递归即可。

   /**
     * 赫夫曼编码
     * 每一次递归都基于父节点的code
     * @param node 赫夫曼树根节点
     * @param codeTable 存放编码结果,key为赫夫曼编码,value为元素值
     */
    public static void huffmanEncode(BinaryNode<HuffmanModel> node, HashMap<String, Integer> codeTable) {
        if (node.isLeaf()) {//到叶子节点了,递归结束
            codeTable.put(node.data.code, node.data.data);//赫夫曼数的编码数据都在叶子节点, 保存编码表
            return;
        }
        /**
         * 向左遍历
         */

        if (node.left != null) {
            StringBuilder sb = new StringBuilder();
            sb.append(node.data.code).append("0");
            node.left.data.code = sb.toString();//子节点编码
            huffmanEncode(node.left, codeTable);
        }

        /**
         * 向右遍历
         */
        if (node.right != null) {
            StringBuilder sb = new StringBuilder();
            sb.append(node.data.code).append("1");
            node.right.data.code = sb.toString();//子节点编码
            huffmanEncode(node.right, codeTable);
        }
    }

赫夫曼解码

根据路径编码串找到叶子节点,然后返回节点数据即可。

   /**
     * 赫夫曼解码
     *
     * @param code
     * @param node
     * @return
     */
    public static int huffmanDecode(BinaryNode<HuffmanModel> node, String code) {
        if (code == null || code.length() == 0) {
            return -1;
        }
        BinaryNode<HuffmanModel> p = node;
        char[] chars = code.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            int c = chars[i] - '0';
            //遍历路径判断
            if (p != null) {
                if (c == 0) {
                    p = p.left;
                } else if (c == 1) {
                    p = p.right;
                }
            } else {
                return -1;//没有查到
            }

        }
        if (p != null && p.isLeaf()) {//必须是叶子节点
            return p.data.data;
        }
        return -1;
    }

注意:遍历完成后,节点必须是叶子节点,因为中间层节点是不挂载元素的,只保存了权重。
测试代码

@Test
    public void testHuffman() {
        System.out.println("=====赫夫曼=====");
        Integer[] huffmanArr = {1, 2, 3, 4, 5};//简单起见,权重=编码数据,升序排列
        BinaryNode<HuffmanModel> huffmanTree = BinaryTree.createHuffmanTree(huffmanArr);
        HashMap<String, Integer> huffmanEncodeTable = new HashMap<>();
        //赫夫曼编码huffmanEncodeTable保存赫夫曼编码
        BinaryTree.huffmanEncode(huffmanTree, huffmanEncodeTable);
        System.out.println("=====赫夫曼编码输出=====");
        Iterator<Map.Entry<String, Integer>> iterator = huffmanEncodeTable.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, Integer> next = iterator.next();
            System.out.println(next.getKey() + "==" + next.getValue());
        }
        System.out.println("=====赫夫曼解码输出=====");

        iterator = huffmanEncodeTable.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, Integer> next = iterator.next();
            System.out.println(next.getKey() + "==" + BinaryTree.huffmanDecode(huffmanTree, next.getKey()));
        }
    }
}

完整代码地址:数据结构与算法学习JAVA描述GayHub地址
下一篇我们研究下二叉树另一重要分支:平衡二叉树。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容