Huffman(哈夫曼)算法与编码的java简单实现

引言

这是我写的第二篇与离散数学有关的算法,希望能对大家有所帮助。下面我们就一起来认识一下什么是哈夫曼算法,哈夫曼算法有什么用。

Huffman(哈夫曼)算法

算法历史

1952年,David A. Huffman在麻省理工学院攻读博士时发表了《一种构建极小多余编码的方法》(A Method for the Construction of Minimum-Redundancy Codes)一文,它一般就叫做Huffman编码,这种算法有效地解决了在编码字符串时带来的存储空间浪费问题。

算法原理

huffman算法的编码是一种可变长度的编码算法。在解释过程中,我们可以假设有一个很长很长的字符串,其中只包含六个字符A1、A2、A3、A4、A5、A6。如果按照传统的固定长度编码,可以将其编译为下表所示:

A1A2A3A4A5A6
000001010011100101

以上就是固定长度的编码了,下面我们按照可变长度编码:

A1A2A3A4A5A6
0011011101010100

可以发现,按照这样进行编码,部分源符号在编码过程中会短一些。如果我们需要编码的符号长度为50万字,假设A1、A2出现的概率为30%、30%,其余字符出现的概率为10%。那么,按照固定长度编码则可能需要3*50=150万个0和1来存储这些字符。按照可变长度来存储的话就是50*0.6*2+50*0.4*3=120万。如此一来是不是就减少了许多。

但是如果按照可变长度编码符号的话,可能会出现一个潜在的问题,那就是编码的歧义。下面我们来看一段编码:

A1A2A3A4A5A6
011011001101100

假设这是一段可变编码,其中的符号对应着下方的编码,如果我们按照如此编码一段符号为01100。

其中不难发现,如果把这段编码的0-1-100如此分开,则解码为A1A2A4,但是情况却不仅仅如此。我们还可以将这段编码理解为0-1100,0-110-0。此时的结果就可能是A1A6或者A1A5A1,产生了很大的歧义,我们不希望如此,于是huffman算法的出现就帮助我们解决了这一难题。

前缀编码

既然我们不希望编码出现歧义,那么我们就需要一种无论何种情况下,编码结果都只可能唯一的编码,就是所谓的前缀编码。

前缀编码的概念是没有一个编码是其它编码的前缀,也可将其理解为无前缀编码。这种编码我们可以通过离散数学中的二叉树来获得。

二叉树

在计算机科学中,二叉树(英语:Binary tree)是每个节点最多只有两个分支(即不存在分支度大于2的节点)的树结构。通常分支被称作“左子树”或“右子树”。二叉树的分支具有左右次序,不能随意颠倒。

下面我们就使用二叉树来解决以下问题:

假设有一串字符,其中包含A1、A2、A3、A4、A5、A6六个符号,出现频率如下表:

符号A1A2A3A4A5A6
出现频率0.130.180.160.070.320.14

问题:求这留个符号的最优编码?

我们先将每个需要编码的符号节点放置在一个集合中,然后根据其权重将其中最小的两个节点拿出后与一个新增的父节点连接,之后将新增父节点再次放入优先序列中,接着选取更新后的优先序列中的权重最小的节点,与下一个新增父节点连接,以此类推。

节点集合A1(13)、A2(18)、A3(16)、A4(7)、A5(32)、A6(14)

上表就是一个节点集合,我们选择最小的两个节点A4和A1,连接到新增节点0后,再将0放入:

节点集合0(20)、A2(18)、A3(16)、A5(32)、A6(14)

再次比较集合中的节点权重大小,将A6和A3拿出,连接新增父节点1,放入集合中。

节点集合0(20)、A2(18)、A5(32)、1(30)

重复这个过程直到集合中没有内容,最终便得到了能够编码这6个字符的二叉树,也是最小二叉树:

这样选取最小节点不断连接的方法就是huffman算法。

程序设计

设计介绍

根据以上解释,我们可以设计一个二叉树类和一个节点类,如下:

(Node)类

import java.util.Comparator;

public class Node implements Comparator<Node> {
    //存储节点号数
    private String node;
    //存储节点权重
    private int weight;
    //存储节点的根节点和其左右子节点
    private Node root=null;
    private Node left=null;
    private Node right=null;

