数据结构-难点突破(C++实现并查集+路径优化,详解哈夫曼编码树)

news2025/2/24 6:38:04

文章目录

  • 1. 并查集
  • 2. 哈夫曼编码树

1. 并查集

并查集是一个多棵树的集合(森林)。

并查集由多个集合构成,每一个集合就是一颗树。
并:合并多个集合。查:判断两个值是否再一个集合中。

每棵树存在数组中,使用双亲表示法。数组每个元素的父节点。如果没有父节点数组保存-1。根节点位置的数组值就算这颗树节点的个数。
eg:
在这里插入图片描述

根据上图分析可知:
如果要合并的节点x和y不在一个树上
ufs[root_x] += ufs[root_y];(将y这个节点对应的子树和x的节点进行合并)
ufs[root_y] = root_x;(改变y这个节点的父亲节点)

根据上文可知,并查集查找的速度与树的高度有关。

当查找节点后,如果能将这个节点到根节点路径上的节点都直接插入到根节点上,则可以显著降低树的高度,从而提高效率。

在这里插入图片描述

上述过程在查找节点的根节点时实现,找到根节点时先不返回,再遍历一遍更新节点的父节点即可。

把这个节点到根节点路径上的所有节点直接插入到根节点上
while (data != root)
{
    int parent = ufs[data];
    ufs[data] = root;
    data = parent;
}

C++代码:

//构建并查集

#include <assert.h>
#include <vector>
#include <stdio.h>

class UnionFindSet
{
private:
    //数组的下标保存的是并查集的数据,数组的值记录的是并查集这个节点的父节点下标
    std::vector<int> ufs;

public:
    UnionFindSet(size_t size)
    {
        ufs.resize(size, -1);
    }

    // x和y所在的两个集合合并
    void Union(int x, int y)
    {
        assert(x < ufs.size() && y < ufs.size());
        int root_x = FindRoot(x);
        int root_y = FindRoot(y);

        if (root_x != root_y)
        {
            //不在一棵树上
            ufs[root_x] += ufs[root_y];
            ufs[root_y] = root_x;
        }
    }

    //找data的根
    int FindRoot(int data)
    {
        int root = data;
        while (ufs[root] >= 0)
        {
            root = ufs[root];
        }

        //找到根后,这里做优化,降低并查集树的高度
        //把这个节点到根节点路径上的所有节点插入到根节点上
        while (data != root)
        {
            int parent = ufs[data];
            ufs[data] = root;
            data = parent;
        }
        return root;
    }

    //获取并查集中树的个数
    int GetTreeSize()
    {
        int ret = 0;
        for (int i = 0; i < ufs.size(); i++)
        {
            if (ufs[i] < 0)
                ret += 1;
        }
        return ret;
    }

    //打印并查集信息
    void PrintUfs()
    {
        for (int i = 0; i < ufs.size(); i++)
        {
            printf("%2d ", i);
        }
        printf("\n");
        for (int i = 0; i < ufs.size(); i++)
        {
            printf("%2d ", ufs[i]);
        }
        printf("\n");
    }
};
#include "UnionFindSet.h"
#include <iostream>
int main(int argc, char const *argv[])
{
    UnionFindSet set(9);

    for (int times = 0; times < 8; times++)
    {
    	//打印并查集树的个数
        std::cout << set.GetTreeSize() << std::endl;
        set.PrintUfs();
        set.Union(times, times + 1);
    }

    std::cout << set.GetTreeSize() << std::endl;
    set.PrintUfs();

    return 0;
}

在这里插入图片描述

2. 哈夫曼编码树

哈夫曼树:
在许多应用中,树中结点常常被赋予一个表示某种意义的数值,称为该结点的权。

从树的根到任意结点的路径长度(经过的边数)与该结点上权值的乘积,称为该结点的带权路径长度。

树中所有叶结点的带权路径长度之和称为该树的带权路径长度。

在这里插入图片描述

哈夫曼编码:
哈夫曼编码就是在哈夫曼树的基础上构建的,这种编码方式最大的优点就是用最少的字符包含最多的信息内容,进而实现信息的压缩存储。

