哈夫曼树原理及Java编码实现

news2024/9/21 18:52:20

文章目录

  • 前言
  • 一、哈夫曼树原理
  • 二、哈夫曼编码(Java题解)
  • 参考资料

前言

所有博客文件目录索引:博客目录索引(持续更新)

源代码:Gitee—Huffman.java、Github—Huffman.java

一、哈夫曼树原理

对于哈夫曼树的构造以及权值计算原理知识点推荐看这个视频:哈夫曼树和哈夫曼编码—bilibili

哈夫曼编码有两个特点

  1. 带权路径长度WPL最短且唯一;【核心减少编码的操作】
  2. 编码互不为前缀(一个编码不是另一个编码的开头)【可进行还原用途】。

应用场景:压缩文件。

公式:路径长度:WPL = l1× w1+l2× w2 +…+ ln × wn,w表示权值,n表示叶子节点个数。

哈夫曼编码是如何进行应用的呢,有什么具体的示例呢?

哈夫曼树是一颗二叉树,其是根据元素的权重来进行构成的一棵树,在树上的每个节点val都使用0或1来进行表示。

  • 这个权重指的是该元素出现的次数频率,频率越高的就在越上层。

就像下面一样,可以说每个元素值所对应的路径都是唯一的:

image-20221029215920468

3:00000
5:00001
11:0001
前者是值,后者即为路径,就是从上到下的路径合并。

问题来了为什么要构成构造成一个哈夫曼树?尤其是为什么要根据权重来进行排列分布呢?

首先解决后面一个问题:若是在一组数据中,有10个不同的字符,字符a出现了1000次,其他字符出现的数量很少,我们是否可以根据出现的次数来对这些字符来进行编码,a用1表示,b字符用01…d字符使用0003表示,那么1000次a仅仅只需要1000个字符即可,是不是大大减少了存储空间?这也就是我们为什么要根据权重进行排列分布。

此时可以来说明前面一个问题:因为哈夫曼树是带权路径长度最短的树,权值较大的节点离根节点较近。而带权路径长度是指:树中所有的叶子节点的权值乘上其到根节点的路径长度,这与最终的哈夫曼编码总长度成正比关系的。

核心操作:一旦哈夫曼树构建出来之后,我们可以得到每个字符与其路径,那么我们根据这个hash表即可进行字符串编码,而由于每个路径都是唯一的,我们同样也可依靠hash表来进行解码!

二、哈夫曼编码(Java题解)

编码思路过程:

encode编码:构造哈夫曼树 -> 获取字符及路径map  ->  根据map去构建指定编码
	1、构造哈夫曼树:
		准备条件:自定义树节点(字符、val、权重,其中val为之后路径组成的值)
		过程:①准备一个map来统计所有字符出现次数即权重。②根据权重进行排序。③遍历map来构建树节点,接着根据权重排序,存储到一个链表节点中。④遍历链表节点每次取出两个构成一个虚拟节点,之后再找前缀最小的进行重复合并,视角是从底之上进行合并构建。
	2、获取字符及路径map:递归过程,核心路径重点就是叶子节点,在哈夫曼树里有效字符节点都是叶子节点。
	3、根据map中的字符与对应匹配的路径来进行对所有源字符遍历编码。

decode解码:根据map来进行前缀匹配(由于每个字符的路径唯一),即可进行还原字符

Java题解:

package com.changlu.tree;

import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

/**
 * @Description: 哈夫曼树
 * @Author: changlu
 * @Date: 8:09 PM
 */
//哈夫曼树节点
class TreeNode {
    public Character ch;
    public int val;
    public int freq;
    public TreeNode left;
    public TreeNode right;
    public TreeNode(){}
    public TreeNode(Character ch, int val, int freq, TreeNode left, TreeNode right) {
        this.ch = ch;
        this.val = val;
        this.freq = freq;
        this.left = left;
        this.right = right;
    }
}

public class Huffman {

    /**
     * 解码
     * @param encodeStr 已编码字符串
     * @param encodeMap 编码map集合
     * @return
     */
    public static String decode (String encodeStr, Map<Character, String> encodeMap) {
        StringBuilder decodeStr = new StringBuilder();
        while (encodeStr.length() > 0) {
            //遍历所有编码map对应的value(也就是编码路径)
            for (Map.Entry<Character, String> entry : encodeMap.entrySet()) {
                String charEncode = entry.getValue();
                //匹配路径前缀
                if (encodeStr.startsWith(charEncode)) {
                    decodeStr.append(entry.getKey());
                    //删除该前缀
                    encodeStr = encodeStr.substring(charEncode.length());
                    break;
                }
            }
        }
        return decodeStr.toString();
    }