    //初始化节点
    public Node(String node,int weight){
        this.node = node;
        this.weight = weight;
    }

    //空构造函数用于有限序列的排序
    public Node(){}

    //get和set方法
    public String getNode() {
        return node;
    }
    public void setNode(String node) {
        this.node = node;
    }
    public int getWeight() {
        return weight;
    }
    public void setWeight(int weight) {
        this.weight = weight;
    }
    public Node getRoot() {
        return root;
    }
    public void setRoot(Node root) {
        this.root = root;
    }
    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 int compare(Node n1, Node n2) {
        return Integer.compare(n1.getWeight(), n2.getWeight());
    }
}

(BinaryTree)二叉树类

import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;

public class BinaryTree {
    private final List<Node> nodeList;//存储节点的最初集合
    private final List<List<Node>> treeList;//存储树的集合
    private int nodeNum;//录入数据时节点的编号

    //初始化nodeList和treeList
    public BinaryTree() {
        this.treeList = new ArrayList<>();
        nodeList = new ArrayList<Node>();
    }

    //录入节点数据
    public void insert(String node, int weight) {
        nodeList.add(new Node(node, weight));
    }

    //huffman算法
    public void huffman() {
        //建立优先队列
        PriorityQueue<Node> pq = new PriorityQueue<Node>(nodeList.size(), new Node());
        //在队列中添加节点集合内容
        pq.addAll(nodeList);
        //开始构造最小生成树
        while (pq.size() > 1) {
            //取出优先队列中权重最小的节点
            Node node1 = pq.poll();
            //再次取出队列中权重最小的节点
            Node node2 = pq.poll();
            //确保第二次取出的值不为null
            assert node2 != null;
            //构造两个节点的根节点
            Node node3 = new Node(String.valueOf(nodeNum++), node1.getWeight() + node2.getWeight());
            //设置node3为其子节点的父节点
            node1.setRoot(node3);
            node2.setRoot(node3);
            //设置node3的子节点
            node3.setLeft(node1);
            node3.setRight(node2);
            //将根节点加入队列
            pq.add(node3);
            //添加节点连接
            List<Node> temp1 = new ArrayList<>();
            List<Node> temp2 = new ArrayList<>();
            temp1.add(node1);
            temp1.add(node3);
            temp2.add(node2);
            temp2.add(node3);
            //构造最小生成树
            treeList.add(temp1);
            treeList.add(temp2);
        }
    }

    //返回最小生成树的集合
    public List<List<Node>> getTreeList() {
        return treeList;
    }

    //打印最小生成树的连接逻辑
    public void printTree() {
        for (List<Node> treeList : treeList) {
            int count = 0;
            for (Node node : treeList) {
                System.out.print(node.getNode() + "(" + node.getWeight() + ")");
                if (count < 1)
                    System.out.print("<-");
                count++;
            }
            System.out.println();
        }
    }

    //打印源符号编码
    public void printCode() {
        for (Node node : nodeList) {
            System.out.println(node.getNode() + ":" + code(node));
        }
    }

    //使用递归获得源符号编码
    public String code(Node node) {
        if (node == null || node.getRoot() == null)
            return "";
        return code(node.getRoot()) + ((node.getRoot().getLeft().equals(node)) ? "0" : "1");
    }
}

(Main)类【用于测试】

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner cin = new Scanner(System.in);
        BinaryTree bt = new BinaryTree();
        int n = cin.nextInt();
        for (int i = 0; i < n; i++) {
            bt.insert(cin.next(), cin.nextInt());
        }
        bt.huffman();
        System.out.println("Huffman Tree:");
        bt.printTree();
        System.out.println("Code:");
        bt.printCode();
    }
}

测试结果

测试数据

6
A1 13 A2 18 A3 16 A4 7 A5 32 A6 14

运行结果

Huffman Tree:
A4(7)<-0(20)
A1(13)<-0(20)
A6(14)<-1(30)
A3(16)<-1(30)
A2(18)<-2(38)
0(20)<-2(38)
1(30)<-3(62)
A5(32)<-3(62)
2(38)<-4(100)
3(62)<-4(100)
Code:
A1:011
A2:00
A3:101
A4:010
A5:11
A6:100

