Huffman 编码算法之 Java 实现

本文深入介绍了Huffman编码的原理及其Java实现。通过可变长度编码压缩数据,节省20%-90%的空间。文章详细解释了如何统计字符频率、构建完全二叉树,并实现了编码与解码的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Huffman编码介绍
Huffman编码 处理的是字符以及字符对应的二进制的编码配对问题,分为编码和解码,目的是压缩字符对应的二进制数据长度。我们知道字符存贮和传输的时候都是二进制的(计算机只认识0/1),那么就有字符与二进制之间的mapping关系。字符属于字符集(Charset), 字符需要通过编码(encode)为二进制进行存贮和传输,显示的时候需要解码(decode)回 字符,字符集与编码方法是一对多关系(Unicode可以用UTF-8,UTF-16等编码)。理解了字符集,编码以及解码,满天飞的乱码问题也就游刃而 解了。以英文字母小写a为例, ASCII编码中,十进制为97,二进制为01100001。ASCII的每一个字符都用8个Bit(1Byte)编码,假如有1000个字符要传输,那 么就要传输8000个Bit。问题来了,英文中字母e的使用频率为12.702%,而z为0.074%,前者是后者的100多倍,但是确使用相同位数的二 进制。可以做得更好,方法就是可变长度编码,指导原则就是频率高的用较短的位数编码,频率低的用较长位数编码。Huffman编码算法就是处理这样的问 题。

Huffman编码Java实现
Huffman编码算法主要用到的数据结构是完全二叉树(full binary tree)和优先级队列。后者用的是java.util.PriorityQueue,前者自己实现(都为内部类),代码如下:

package huffman;
/**
 * 构建完全二叉树(full binary tree)
 * @author fh
 *
 */
public class Tree {

    private Node root;

    public Node getRoot() {
        return root;
    }

    public void setRoot(Node root) {
        this.root = root;
    }

    static class Node implements Comparable<Node> {

        private String val = "";
        private int frequence = 0;
        private Node parent;
        private Node leftNode;
        private Node rightNode;

        @Override
        public int compareTo(Node n) {
            return this.frequence - n.frequence;
        }

        //是否是叶子节点
        public boolean isleaf(){
            return this.leftNode==null&&this.rightNode==null;
        }

        public boolean isRoot(){
            return this.parent==null;
        }

        public boolean isLeftChild(){
            return this.parent!=null&&this==this.parent.leftNode;
        }

        public String getVal() {
            return val;
        }

        public void setVal(String val) {
            this.val = val;
        }

        public int getFrequence() {
            return frequence;
        }

        public void setFrequence(int frequence) {
            this.frequence = frequence;
        }

        public Node getParent() {
            return parent;
        }

        public void setParent(Node parent) {
            this.parent = parent;
        }

        public Node getLeftNode() {
            return leftNode;
        }

        public void setLeftNode(Node leftNode) {
            this.leftNode = leftNode;
        }

        public Node getRightNode() {
            return rightNode;
        }

        public void setRightNode(Node rightNode) {
            this.rightNode = rightNode;
        }



    }
}
package huffman;

import huffman.Tree.Node;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.PriorityBlockingQueue;

public class Huffman {

    public static void main(String[] args) {
        String oriStr = "Huffman codes compress data very effectively: savings of 20% to 90% are typical, "
                + "depending on the characteristics of the data being compressed. 中华崛起";
        // 测试编码
        Map<Character, Integer> statistics = collect(oriStr.toCharArray());
        String encodedBinariStr = encode(oriStr, statistics);
        System.out.println(encodedBinariStr);
        String decodedBinariStr = decode(encodedBinariStr, statistics);
        System.out.println(decodedBinariStr);
    }

    /**
     * 1,统计数据:统计词频
     * 
     * @param charArray
     * @return
     */
    static Map<Character, Integer> collect(char[] charArray) {
        Map<Character, Integer> map = new HashMap<Character, Integer>();

        for (int i = 0; i < charArray.length; i++) {
            Character character = new Character(charArray[i]);
            if (map.containsKey(character)) {
                map.put(character, map.get(character) + 1);
            } else {
                map.put(character, 1);
            }
        }
        return map;
    }

