数据结构05:树与二叉树[C++][并查集]

news2024/11/24 14:03:57

图源:文心一言

Chat GPT生成,代码的核心思想与王道咸鱼老师的视频虽然类似,但是在具体实现上毕竟还是略有差别~~因此,如果对考研方向的并查集代码感兴趣,可以查看——

王道咸鱼老师的视频:{5.5_2_并查集_哔哩哔哩_bilibili}

Pecco大佬的博文:算法学习笔记(1) : 并查集 - 知乎 (zhihu.com)

第1版:查资料、画导图、画配图~🧩🧩

参考用书:王道考研《2024年 数据结构考研复习指导》

特别感谢: Chat GPT老师、文心一言老师、BING AI老师~


📇目录

目录

📇目录

🦮思维导图 

🧵基本概念

⏲️定义

⌨️代码实现

🧵分段代码

 🔯P0:调用库文件

 🔯P1:定义数组存储变量

 🔯P2:初始化

 🔯P3:查询元素的根结点

 🔯P4:合并集合

 🔯P5:查询结点是否在同一集合

 🔯P6:main函数

🧵完整代码

 🔯P0:完整代码

 🔯P1:执行结果

🔚结语


🦮思维导图 

备注:

  • 思维导图为整理王道教材第5章 查找的所有内容;
  • 本篇仅涉及到哈夫曼树HuffmanTree的代码;
  • 本章节往期博文,涉及到树与二叉树的内容如下~
    • 🌸[树:双亲、孩子、兄弟表示法][二叉树:先序、中序、后序遍历]
    • 🌸数据结构05:树与二叉树[C++][线索二叉树:先序、中序、后序]
    • 🌸数据结构05:树与二叉树[C++][哈夫曼树HuffmanTree]

🧵基本概念

⏲️定义

首先,让我们来了解一下并查集是什么。并查集是一种可以维护一些元素之间等价关系的数据结构。说得直白一点,就是可以用来管理一些“兄弟关系”。

并查集的核心操作:查找和合并。(1)查找操作就像是问某个过路人:“请问你是他兄弟吗?”(2)而合并操作就像在说:“以后我们就是兄弟了!”

  • 查找(Find):查找元素所属的集合,即找到该集合的代表元素。通过递归或迭代,可以找到代表元素。
  • 合并(Union):将两个集合合并成一个集合。首先找到两个集合的代表元素,然后将其中一个集合的代表元素指向另一个集合的代表元素。

并查集的优化技巧:路径压缩和按秩合并。(1)路径压缩就像是一个人在寻找他的老大时,叫上自己所有的兄弟,堵在家族大佬的门口。(2)而按秩合并则像是一个家族中的老大,他把一些小弟合并到自己的家族中,这样可以保证家族的规模不会太大,从而优化查询和合并的效率。

  • 路径压缩(path compression):在查找操作 find 中,通过将当前节点的父节点设置为根节点来压缩路径。这样可以使得树的高度变得更小,从而加快后续查找操作的速度。
  • 按秩合并(union by rank):在合并操作 unite 中,通过根据集合的大小(秩)决定合并方向,将较小的集合合并到较大的集合上。这样可以避免将较大的树合并到较小的树上,从而保持整体树的平衡性,减少查找操作的平均时间复杂度。

以上的举栗可能过于抽象,且术语可能过于拗口。下面我们以代码+配图的方式说明如何创建并查集~


图源:文心一言

⌨️代码实现

🧵分段代码

 🔯P0:调用库文件

  • 输入输出流文件iostream{用于输入与输出};
  • 动态数组的向量文件vector{用于创造元素的集合};
#include <iostream>
#include <vector>

 🔯P1:定义数组存储变量

  • 向量parent:存储元素的父结点;
  • 向量size:存储以当前结点为根结点的子集合数量;

备注:个人还是习惯把向量叫做动态数组,因为实现的逻辑确实与动态的数组很类似~

    std::vector<int> parent;    // parent数组 用于存储每个元素的父节点
    std::vector<int> size;      // size数组 用于存储每个集合的大小,即集合中元素的数量

 🔯P2:初始化

  • 向量parent初始化:长度为在main函数中定义的长度,每个元素的值为1,表示当前结点的子集合数量为n;
  • 向量size初始化:长度为在main函数中定义的长度,每个元素的值为自身,表示当前结点的父结点为自身;

