哈夫曼树(Huffman Tree)及哈夫曼编码(Huffman Coding)

本文介绍了Huffman树的概念,如何通过构造算法生成最优二叉树,并详细阐述了哈夫曼编码在数据压缩中的应用。通过实例演示了如何使用Huffman编码对字符进行可变长度编码,以及其实现步骤和压缩效果。

目录

一、Huffman树(最优二叉树)

1、定义

2、构造

构造哈夫曼树的算法

哈夫曼树特点

二、Huffman编码


一、Huffman树(最优二叉树)

1、定义

        树的带路径长度,就是树中所有的叶节点的权值乘上其到根节点的路径长度。

        在含有n 个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树, 也称最优二叉树。如图,c树的WPL=35最小,经验证其为哈夫曼树。

2、构造

构造哈夫曼树的算法

(给定n 个权值分别为wi的结点)

1)将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。

2)构造一个新结点,从 F 中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。

3)从 F 中删除刚才选出的两棵树,同时将新得到的树加入F中。

4)重复步骤2)和 3), 直至F中只剩下一棵树为止。

//哈夫曼
class Node implements Comparable<Node> {
    Byte data;
    int weight;
    Node left;
    Node right;

    public Node(Byte data, int weight) {
        this.data = data;
        this.weight = weight;
    }

    @Override
    public int compareTo(Node o) {
        return this.weight - o.weight;
    }

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

    //遍历
    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.preOrder();
        }
        if (this.right != null) {
            this.right.preOrder();
        }
    }
}
private static Node createHuffmanTree(List<Node> nodes) {
    /**
         *@MethodName createHuffmanTree
         *@Description TODO 创建哈夫曼树
         *@Author SSRS
         *@Date 2020-10-31 13:21
         *@Param [nodes]
         *@ReturnType Java_note.Algorithm.HuffmanCode.Node
         */
    while (nodes.size() > 1) {
        //排序
        Collections.sort(nodes);
        //取最小两个二叉树
        Node leftNode = nodes.get(0);
        Node rightNode = nodes.get(1);
        //创建新的二叉树(他的根节点没有data,只有权值)
        Node parent = new Node(null, leftNode.weight + rightNode.weight);
        parent.left = leftNode;
        parent.right = rightNode;
        //删除已用结点
        nodes.remove(leftNode);
        nodes.remove(rightNode);
        //加入新节点
        nodes.add(parent);
    }
    return nodes.get(0);
}

哈夫曼树特点

1)每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大

2)构造过程中共新建了n-1个结点 (双分支结点),因此哈夫曼树的结点总数为2n- 1 。

3)每次构造都选择 2 棵树作为新结点的孩子,因此哈夫曼树中不存在度为1 的结点

例如,权值{7, 5, 2, 4}的哈夫曼树的构造过程如图所示。

二、Huffman编码

        在数据通信中,若对每个字符用相等长度的二进制位表示,称这种编码方式为固定长度编码。 若允许对不同字符用不等长的二进制位表示,则这种编码方式称为可变长度编码

        可变长度编码 比固定长度编码要好得多,其特点是对频率高的字符赋以短编码,而对频率较低的字符则赋以较长一些的编码,从而可以使字符的平均编码长度减短,起到压缩数据的效果。哈夫曼编码是一种被广泛应用而且非常有效的数据压缩编码

        若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。哈夫曼编码是前缀编码。

 

class HuffmanCode {
    public static void main(String[] args) {
        String str = "I like like like java Do you like a java";
        byte[] strBytes = str.getBytes();
        System.out.println(str + "的原长度是" + strBytes.length);
        HuffmanCode huffmanCode = new HuffmanCode();
        System.out.println("转成哈夫曼编码为:");
        String huffmanstr = huffmanCode.createHuffmanCode(strBytes);
        System.out.println(huffmanstr);
        System.out.println("其长度为:" + huffmanstr.getBytes().length);
        byte[] bytes = huffmanCode.zipbytes(strBytes);
        System.out.println("压缩后的结果是:" + Arrays.toString(bytes));
        System.out.println("其长度为:" + bytes.length);
        System.out.println("解压缩:" + new String(huffmanCode.rezip(bytes)));

    }