总结

如果大家没有看懂我讲的算法原理的话,可以去看b站上的动画演示:

https://www.bilibili.com/video/BV18V411v7px/?share_source=copy_web&vd_source=64f6cad256b7a8881c7b8f651e3080f9

最后,感谢大家阅读我的文章,如有什么不妥之处可以在评论区指出,谢谢大家,祝大家学业进步啦!

  • 54
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
哈夫曼编码是一种常用的压缩算法,使用贪心算法可以比较简单实现。 首先需要构建哈夫曼树,然后根据哈夫曼树生成对应的哈夫曼编码。具体实现步骤如下: 1. 定义一个节点类Node,包含字符ch、权值weight、左右子节点left和right等属性。 2. 定义一个哈夫曼树类HuffmanTree,包含根节点root,以及构建哈夫曼树的方法buildTree。 3. 构建哈夫曼树的方法buildTree,接收一个字符数组chars,返回一个哈夫曼树。 a. 计算每个字符出现的频率,并将每个字符和其对应的频率保存在一个Map中。 b. 将Map中的所有节点按照权值从小到大排序,并将节点放入一个优先队列中。 c. 从优先队列中取出两个权值最小的节点作为左右子节点,生成一个新的父节点,并将其放回优先队列中。 d. 重复c的操作,直到优先队列只剩下一个节点,此节点即为哈夫曼树的根节点。 4. 定义一个编码HuffmanCode,包含一个编码表Map,以及生成编码的方法generateCodes。 5. 生成编码的方法generateCodes,接收一个哈夫曼树,并返回一个编码表。 a. 如果节点是叶子节点,则将其编码加入编码表中。 b. 如果节点有左右子节点,则将左子节点的编码加上字符'0',将右子节点的编码加上字符'1',并分别递归处理左右子节点。 6. 最后可以使用生成的编码表对原始字符串进行编码,将每个字符替换为对应的编码即可。 下面是Java代码实现: ``` import java.util.*; class Node { char ch; int weight; Node left; Node right; public Node(char ch, int weight) { this.ch = ch; this.weight = weight; } } class HuffmanTree { Node root; public HuffmanTree(Node root) { this.root = root; } public static HuffmanTree buildTree(char[] chars) { Map<Character, Integer> freq = new HashMap<>(); for (char ch : chars) { freq.put(ch, freq.getOrDefault(ch, 0) + 1); } PriorityQueue<Node> pq = new PriorityQueue<>((a, b) -> a.weight - b.weight); for (Map.Entry<Character, Integer> entry : freq.entrySet()) { pq.offer(new Node(entry.getKey(), entry.getValue())); } while (pq.size() > 1) { Node left = pq.poll(); Node right = pq.poll(); Node parent = new Node('\0', left.weight + right.weight); parent.left = left; parent.right = right; pq.offer(parent); } return new HuffmanTree(pq.poll()); } } class HuffmanCode { Map<Character, String> codes = new HashMap<>(); public HuffmanCode(HuffmanTree tree) { generateCodes(tree.root, ""); } private void generateCodes(Node node, String code) { if (node.left == null && node.right == null) { codes.put(node.ch, code); return; } generateCodes(node.left, code + "0"); generateCodes(node.right, code + "1"); } public String encode(String str) { StringBuilder sb = new StringBuilder(); for (char ch : str.toCharArray()) { sb.append(codes.get(ch)); } return sb.toString(); } public String decode(String code) { StringBuilder sb = new StringBuilder(); Node node = HuffmanTree.root; for (char bit : code.toCharArray()) { node = (bit == '0') ? node.left : node.right; if (node.left == null && node.right == null) { sb.append(node.ch); node = HuffmanTree.root; } } return sb.toString(); } } public class Main { public static void main(String[] args) { char[] chars = "ABRACADABRA".toCharArray(); HuffmanTree tree = HuffmanTree.buildTree(chars); HuffmanCode code = new HuffmanCode(tree); String encoded = code.encode("ABRACADABRA"); System.out.println(encoded); System.out.println(code.decode(encoded)); } } ```
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值