根据发送信息的内容,通过统计文本中相同字符的个数作为每个字符的权值,建立哈夫曼树。对于树中的每一个子树,统一规定其左孩子标记为 0 ,右孩子标记为 1 。这样,用到哪个字符时,从哈夫曼树的根结点开始,依次写出经过结点的标记,最终得到的就是该结点的哈夫曼编码。

文本中字符出现的次数越多,在哈夫曼树中的体现就是越接近树根。编码的长度越短。

eg:
在这里插入图片描述

这是用权值分别为 7、5、2、4 的字符 a、b、c、d 构建的哈夫曼树。

显然,字符 a 用到的次数最多,所以它对应的哈弗曼编码应最短,这里用 0 表示;其次,是字符 b 用的多,因此字符 b 编码为 10 ,
以此类推,字符 c 的编码为 110 ,字符 d 的编码为 111。

权值越大,表示此字符在文件中出现的次数越多,那么,为了实现用最少的字符包含最多的内容,就应该给出现次数越多的字符,分配的哈弗曼编码越短。

使用程序求哈夫曼编码有两种方法:

  1. 从叶子结点一直找到根结点,逆向记录途中经过的标记。例如,上图中字符 c 的哈夫曼编码从结点 c 开始一直找到根结点,结果为:0 1 1 ,所以字符 c 的哈夫曼编码为:1 1 0(逆序输出)。
  2. 从根结点出发,一直到叶子结点,记录途中经过的标记。例如,上图中字符 c 的哈夫曼编码,就从根结点开始,依次为:1 1 0。

需要注意:

注意:0和1究竟是表示左子树还是右子树没有明确规定。
左、右孩子结点的顺序是任意的, 所以构造出的哈夫曼树并不唯一,但各哈夫曼树的带权路径长度WPL相同且为最优。
此外,如有若干权值相同的结点,则构造出的哈夫曼树更可能不同,但WPL必然相同且是最优的

哈夫曼树构造流程:
给定N个权值分别为W1, W2,…,Wn”的结点,构造哈夫曼树的算法描述如下:

  1. 将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
  2. 构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
  3. 从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
  4. 重复步骤2和3,直至F中只剩下一棵树为止。

从上述构造过程中可以看岀哈夫曼树具有如下特点:

  1. 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大。
  2. 构造过程中共新建了n-1个结点(双分支结点),因此哈夫曼树的结点总数为2*n-1
  3. 每次构造都选择2棵树作为新结点的孩子,因此哈夫曼树中不存在度为1的结点。

C++代码:

#include <iostream>

#include <unordered_map>

#include <string>

#include <vector>

#include <assert.h>
//传入一串字符,使用哈夫曼树对其进行编码,字符串中每个字符出现的次数就是作为哈夫曼树的权值

struct TreeNode
{
    char val;   //节点保存的值
    int weight; //权值
    TreeNode *left;
    TreeNode *right;
    TreeNode(int _weight) : val(0), weight(_weight), left(nullptr), right(nullptr) {}
    TreeNode(char _val, int _weight) : val(_val), weight(_weight), left(nullptr), right(nullptr) {}
};

class HuffTree
{
private:
    void _InitTimes(const std::string &src)
    {
        for (int i = 0; i < src.size(); i++)
        {
            times[src[i]] += 1;
        }
    }

    void _PreDisplay(TreeNode *node, std::string &str)
    {
        if (node->left == nullptr && node->right == nullptr)
        {
            //叶子节点
            // std::cout << str << std::endl;
            code[node->val] = str;
            return;
        }
        if (node->left != nullptr)
        {
            str.push_back('0');
            _PreDisplay(node->left, str);
            str.pop_back(); //出递归后,刚刚递归进去给字符串插入的字符也要弹出
        }
        if (node->right != nullptr)
        {
            str.push_back('1');
            _PreDisplay(node->right, str);
            str.pop_back();
        }
    }

    void _PreDisplay(TreeNode *node)
    {
        std::string retBuff;
        _PreDisplay(node, retBuff);
    }

public:
    TreeNode *root;                             //构造的哈夫曼树
    std::unordered_map<char, std::string> code; //保存字符进行编码的结果
    std::unordered_map<char, int> times;        //保存传入的字符串中每个字符出现的次数