    private static List<Node> getNodes(byte[] bytes) {
        /**
         *@MethodName getNodes
         *@Description TODO 获得构建哈夫曼树的Node
         *@Author SSRS
         *@Date 2020-10-31 13:07
         *@Param [bytes]
         *@ReturnType java.util.List<Java_note.Algorithm.HuffmanCode.Node>
         */
        ArrayList<Node> nodes = new ArrayList<Node>();//要返回的nodes集合
        Map<Byte, Integer> counts = new HashMap<>();
        //统计每一个byte出现的次数
        for (byte b : bytes) {
            Integer count = counts.get(b);
            if (count == null) {
                counts.put(b, 1);
            } else {
                counts.put(b, count + 1);
            }
        }
        //把每一个键值对转换成Node对象,并加入nodes集合
        for (Map.Entry<Byte, Integer> each : counts.entrySet()) {
            nodes.add(new Node(each.getKey(), each.getValue()));
        }
        return nodes;
    }

    private static Node createHuffmanTree(List<Node> nodes) {
        /**
         *@MethodName createHuffmanTree
         *@Description TODO 创建哈夫曼树
         *@Author SSRS
         *@Date 2020-10-31 13:21
         *@Param [nodes]
         *@ReturnType Java_note.Algorithm.HuffmanCode.Node
         */
        while (nodes.size() > 1) {
            //排序
            Collections.sort(nodes);
            //取最小两个二叉树
            Node leftNode = nodes.get(0);
            Node rightNode = nodes.get(1);
            //创建新的二叉树(他的根节点没有data,只有权值)
            Node parent = new Node(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;
            //删除已用结点
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //加入新节点
            nodes.add(parent);
        }
        return nodes.get(0);
    }

    static Map<Byte, String> codes = new HashMap<Byte, String>();

    private static Map<Byte, String> getCodes(Map<Byte, String> codes, Node node, String code, StringBuilder stringBuilder) {
        StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);
        stringBuilder1.append(code);
        if (node != null) {
            if (node.data == null) {
                getCodes(codes, node.left, "0", stringBuilder1);
                getCodes(codes, node.right, "1", stringBuilder1);
            } else {
                codes.put(node.data, stringBuilder1.toString());
            }
        }
        return codes;
    }

    private static byte[] bytesArrays(String string) {
        int len;//bytes数组长度
        if (string.length() % 8 == 0) {
            len = string.length() / 8;
        } else {
            len = string.length() / 8 + 1;
        }
        int index = 0;//bytes角标
        byte[] huffmanCodeBytes = new byte[len];
        for (int i = 0; i < string.length(); i += 8) {
            String strTemp;
            if (i + 8 > string.length()) {
                strTemp = string.substring(i);
            } else {
                strTemp = string.substring(i, i + 8);
            }
            //转换
            huffmanCodeBytes[index] = (byte) Integer.parseInt(strTemp, 2);
            index++;
        }
        return huffmanCodeBytes;
    }

    public static String createHuffmanCode(byte[] bytes) {
        /**
         *@MethodName createHuffmanCode
         *@Description TODO 得到哈夫曼转码
         *@Author SSRS
         *@Date 2020-10-31 20:15
         *@Param [bytes]
         *@ReturnType java.lang.String
         */
        Node root = createHuffmanTree(getNodes(bytes));

        StringBuilder stringBuilder = new StringBuilder();

        getCodes(codes, root, "", stringBuilder);

        String str = "";
        for (Byte each : bytes) {
            str += codes.get(each);
        }
        return str;
    }

    public byte[] zipbytes(byte[] bytes) {
        return bytesArrays(createHuffmanCode(bytes));
    }

    //解压缩
    private String byteToBitString(boolean flag, byte b) {
        /**
         *@MethodName byteToBitString
         *@Description TODO 将一个byte转成二进制字符串
         *@Author SSRS
         *@Date 2020-10-31 20:56
         *@Param [flag 正数为true要补高位, b]
         *@ReturnType java.lang.String
         */
        int temp = b;//转int
        //如果是正数还要补高位
        if (flag) {
            temp |= 256;//按位与 256 是 1 0000 0000 | 0000 0001=》1 0000 0001
        }
        String str = Integer.toBinaryString(temp);//返回的是二进制的补码

        if (flag) {
            return str.substring(str.length() - 8);
        } else {
            return str;
        }

    }