    /**
     * 2,构建树:完全二叉树
     * 
     * 构建树是Huffman编码算法的核心步骤。思想是把所有的字符挂到一颗完全二叉树的叶子节点,任何一个非页子
     * 节点的左节点出现频率不大于右节点。算法为把统计信息转为Node存放到一个优先级队列里面,每一次从队列里面弹出两个最小频率的节点,构建一个新的父
     * Node(非叶子节点),
     * 字符内容刚弹出来的两个节点字符内容之和,频率也是它们的和,最开始的弹出来的作为左子节点,后面一个作为右子节点,并且把刚构建的父节点放到队列里面。
     * 重复以上的动作N-1次,N为不同字符的个数(每一次队列里面个数减1)。结束以上步骤,队列里面剩一个节点,弹出作为树的根节点。
     */
    static Tree buildTree(Map<Character, Integer> collect, List<Node> list) {
        Tree tree = new Tree();
        // 根据Node的构造函数及compartor判断优先级
        PriorityBlockingQueue<Node> priorityBlockingQueue = new PriorityBlockingQueue<>();

        Character[] keys = collect.keySet().toArray(new Character[0]);
        // 转换成节点,存储到队列、链表中
        for (Character c : keys) {
            Node node = new Node();
            node.setVal(c.toString());
            node.setFrequence(collect.get(c));

            priorityBlockingQueue.offer(node);
            list.add(node);
        }

        // 构建二叉树
        while (priorityBlockingQueue.size() != 1) {
            Node node1 = priorityBlockingQueue.poll();
            Node node2 = priorityBlockingQueue.poll();

            Node parentNode = new Node();
            parentNode.setVal(node1.getVal() + node2.getVal());
            parentNode
                    .setFrequence(node1.getFrequence() + node2.getFrequence());

            parentNode.setLeftNode(node1);
            parentNode.setRightNode(node2);
            node1.setParent(parentNode);
            node2.setParent(parentNode);

            priorityBlockingQueue.offer(parentNode);
        }
        tree.setRoot(priorityBlockingQueue.poll());
        return tree;
    }

    /**
     * 编码 :
     * 
     * 某个字符对应的编码为,从该字符所在的叶子节点向上搜索,如果该字符节点是父节点的左节点,编码字符之前加0,反之如果是右节点,加1,直到根节点。
     * 只要获取了字符和二进制码之间的mapping关系,编码就非常简单.
     * 
     * 给出字符串,输出二进制码010101010
     */

    static String encode(String input, Map<Character, Integer> collect) {
        if (input == null || input.length() == 0) {
            return "";
        }
        StringBuilder out = new StringBuilder();

        // 预处理
        char[] chars = input.toCharArray();
        List<Node> list = new ArrayList<>();
        Tree tree = buildTree(collect, list);

        // 生成密码本:a=0101,b=0011
        Map<Character, String> encodInfo = buildEncodingInfo(list);
        // 比对
        for (char c : chars) {
            out.append(encodInfo.get(c));
        }
        return out.toString();
    }

    /**
     * 生成密码本:a=0101,b=0011
     * 
     * @param list
     * @return
     */
    private static Map<Character, String> buildEncodingInfo(List<Node> list) {
        Map<Character, String> map = new HashMap<Character, String>();

        for (Node n : list) {
            Character character = n.getVal().toCharArray()[0];
            StringBuilder stringBuilder = new StringBuilder();
            do {
                if (n.isLeftChild()) {
                    stringBuilder.append("0");
                } else {
                    stringBuilder.append("1");
                }
                n = n.getParent();
            } while (n.getParent() != null);
            map.put(character, stringBuilder.reverse().toString());
        }
        return map;
    }

    /**
     * 解码
     * 因为Huffman编码算法能够保证任何的二进制码都不会是另外一个码的前缀,解码非常简单,依次取出二进制的每一位,从树根向下搜索,1向右,0向左
     * ,到了叶子节点(命中),退回根节点继续重复以上动作。
     */

    static String decode(String input, Map<Character, Integer> collect) {
        if (input == null || input.equals("")) {
            return "";
        }
        StringBuilder outBuilder = new StringBuilder();

        // 预处理
        char[] chars = input.toCharArray();
        List<Node> list = new ArrayList<>();
        Tree tree = buildTree(collect, list);
        int i = 0;
        while (i < chars.length) {
            Node root = tree.getRoot();
            while (!root.isleaf()) {
                if (chars[i] == '0') {
                    root = root.getLeftNode();
                } else {
                    root = root.getRightNode();
                }
                ++i;
            }
            outBuilder.append(root.getVal());
        }
        return outBuilder.toString();
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值