    HuffTree(const std::string &src)
    {
        //统计每个字符出现的次数
        _InitTimes(src);

        std::vector<TreeNode *> ArrayNode;
        //先给所有节点开辟空间
        std::unordered_map<char, int>::iterator pos = times.begin();
        while (pos != times.end())
        {
            ArrayNode.push_back(new TreeNode(pos->first, pos->second));
            pos++;
        }

        //循环创建哈夫曼树节点
        //如果只有一个节点
        if (ArrayNode.size() == 0)
        {
            root = ArrayNode[0];
        }
        else
        {
            for (int time = 0; time < ArrayNode.size() - 1; time++)
            {
                //找权值最小的和第二小的节点
                int minIndex = 0;
                int minSecIndex = 0;
                // ArrayNode[minIndex] == nullptr证明这个节点已经建立过哈夫曼树了,需要跳过
                while (ArrayNode[minIndex] == nullptr)
                {
                    minIndex++;
                }
                for (int i = 0; i < ArrayNode.size(); i++)
                {
                    if (ArrayNode[i] != nullptr && ArrayNode[i]->weight < ArrayNode[minIndex]->weight)
                    {
                        minIndex = i;
                    }
                }

                //找次小值
                while (ArrayNode[minSecIndex] == nullptr || minIndex == minSecIndex)
                {
                    minSecIndex++;
                }
                for (int i = 0; i < ArrayNode.size(); i++)
                {
                    if (i != minIndex)
                    {
                        if (ArrayNode[i] != nullptr && ArrayNode[i]->weight < ArrayNode[minSecIndex]->weight)
                        {
                            minSecIndex = i;
                        }
                    }
                }

                // printf("出现次数最小的字符是%c,出现次数%d\n", ArrayNode[minIndex]->val, ArrayNode[minIndex]->weight);
                // printf("出现次数次少的字符是%c,出现次数为%d\n", ArrayNode[minSecIndex]->val, ArrayNode[minSecIndex]->weight);
                // printf("============\n");

                //创建新节点,将这个节点插入到最小字符位置,次少节点位置处理过了置空.并将树结构构造好
                root = new TreeNode(ArrayNode[minIndex]->weight + ArrayNode[minSecIndex]->weight);

                root->left = ArrayNode[minIndex];
                root->right = ArrayNode[minSecIndex];

                ArrayNode[minIndex] = root;
                ArrayNode[minSecIndex] = nullptr;
            }
        }
    }

    //打印编码
    void PrintCode()
    {
        //遍历树,直到遍历到叶子节点,规定向左树走编码为0,向右树走编码为1
        _PreDisplay(root);

        //打印编码集合
        auto pos = code.begin();
        while (pos != code.end())
        {
            std::cout << pos->first << ":" << pos->second << std::endl;
            pos++;
        }
    }

    //解码
    char DeCode(const std::string &src)
    {
        TreeNode *node = root;
        for (int i = 0; i < src.size(); i++)
        {
            if (src[i] == '0')
                node = node->left;
            if (src[i] == '1')
                node = node->right;
        }
        assert(node->left == nullptr && node->right == nullptr); // node一定走到了叶子节点
        return node->val;
    }
};
#include "hufftree.h"

int main(int argc, char const *argv[])
{
    HuffTree tree("abandon");
    tree.PrintCode();

    std::cout << "010解码:" << tree.DeCode("010") << std::endl;
    return 0;
}

这里构建的哈夫曼树如下图:
abandon中
a出现2次,b出现1次,n出现2次,d出现1次,o出现1次。次数作为整个节点的权值

在这里插入图片描述

在这里插入图片描述
需要注意的是,这里对其编码使用一个字符‘1’或‘0’进行编码,压缩效果不明显。

正式做项目时,可以选择树向左移动时,代表比特位0,向右移动时代表比特1。这样才可以真正达到压缩效果。