    //编码
    //0:编码后的字符串
    //1:对应哈弗曼树中字符的路径表示
    public static Object[] encode(String s) {
        Object[] res = new Object[2];
        //编码字符串
        //1、构建哈夫曼树
        TreeNode rootNode = constructTree(s);
        //2、根据哈夫曼树找到所有的路径并存储到map中
        Map<Character, String> encodeMap = new HashMap<>();
        findPath(rootNode, encodeMap, new StringBuilder());//存储所有字符、编码路径到map中
        //此时 字符:路径编码  已经再encodeMap中存储了,我们即可来进行编码
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            sb.append(encodeMap.get(s.charAt(i)));
        }
        res[0] = sb.toString();
        res[1] = encodeMap;
        return res;
    }

    /**
     * 寻找到哈夫曼树中所有字符的路径,并存储到map中
     * @param root 哈夫曼树的根节点
     * @param encodeMap 编码map
     * @param path 存储路径
     */
    private static void findPath(TreeNode root, Map<Character, String> encodeMap, StringBuilder path) {
        //每个元素的终点就是n0节点
        if (root.left == null || root.right == null) {
            path.append(root.val);
            //为什么要从第1位开始呢?因为完全可以再少一位0,这样也能减少不少字符
            encodeMap.put(root.ch, path.substring(1));
            path.deleteCharAt(path.length() - 1);
            return;
        }
        path.append(root.val);
        //递归左右
        if (root.left != null) findPath(root.left, encodeMap, path);
        if (root.right != null) findPath(root.right, encodeMap, path);
        //删除所有最后一个字符
        path.deleteCharAt(path.length() - 1);
    }


    /**
     * 构造哈夫曼树
     * @param s
     * @return
     */
    private static TreeNode constructTree(String s) {
        //1、统计每个字符出现的权重
        Map<Character, Integer> cntMap = new HashMap<>();
        for (int i = 0; i < s.length(); i++) {
            cntMap.put(s.charAt(i), cntMap.getOrDefault(s.charAt(i), 0));
        }
        //2、将所有的字符构建成TreeNode节点存储到LinkedList中
        LinkedList<TreeNode> nodelist = new LinkedList<>();
        for (Map.Entry<Character, Integer> entry : cntMap.entrySet()) {
            Character ch = entry.getKey();//字符
            Integer freq = entry.getValue();//频率
            int val = 0;//节点值
            nodelist.add(new TreeNode(ch, val, freq, null, null));
        }
        //3、根据频率来进行排序
        Collections.sort(nodelist, (node1, node2)->node1.freq - node2.freq);
        //4、构建哈夫曼树
        //处理只有一个节点的情况
        if (nodelist.size() == 1) {
            //1个节点也要有父节点
            TreeNode treeNode = nodelist.get(0);
            return new TreeNode(null, 0, treeNode.freq, treeNode, null);
        }
        //开始构建哈夫曼树
        TreeNode root = null;
        while (nodelist.size() > 0) {
            //取出头部两个节点
            TreeNode t1 = nodelist.removeFirst();
            TreeNode t2 = nodelist.removeFirst();
            //左右子树节点来进行分别赋值0,1
            t1.val = 0;
            t2.val = 1;
            //若是当前节点中没有节点了
            if (nodelist.size() == 0) {
                root = new TreeNode(null, 0, t1.freq + t2.freq, t1, t2);
            }else {
                //首先构建成一个虚拟节点
                TreeNode combineNode = new TreeNode(null, 0, t1.freq + t2.freq, t1, t2);
                //将虚拟节点插入到链表中,同样需要维护其有序性
                //暴力法【实际这里可用二分来进行】
                if (combineNode.freq > nodelist.getLast().freq) {
                    nodelist.addLast(combineNode);
                }else {
                    //遍历找到权重>该合并节点的权重
                    for (int i = 0; i < nodelist.size(); i++) {
                        if (nodelist.get(i).freq >= combineNode.freq) {
                            nodelist.add(i, combineNode);
                            break;
                        }
                    }
                }
            }
        }
        return root;
    }

}

测试代码:

public class Huffman {
	public static void main(String[] args) {
        String s = "abbbbeeerrryuiolll";
        System.out.println("编码前:" + s);
        //编码API
        Object[] encodeRes = encode(s);
        String encodeStr = (String) encodeRes[0];
        Map<Character, String> encodeMap = (Map<Character, String>) encodeRes[1];
        System.out.println("编码表:");
        for (Map.Entry<Character, String> e : encodeMap.entrySet()) {
            System.out.println(e.getKey() + ":" + e.getValue());
        }
        System.out.println("编码后:" + encodeStr);
        //解码API
        String decodeStr = decode(encodeStr, encodeMap);
        System.out.println("解码后:" + decodeStr);
    }
}

image-20221029215518786

参考资料

[1] 哈夫曼编码(Huffman Coding)原理详解

[2]. 哈夫曼编码细解& Java 实现

[3]. 视频:哈夫曼树和哈夫曼编码

[4]. 【JAVA】KMP算法保姆级教程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1884.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java语言高级-10MySQL-第2节MySQL安装与使用

2、MySQL数据库软件 1、安装 详细见视频 2、卸载 1、去mysql的安装目录找到my.ini文件 “复制datadir “C:/programData/MySQL/MySQL Server 5.5/Data” 2、卸载MySQL 控制面板->程序卸载&#xff0c;但是此时并没有卸载干净 3、删除C:/ProgramData目录下得MySQL文件夹 注意…

java 内部类

小镇做题家 前段时间很火的一个话题“小镇做题家”&#xff0c;我没有具体了解&#xff0c;我以为是在鼓励一些努力拼搏的人。 某一天&#xff0c;禁不住好奇&#xff0c;我打开了百度百科&#xff0c;看了看小镇做题家的解释 就是指一些村里或者小镇的人&#xff0c;通过学习…

模板和泛型编程(上)

目录函数模板泛型编程函数模板概念及原理函数模板的实例化函数模板的匹配原则类模板类模板实例化函数模板 泛型编程 泛型编程&#xff08;Generic Programming&#xff09;最初提出时的动机很简单直接&#xff1a;发明一种语言机制&#xff0c;能够帮助实现一个通用的标准容器…

猿创征文|Python迭代器、生成器、装饰器、函数闭包

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起探讨和分享Linux C/C/Python/Shell编程、机器人技术、机器学习、机器视觉、嵌入式AI相关领域的知识和技术。 Python迭代器、生成器、装饰器、函数闭包1. 迭代器 iterator☞迭代器协议☞Python中的for循环2. 生成器…

C语言qsort()函数针对:整型、单个字符、字符串、结构体,超详细讲解(多维度分析举例,小白一看就懂!!!!!)

目录 一、前言 二、qsort()函数 &#x1f351;qsort()函数简介 &#x1f349;qsort()函数中整型、double型、字符型的应用 &#x1f4a6;整型 &#x1f4a6; double型 &#x1f4a6;字符排序 &#x1f34e;qsort()函数在字符串中的应用 &#x1f4a6;在字符串中按首字母…

Android NDK开发基础

文章目录cmake语法基础cmake添加日志&#xff1a;cmake增加宏字符串比较cmake在build.gradle中传递编译参数到cmake通过javah生成native对应的c头文件jni和java之间字符串的相互操作JavaVM和JNIEnv字符串的编码native方法静态注册和动态注册静态注册动态注册extern cC中STATIC和…

SpringCloud Alibaba-Sentinel保姆级教程

文章目录1、Sentinel前奏1.1、服务雪崩效应1.2、常见容错方案1、隔离2、超时3、限流4、熔断5、降级1.3、常见容错组件1、Hystrix2、Resilience4J3、Sentinel2、Sentinel入门2.1、什么是Sentinel2.2、实战编码1、安装Sentinel服务端2、微服务引入Sentinel2.3、Sentinel流控模式1…

YOLOv5蒸馏 | 知识蒸馏理论篇 | 1/2

之前在《一文搞懂【知识蒸馏】【Knowledge Distillation】算法原理》这篇文章中介绍过一些知识蒸馏的原理,这篇博文将会着重介绍目标检测领域的知识蒸馏原理。 文章目录 1.《Object detection at 200 Frames Per Second》1.1 蒸馏难点1.2 蒸馏损失1.3 实验结果2. 《Learning E…

计算机毕业设计ssm+vue基本微信小程序的体检预约小程序

项目介绍 我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;体检预约系统小程序被用户普遍使用&#xff0c;为方便用户…

【前端】CSS(1) —— CSS的基本语法和一些简单的选择器