    public byte[] rezip(byte[] huffmanbytes) {
        StringBuilder stringBuilder = new StringBuilder();//储存二进制字符串
        //全部转成二进制字符串
        for (int i = 0; i < huffmanbytes.length; i++) {
            //如果是最后一个可能不满8位就转码的那个就不用补高位无论正负,因此要判断
            boolean flag = (i == huffmanbytes.length - 1);
            stringBuilder.append(byteToBitString(!flag, huffmanbytes[i]));
        }

        //把哈夫曼编码表反向
        Map<String, Byte> map = new HashMap<String, Byte>();
        for (Map.Entry<Byte, String> each : codes.entrySet()) {
            map.put(each.getValue(), each.getKey());
        }

        List<Byte> list = new ArrayList();//解压缩后的语句的储存位置
        for (int i = 0; i < stringBuilder.length(); ) {
            int count = 1;//计数,计每一个字符的二进制字符位数
            boolean flag = true;
            Byte b = null;

            //找字符对应哈夫曼编码
            while (flag) {
                String key = stringBuilder.substring(i, i + count);
                b = map.get(key);
                if (b == null) {
                    count++;
                } else {//匹配成功
                    flag = false;
                }
            }
            list.add(b);
            i = i + count;
        }
        byte b[] = new byte[list.size()];
        for (int i = 0; i < b.length; i++) {
            b[i] = list.get(i);
        }
        return b;
    }

    public void preOrder(Node root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("二叉树为空。");
        }
    }
}

 

 本文所有概念性描述及图示均来自王道考研数据结构一书

### Huffman 编码算法的实现与原理 #### 1. Huffman 编码的核心概念 Huffman 编码是一种基于概率统计的无损数据压缩技术,其核心思想是通过对高频字符分配较短的编码,而对低频字符分配较长的编码,从而减少整体编码长度。这种方法能够显著降低数据存储需求并提高传输效率[^1]。 #### 2. Huffman 编码的工作流程 以下是 Huffman 编码的主要工作阶段: - **构建频率表** 首先分析输入数据集中各个字符出现的次数或频率,并将其记录下来作为后续建的基础[^4]。 - **创建霍夫曼 (Huffman Tree)** 使用优先队列(最小堆)按字符频率从小到大排列节点。每次取出两个频率最低的节点组合成一个新的父节点,新节点的频率等于子节点之和。重复此过程直到只剩下一个根节点为止[^2]。 - **生成编码字典** 自顶向下遍历霍夫曼,在每条路径上标记左分支为 `0` 和右分支为 `1`。最终到达叶子结点时所形成的二进制序列即为此字符对应的唯一前缀码[^3]。 - **数据压缩与解压** 利用上述生成好的编码规则替换原文档中的每一个原始符号完成压缩;相反方向则是依据同样的结构重新解析回原样内容实施解码操作。 #### 3. C语言实现示例 下面给出一段简单的C程序演示如何运用该方法来进行基本的文字串压缩处理: ```c #include <stdio.h> #include <stdlib.h> typedef struct Node { char ch; int freq; struct Node *left, *right; } Node; // 创建新的节点函数定义省略... void buildTree(Node **root){ // 构造霍夫曼逻辑... } void generateCodes(Node* root, int arr[], int top){ // 生产具体编码方案递归调用细节忽略.. } int main(){ // 初始化变量及读取输入字符串部分跳过.. buildTree(&root); generateCodes(root, codeArr, 0); return 0; } ``` 以上仅为框架示意,实际开发需补充完整功能模块才能正常使用。 #### 4. 应用场景举例 除了常规文本文件外,Huffman 编码还广泛应用于多媒体领域比如JPEG图片标准之中,用来优化颜色采样后的量化系数表达形式以进一步缩减体积而不失真太多。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SS上善

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值