具体细节可以移步数据结构-压缩软件核心-C++(利用哈夫曼树进行编码,对文件进行压缩与解压缩)

代码位置Github

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

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

相关文章

js 中的 Event Loop 以及 宏任务 与 微任务

目录前言1、JS 的 执行引擎 与 执行环境2、js 是单线程的一、事件循环&#xff08;Event Loop&#xff09;二、任务队列三、宏任务 与 微任务1、宏任务2、微任务3、宏任务与微任务的运行机制四、Event Loop 实例案例一案例二前言 1、JS 的 执行引擎 与 执行环境 简单来说&…

SpringCloud微服务(八)——OpenFeign服务调用

OpenFeign服务调用 SpringCloud github官网&#xff1a;https://github.com/spring-cloud/spring-cloud-openfeign Feign是一个声明式的Web Service客户端。它的出现使开发Web Service客户端变得很简单。使用Feign只需要创建一个接口加上对应的注解&#xff0c;比如&#xf…

基于java+springboot+mybatis+vue+elementui的人职匹配推荐系统

项目介绍 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff0c;对于人职匹配推荐系统当然也不能排除在外&#xff0c;随着网络技术的不断成熟&#xff0c;带动了人职匹配推荐系统&#xff0c;它彻底改变…

分享一下前几个月我做的超炫的登录页面

先给大家看看登录页面的效果演示 这个登录页面分为三个部分&#xff08;页面切换&#xff1a;连续按五次V&#xff0c;大小写都可以&#xff09; 第一个&#xff08;最初的鱼儿游动页面&#xff09; 登录、切换页面、和鱼儿游动这个页面的代码就不放在这里了&#xff0c;这个虽…

RabbitMQ 入门案例项目

写在前面 本文不作消息队列的实现原理、异步处理优劣、rabbitmq安装说明、消息工作模式等内容分析&#xff0c;只讲述rabbitmq实际开发中的步骤说明&#xff0c;帮助同学快速上手体验消息队列的使用。 本文使用SpringAMQP&#xff0c;并非rabbitmq官方文档上的原生http请求连…

Jupyter notebook在超算平台上使用的详细教程

Jupyter Notebook 的本质是一个 Web 应用程序&#xff0c;便于创建和共享文学化程序文档&#xff0c;支持实时代码&#xff0c;数学方程&#xff0c;可视化和 markdown。 用途包括&#xff1a;数据清理和转换&#xff0c;数值模拟&#xff0c;统计建模&#xff0c;机器学习等等…

LeetCode 数据结构与算法:最大子数组和

打开我的题库&#xff0c;调为简单难度。 计算最大子数&#xff0c;直接给我难住。 报错铺满屏幕&#xff0c;凝望没有思路。 缝缝补补做出&#xff0c;击败零个用户。 翻阅评论找补&#xff0c;令我勃然大怒。 打开思维第一步&#xff0c;编写代码求数组&#xff0c; …

报错解决:Process finished with exit code -1073741819 (0xC0000005)

简单记录一下程序异常终止&#xff0c;抛出 Process finished with exit code -1073741819 (0xC0000005) 的解决方法。 一、程序中文件位置错误/缺少文件 位置错误1&#xff1a;如果使用相对路径的话&#xff0c;推荐换成绝对路径进行排查。位置错误2&#xff1a;如果使用了o…

CAN总线协议测试拓扑图

记录测试CAN总线协议&#xff0c; CAN总线目前主要应用在汽车。 记录在PC使用USB-CAN连接测试

Talk预告 | Salesforce AI研究院研究科学家徐嘉诚:文本生成中的结构化解码

本期为TechBeat人工智能社区第457期线上Talk&#xff01; 北京时间11月23日(周三)20:00&#xff0c;Salesforce AI研究院研究科学家——徐嘉诚的Talk将准时在TechBeat人工智能社区开播&#xff01; 他与大家分享的主题是: “文本生成中的结构化解码”&#xff0c;届时将详细讲解…

学会用数据分析汇报工作,升职加薪指日可待