JavaEE传送门JavaEE 网络原理——网络层与数据链路层 【前端】HTML入门 —— HTML的常见标签 目录CSS基本语法注释引入方式内部样式内联样式外部样式代码风格样式格式样式大小写空格规范CSS 选择器标签选择器类选择器id 选择器通配符选择器复合选择器后代选择器子选择器并集选…

文件包含漏洞和hash破解

介绍 Windows是当今世界最主要的操作系统&#xff0c;因为它易于使用的GUI可访问性。大约85%的市场份额已成为攻击的关键操作系统。此外&#xff0c;大多数组织使用Active Directory来设置其Windows域网络。微软聘请NTLM&#xff08;New Technology LAN Manager&#xff09;和…

Pico Neo3 4VR游戏下载地址及十大好玩游戏推荐

大家好&#xff0c;杰克今天为大家分享的是VR一体机游戏。 说到VR一体机&#xff0c;我们首先想到的一定是国产之光pico啦&#xff0c;picovr被认为是国内最被看好的VR一体机&#xff0c;有望在国内取代红极一时的OculusQuest&#xff0c;填补这块市场的空白。OculusQuest将6D…

#Primavera Unifier:关于零代码/低代码平台特点【2/3】

在之前对Unifier的介绍中&#xff0c;我提到了Unifier应用的一个非常关键的特征&#xff0c;及零代码快速配置使用&#xff0c;而为了更好的介绍Oracle Primavera Unifier 的零代码特点&#xff0c;以下我将通过3篇内容来逐一介绍零代码/低代码平台的特点。 前面介绍到了零代码…

Opencv项目实战:14 手势控制音量

目录 0、项目介绍 1、项目展示 2、项目搭建 3、项目的代码与讲解 4、项目资源 5、项目总结 0、项目介绍 本篇与上一篇有很多联系&#xff0c;大家可以看看这篇Opencv项目实战&#xff1a;13 手部追踪&#xff0c;我们将根据上一节的内容&#xff0c;进行一个拓展。本篇你…

AtCoder Beginner Contest 275 【E】【F】

E - Sugoroku 4 【概率dp】 题意&#xff1a; 对于每个样例&#xff0c;读入n&#xff0c;m&#xff0c;k。 一维数轴&#xff0c;你现在在0这个点上&#xff0c;目标是到达n这个点&#xff0c;你有k次掷骰子的机会&#xff0c;每次可能等概率的掷出1~m的任意一个数字&#xff…

面了一个4年经验的测试工程师,自动化都不会也要15k,我真是醉了...

在深圳这家金融公司也待了几年&#xff0c;被别人面试过也面试过别人&#xff0c;大大小小的事情也见识不少&#xff0c;今天又是团面的一天&#xff0c; 一百多个人都聚集在一起&#xff0c;因为公司最近在谈项目出来面试就2个人&#xff0c;无奈又被叫到面试房间。 整个过程…

.NET Core HttpReports 监控

HttpReports 基于.NET Core 开发的APM监控系统&#xff0c;使用MIT开源协议&#xff0c;主要功能包括&#xff0c;统计, 分析, 可视化&#xff0c; 监控&#xff0c;追踪等&#xff0c;适合在中小项目中使用。 语雀&#xff1a;https://www.yuque.com/httpreports/docs/uyaiil …

SQL注入之绕过is_numeric过滤

目录预备知识PHP常用的过滤类函数is_numeric()函数介绍实验目的实验环境实验步骤一通过源代码审计&#xff0c;发现临时文件实验步骤二通过分析源代码的执行逻辑&#xff0c;了解程序的功能&#xff0c;发现程序中的安全漏洞实验步骤三绕过过滤函数实现SQL注入预备知识 PHP常用…

Linux进程信号

文章目录什么是信号signal函数的功能&#xff08;捕捉信号后自己处理&#xff09;Core Dump&#xff08;核心转储&#xff09;kill&#xff0c;raise&#xff0c;alarm系统调用再度理解OS给进程发送信号信号集操作函数自定义捕捉详解什么是信号 生活中的信号&#xff1a;闹钟&…

0083 环形链表

package LinkedList_; /* * 单向环形链表应用场景——约瑟夫问题 * 设编号为1&#xff0c;2....n的n个人围成一圈&#xff0c;约定编号为k&#xff08;1<k<n&#xff09;的人从1开始报数&#xff0c;数到m的人出列&#xff0c; * 它的下一位又从…