说明1:执行的效果大概如下图所示~

说明2:

  • size初始化为1,是因为此时的结点没有从属关系,每个结点都没有孩子;
  • parent初始化为自身,可能与后面介绍的find函数使用路径压缩频繁递归调用自身有关~
    UnionFind(int n) {                  // 初始化并查集,长度为n
        parent.resize(n);               // 定义 parent数组长度为n
        size.resize(n, 1);              // 定义 size数组长度为n,每个元素的值为1
        for (int i = 0; i < n; ++i) {   // 定义 parent数组的父结点为自身
            parent[i] = i;
        }
    }

 🔯P3:查询元素的根结点

这段代码的功能是查询某个集合内的祖先结点,核心是“parent[x] = find(parent[x]);”:

  • 若仅执行 find(parent[x]); 会递归查询父结点,可以完成查询功能,不会改变树的结构,是最最常规的操作~
  • 若执行 parent[x] = find(parent[x]); 会递归查询父结点,不仅可以完成查询功能,而且会将本结点祖先们的父结点{parent}都修改为树的根结点的值~

这样做,由于会调整树形,因此本次查询的操作时间实际会略微增长,但是在下次查询时的次数就会变短~

我们举个栗子,在一棵瘦树上查找结点3:

  • 左侧是普通查询:查询结点3每次都会是3次;
  • 右侧是压缩路径:查询结点3第1次是3次{同普通查询},然后将路径上的点都挂在根结点下,之后每次查询结点3{甚至是同路径上的结点2、结点1}都会是1次~ 
    int find(int x) {                       // 查询集合的根
        if (x != parent[x]) {                // 若{查询的元素 ≠ 父结点}
            parent[x] = find(parent[x]);     // 递归调用本函数,将本函数到根结点路径上的所有结点都挂在最末端的结点(即根结点)下
        }
        return parent[x];   

说实话,这段代码我理解了很久,因为涉及到递归调用栈,因此像我一样有点迷糊的同学可以替换成下面这段代码进行测试——

    int find(int x) {
        std::cout << "传入参数find(x=" << x << ")" << std::endl;
        if (x != parent[x]) {
            std::cout << "parent[x]=" << parent[x] << ", x != parent[x]" << std::endl;
            parent[x] = find(parent[x]);
            std::cout << "将结点" << x << "的parent设置为结点" << parent[x] << std::endl;
        } else {
            std::cout << "parent[x]=" << parent[x] << ", x == parent[x]" << std::endl;
            std::cout << "返回parent[x] = " << parent[x] << std::endl;
        }
        return parent[x];
    }

就会很清楚地看到栈是怎么运行的~

 🔯P4:合并集合

这段代码的功能是传入两个元素x、y,使用找到两个集合的根rootx、rooty{在寻找的过程中调用了find函数,因此也会执行路径压缩的操作},然后将小树{元素更少的树}合并到大树{元素更多的树}下~

为什么这样合并呢?因为树的层数直接决定了查找与合并的效率。如果频繁地把大树合并到小树的下面,那么这棵树肯定会倾向于又瘦又高,不利于我们执行频繁的查询操作~

    void unite(int x, int y) {              // 合并集合
        int rootX = find(x);                 // 寻找x元素所在集合的根 rootx
        int rootY = find(y);                 // 寻找y元素所在集合的根 rooty
        if (rootX != rootY) {                   // 若{两个集合的根不等}
            if (size[rootX] < size[rootY]) {   // 若{集合x的元素数量 < 集合y的元素数量}
                parent[rootX] = rootY;          // 将x元素的祖先结点rootx 挂在 y元素的祖先结点rooty 的后代位置
                size[rootY] += size[rootX];    // 集合y的元素数量 = 集合y的元素数量 + 集合x的元素数量
            } else {                            // 若不满足{集合x的元素数量 < 集合y的元素数量}
                parent[rootY] = rootX;          // 将y元素的祖先结点rooty 挂在 x元素的祖先结点rootx 的后代位置
                size[rootX] += size[rootY];    // 集合x的元素数量 = 集合x的元素数量 + 集合y的元素数量
            }
        }
    }

 🔯P5:查询结点是否在同一集合

这段代码的功能是传入两个元素x、y,使用找到两个集合的根rootx、rooty{在寻找的过程中调用了find函数,因此也会执行路径压缩的操作}:根结点相同则在同一个集合内~

    void isConnected(int x, int y) {        // 查询是否为同一集合
        if (find(x) == find(y)){            // 若{x元素所在集合的根 = y元素所在集合的根}
            std::cout << x << "与" << y << "属于同一集合" << std::endl;    // 输出x与y属于同一集合
        }else{                               // 若不满足{x元素所在集合的根 = y元素所在集合的根}
            std::cout << x << "与" << y << "不属于同一集合" << std::endl;  // 输出x与y不属于同一集合
        }

    }

 🔯P6:main函数

main函数除了P0~P5的函数调用,就创建了3棵小树,进行了并、查操作,如果我没有理解错,完整的过程应该是这样的~

  • 第1步:设定数组元素、初始化;
  • 第2步:经过元素合并{main函数从合并(0,6)执行到合并(2,5)},创建如图所示的3棵小树,注意创建时,尺寸size数组和父结点parent数组都会更改~

  • 第3~4步:合并(4,8)(2,9)即将3棵树合并为1棵大树,如果不执行路径压缩与按秩合并,结果应该是下面这样的;

根据上图的话,如果查询根结点,结点1、3、5都会查询1次, 结点0、4、9都会查询2次,  结点0、4、9都会查询3次,平均下来就是平均每个结点查询2次~

  • 第3~4步:合并(4,8)(2,9)即将3棵树合并为1棵大树,如果执行路径压缩与按秩合并,结果应该是下面这样的;

 根据上图的话,如果查询根结点,结点6、7、8、1、9、2都会查询1次, 结点4、3、5都会查询2次,平均下来就是平均每个结点查询1.3次~

相比不优化,平均每个结点节省了35%的效率~当然如果元素越多,节省的查询效率也就越多哦~

int main() {
    int n = 10;          // 设置数组共有10个元素
    UnionFind uf(n);    // 初始化

    uf.unite(0, 6);     // 合并集合{0,6}
    uf.unite(0, 7);     // 合并集合{0,7}
    uf.unite(0, 8);     // 合并集合{0,8}

    uf.unite(1, 4);
    uf.unite(1, 9);

    uf.unite(2, 3);
    uf.unite(2, 5);

    // uf.isConnected(0, 6);  // 输出 0,不属于同一个集合
    // uf.isConnected(0, 9);  // 输出 1,属于同一个集合

    uf.unite(4, 8);      // 合并集合{8,1}
    uf.unite(3, 9);      // 合并集合{3,9}

    uf.isConnected(0, 5);  // 输出 1,属于同一个集合

    return 0;
}

🧵完整代码

 🔯P0:完整代码

为了凑本文的字数,我这里贴一下整体的代码,删掉了细部注释~

#include <vector>
#include <iostream>

// UnionFind类,用于实现并查集算法
class UnionFind {
// 私有变量
private:
    std::vector<int> parent;    //成员变量,用于存储每个元素的父节点
    std::vector<int> size;      //成员变量,用于存储每个集合的大小,即集合中元素的数量

// 公共变量
public:
    // 初始化并查集,参数 n 表示集合中元素的数量
    UnionFind(int n) {
        parent.resize(n);
        size.resize(n, 1);
        for (int i = 0; i < n; ++i) {
            parent[i] = i;
        }
    }

    // 查询集合的根,参数 x 表示要查询的元素
    int find(int x) {
        if (x != parent[x]) {
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }

    // 合并集合,参数 x 和 y 表示要合并的两个元素
    void unite(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        if (rootX != rootY) {
            if (size[rootX] < size[rootY]) {
                parent[rootX] = rootY;
                size[rootY] += size[rootX];
            } else {
                parent[rootY] = rootX;
                size[rootX] += size[rootY];
            }
        }
    }

    // 查询是否为同一集合,参数 x 和 y 表示要查询的两个元素
    void isConnected(int x, int y) {
        if (find(x) == find(y)){
            std::cout << x << "与" << y << "属于同一集合" << std::endl;
        }else{
            std::cout << x << "与" << y << "不属于同一集合" << std::endl;
        }

    }
};

int main() {
    int n = 10;
    UnionFind uf(n);

    uf.unite(0, 6);
    uf.unite(0, 7);
    uf.unite(0, 8);

    uf.unite(1, 4);
    uf.unite(1, 9);

    uf.unite(2, 3);
    uf.unite(2, 5);

    uf.isConnected(0, 6);
    uf.isConnected(0, 9);

    uf.unite(4, 8);
    uf.unite(3, 9);

    uf.isConnected(0, 5);
    std::cout << "x = 4的根是:" << uf.find(4) << std::endl;

    return 0;
}

 🔯P1:执行结果

运行结果如下图所示~


🔚结语

博文到此结束,写得模糊或者有误之处,欢迎小伙伴留言讨论与批评,督促博主优化内容~😶‍🌫️😶‍🌫️博文若有帮助,欢迎小伙伴动动可爱的小手默默给个赞支持一下~🌟🌟

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

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

相关文章

会议邀请|思腾合力邀您共赴MICS 2023第十届医学图像计算青年研讨会

医学图像计算青年研讨会 (Medical Imaging Computing Seminar, MICS) 于2014年成立&#xff0c;宗旨是为医学影像分析领域的全球华人学者提供学术交流平台&#xff0c;增进本领域科研人员和医学专家的交流和合作。 第十届医学图像计算青年研讨会 (MICS 2023) 将于2023年7月14-1…

form表单禁止浏览器自动填充密码

因为用户修改密码的时候,谷歌浏览器、edge等浏览器,总是自动将保存的密码填充到重置密码输入框中,给用户使用带来困扰。原因是因为你在登录的时候选择记住了账号和密码了,所以就会把信息存在浏览器里面,当你在修改密码的时候,由于form表单的 type="password" 所…

【Arduino小车实践】PID算法简介

一、介绍 1. 特点 流量稳定、改变流量&#xff08;水阀&#xff09;&#xff1a;测量当前流量&#xff0c;与预期流量进行比对&#xff0c;不相等则进行相应的调整。 2. 适用系统 适用线性系统&#xff08;二阶以内的线性系统&#xff09;&#xff1a;齐次性、叠加性 3. 宏…

ASEMI-A7二极管可用什么代替,M7二极管正负极判断

编辑-Z 在电子设备的世界中&#xff0c;二极管是最常见的组件之一。它们在各种设备中发挥着重要的作用&#xff0c;包括电源适配器、电源供应器、电池充电器等。在这篇文章中&#xff0c;我们将探讨A7二极管可用什么代替&#xff0c;以及M7二极管正负极判断。 首先&#xff0c…

解决Vue 报错error:0308010C:digital envelope routines::unsupported问题

解决Vue 报错error:0308010C:digital envelope routines::unsupported问题 问题原因方法一&#xff1a;修改配置方法二&#xff1a;尝试卸载Node.js 17版本并重新安装Node.js 16版本&#xff0c;然后再重新启动方法三&#xff1a;package.json增加配置&#xff08;大多数解决问…

【自动驾驶汽车量子群粒子过滤器】用于无人驾驶汽车列车定位的量子粒子滤波研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

Doris单机安装部署

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、前期准备1.下载安装包2.修改limits文件 二、安装1.配置启动FE2.配置启动BE3.web测试4.连接测试、添加BE 结尾 前言 Apache Doris 是一个基于 MPP 架构的高…

Linux搭建PXE服务器

安装PXE启动服务器所需的软件包 Tftp&#xff0c;dhcp和vsftpd&#xff08;httpd也可以作为安装源&#xff09; 编辑TFTP配置文件 重启tftp服务 编辑/etc/dhcp/dhcpd.conf,新建作用域&#xff0c;主要设置下一跳服务器地址和引导文件名称。 重启dhcp服务 把centos光盘内容拷贝…

营销本地化,跨境电商都要懂的行业知识

营销本地化是调整营销内容以满足目标市场的文化、语言和其他相关差异的过程&#xff0c;这对于那些希望在全球市场取得成功的企业至关重要。它可以帮助公司与客户建立有意义的联系&#xff0c;并提供优质的客户体验。随着技术和互联网的进步&#xff0c;企业可以获得大量资源、…

基于单片机的厨房安全监测系统的设计与实现

功能介绍 以STM32/51单片机作为主控系统&#xff1b; OLED液晶显示当前检测的气体浓度&#xff0c;温度&#xff0c;是否有火等信息&#xff1b; 按键可以设置温度上限、可燃气体浓度上限&#xff1b; 当温度超过我们设置自动开启风扇进行降温&#xff1b; 当检测到天然气泄露后…

【误差自适应跟踪方法AUV】自适应跟踪(EAT)方法研究(Matlab代码Simulin实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

使用wireshark抓包,验证feign http请求的数据透传结果

一、背景 在灰度部署、A/B测试、单元化部署等场景下&#xff0c;微服务服务之间的调用&#xff0c;要求我们对上游服务给过来的数据进行透传至下游服务。 如果是灰度部署&#xff0c;需要对http请求进行染色&#xff0c;http header头部增加灰度标识&#xff0c;然后传递给下…

掌握std::move和std::forward

在讲解std::move和std::forward之前&#xff0c;我们必须先了解C中左值、右值的相关概念。 1、左值、右值 &#xff08;1&#xff09;左值&#xff1a;一般指的是在内存中有对应的存储单元的值&#xff0c;最常见的就是程序中创建的变量。 &#xff08;2&#xff09;右值&…

热泵空调渗透率超10%,哪三家本土供应商领跑市场

作为新能源汽车产业链的潜在高增长赛道&#xff0c;以及整车热管理系统的关键组成部分&#xff0c;车用热泵空调市场近年来呈现快速发展的态势。 而从新能源整车热管理角度来看&#xff0c;通过制冷及热泵技术的完善&#xff0c;发展低碳的热管理技术&#xff0c;从而提升热管…

基于Hadoop生态实现离线与实时的消费者商品交易行为分析(消费行为分析、购买偏好分析)

项目背景 大数据专业综合项目实践&#xff0c;数据集采用阿里天池的公开数据集&#xff0c;下载链接: 消费者商品交易调研清单 这个数据集是一个样本集&#xff0c;共有5000多条记录&#xff0c;每条记录代表一个消费者的商品交易调研信息。以下是对每个字段的描述&#xff1a…

什么是卷积神经网络——CNN

卷积神经网络(CNN) PS&#xff1a; 卷积神经网络 主要包括&#xff1a;输入层、卷积层、池化层、全连接层 1. 卷积神经网络结构介绍 如果用全连接神经网络处理大尺寸图像具有三个明显的缺点&#xff1a; &#xff08;1&#xff09;首先将图像展开为向量会丢失空间信息&…

【C#】Kestrel和IIS服务器下的同步与异步配置

最近在回看自己写的代码时&#xff0c;发现服务配置里最开头写了两段代码&#xff0c;第一感觉&#xff0c;这是啥功能&#xff0c;太久有点生疏了&#xff0c;经过一顿搜索和回顾&#xff0c;简单整理如下 目录 1、Kestrel服务器1.1、跨平台1.2、高性能1.3、可扩展性1.4、安全…

Hadoop 3.2.4 本机伪分布式安装

Hadoop 3.2.4 伪分布式安装 文章目录 Hadoop 3.2.4 伪分布式安装前言配置ssh免密登录下载安装包解压并调整配置文件解压安装包到当前位置调整配置文件hadoop-env.shyarn-env.shcore-site.xmlhdfs-site.xmlmapred-site.xmlyarn-site.xmlworkers 配置 启动验证启动与命令查验web页…

装机——固态硬盘的选择

文章目录 问题描述知识学习硬盘分类PCIe接口SATA接口M.2接口 通道PCI-E通道SATA通道SAS通道FC通道 通信协议IDE协议AHCI协议NVMe协议 硬盘参数表主控存储颗粒SLCMLCTLCQLC失败的颗粒&#xff08;需要购买原装厂商的存储颗粒&#xff09; 问题解决问题总结 问题描述 女朋友笔记本…

Spark(20):SparkStreaming之概述

目录 0. 相关文章链接 1. Spark Streaming 是什么 2. Spark Streaming 的特点 2.1. 易用 2.2. 容错 2.3. 易整合到Spark体系 3. Spark Streaming 架构 3.1. 架构图 3.2. 背压机制 0. 相关文章链接 Spark文章汇总 1. Spark Streaming 是什么 Spark 流使得构建可扩展的…