你是否每天的八小时工作时长&#xff0c;分成八瓣用&#xff0c;却仍被领导安排众多工作&#xff1f;明明做了很多事&#xff0c;领导依旧认为工作量不饱和&#xff1f;这样的现象在职场中早已司空见惯&#xff0c;不足为奇了&#xff0c;但是究其原因是什么呢&#xff1f;工作…

Android App网络通信中通过runOnUiThread快速操纵界面以及利用线程池Executor调度异步任务实战(附源码 简单易懂)

运行有问题或需要源码请点赞关注收藏后评论区留言私信~~~ 一、通过runOnUiThread快速操纵界面 因为Android规定分线程不能够直接操纵界面&#xff0c;所以它设计了处理程序工具&#xff0c;由处理程序负责在主线程和分线程之间传递数据&#xff0c;如果分线程想刷新界面&#…

精心整理16条MySQL使用规范,减少80%问题

1. 禁止使用select * 阿里开发规范中&#xff0c;有这么一句话&#xff1a; **select *** 会查询表中所有字段&#xff0c;如果表中的字段有更改&#xff0c;必须修改SQL语句&#xff0c;不然就会执行错误。 查询出非必要的字段&#xff0c;徒增磁盘IO和网络延迟。 2. 用小表…

小学生python游戏编程arcade----敌人精灵上方显示方框及子弹显示问题

小学生python游戏编程arcade----敌人精灵上方显示方框及子弹显示问题前言1、敌人精灵上方显示方框1.1 修改enemy_tank类1.2 引用1.3 效果图2、调整方法2.1 类方法2.2 类的引用2.3 效果图2.4 大小位置调整后3、子弹过线自动消失3.1 子弹的更新中3.2 原因查到&#xff0c;把以下代…

day11 多级缓存

day11 多级缓存 1、什么是多级缓存 传统的缓存策略一般是请求到达Tomcat后&#xff0c;先查询Redis&#xff0c;如果未命中则查询数据库&#xff0c;如图&#xff1a; 存在下面的问题&#xff1a; 请求要经过 Tomcat 进行处理&#xff0c;Tomcat 的性能成为整个系统的瓶颈Red…

数字孪生助力轨道交通安保可视化应用

截至2020年12月31日&#xff0c;全国&#xff08;不含港澳台&#xff09;共有44个城市开通运营城市轨道交通线路233条&#xff0c;运营里程7545.5公里&#xff0c;车站4660座&#xff0c;完成客运量175.9亿人次&#xff0c;进站量109.1亿人次。针对轨道交通地铁站内日常监测、事…

牛客网语法篇练习分支控制(一)

1.据说智商140以上者称为天才&#xff0c;KiKi想知道他自己是不是天才&#xff0c;请帮他编程判断。输入一个整数表示一个人的智商&#xff0c;如果大于等于140&#xff0c;则表明他是一个天才&#xff0c;输出“Genius”。 while True:try:a int(input())if a >140:print…

云原生加速器企业维格表创始人陈霈霖:提供人人可用的数字化转型全新方案,真正驱动组织创新

看上去是像Excel一样的在线协同表格&#xff0c;却能把文件、表格、图片、视频、填表单等变换出各种视图&#xff0c;它能帮助你高效方便的管理各种零碎的信息和数据&#xff1b;也能根据你的想法DIY各种功能&#xff0c;5分钟即可搭建一个适合自己的文档管理系统&#xff0c;实…

C#上位机系列(1)—项目的建立

本文是讲解C#.net平台的Winform框架下的第一个内容&#xff0c;手把手介绍项目的创建方式以及一些写软件时常用的功能。之前写过一篇关于示波器的比较抽象&#xff0c;本文讲解从零开始的每一个步骤。 VS2022以及C#.net平台的Winform框架自行百度下载。 1.创建一个新的项目 …

智慧经营| 物业数字化管理系统

无论小区、公寓还是豪华区&#xff0c;总是少不了物业的身影&#xff0c;通过物业可以为住户解决许多事情&#xff0c;比如物业报事、物业维修、便民服务、送水上门、房屋租赁、投诉等&#xff0c;但小区内公告栏没人看、挨家挨户的去通知效率低、且无法全面完善管理区域内的所…