高阶数据结构——图

embedded/2024/11/17 14:04:00/

 


文章目录

目录

文章目录

前言

一、并查集

1、并查集概念

2、并查集的实现

二、图的概念

三、邻接矩阵、邻接表的存储

1、邻接矩阵

写过注释了,就不展开解释了。

2、邻接表

四、图的遍历

1、广度优先遍历

2、深度优先遍历

​编辑


前言


一、并查集

1、并查集概念

在一些应用问题中,需要将n个不同的元素划分成一些不相交的集合。开始时,每个元素自成一个 单元素集合,然后按一定的规律将归于同一组元素的集合合并。在此过程中要反复用到查询某一 个元素归属于那个集合的运算。适合于描述这类问题的抽象数据类型称为并查集(union-find set)。 

        

如果说有一个森林,我们想要将它在数组中找到对应的关系因该怎么做呢?

从上图可以看出:编号6,7,8同学属于0号小分队,该小分队中有4人(包含队长0);编号为4和9的同学属于1号小分队,该小分队有3人(包含队长1),编号为3和5的同学属于2号小分队,该小分队有3个人(包含队长1)。 

从上图可以看出:编号6,7,8同学属于0号小分队,该小分队中有4人(包含队长0);编号为4和9的同学属于1号小分队,该小分队有3人(包含队长1),编号为3和5的同学属于2号小分队,该小分队有3个人(包含队长1)。

仔细观察数组中内融化,可以得出以下结论:

1. 数组的下标对应集合中元素的编号

2. 数组中如果为负数,负号代表根,数字代表该集合中元素个数

3. 数组中如果为非负数,代表该元素双亲在数组中的下标

现在0集合有7个人,2集合有3个人,总共两个朋友圈。

通过以上例子可知,并查集一般可以解决一下问题:

1. 查找元素属于哪个集合

沿着数组表示树形关系以上一直找到根(即:树中中元素为负数的位置)

2. 查看两个元素是否属于同一个集合

沿着数组表示的树形关系往上一直找到树的根,如果根相同表明在同一个集合,否则不在3. 将两个集合归并成一个集合

两个集合中的元素合并

将一个集合名称改成另一个集合的名称

4. 集合的个数

遍历数组,数组中元素为负数的个数即为集合的个数

2、并查集的实现

原理:下标对应的数是负数就为父亲节点;然后让孩子的值加到父亲上去,孩子节点记录父亲节点的下标;

代码实现:

#pragma once
#include<vector>
#include<cstdlib>
#include<map>
#include<iostream>
using namespace std;class UnionFindSet
{
public:UnionFindSet(size_t n):_Set(n,-1){}//映射关系//下标等价于名字,下标存储的值为根size_t FindRoot(int x){int root = x;while (_Set[root]>0){root = _Set[root];}return root;}//合并两棵树void Union(int x1, int x2){//合并两个数,将它们的根节点合并在一起;将节点少的的合并到节点多的里面、size_t root1 = FindRoot(x1);size_t root2 = FindRoot(x2);//找到了两数的根,将两个数的根合并//找对比节点个数,root对应的下标的绝对值为节点个数if (abs(_Set[root1]) < abs(_Set[root2])){swap(root1, root2);}//root1大于root2//将root2加到root1//root2的指向root1_Set[root1] += _Set[root2];_Set[root2] = root1;}//统计根的个数size_t SetCount(){int count = 0;for (int i = 0; i < _Set.size(); i++){if (_Set[i] < 0) count++;}return count;}
private:vector<int> _Set;//定于一个数组
};
void TestUFS()
{UnionFindSet u(10);u.Union(0, 6);u.Union(7, 6);u.Union(7, 8);u.Union(1, 4);u.Union(4, 9);u.Union(2, 3);u.Union(2, 5);cout << u.SetCount() << endl;
}

代码都有注释,就不再多数了。 

相关力扣题目

省份数量

二、图的概念

图是由顶点集合及顶点间的关系组成的一种数据结构:G = (V, E),其中:

顶点集合V = {x|x属于某个数据对象集}是有穷非空集合;

E = {(x,y)|x,y属于V}或者E = {|x,y属于V && Path(x, y)}是顶点间关系的有穷集合,也叫 做边的集合。

(x, y)表示x到y的一条双向通路,即(x, y)是无方向的;Path(x, y)表示从x到y的一条单向通路,即 Path(x, y)是有方向的。

顶点和边:图中结点称为顶点,第i个顶点记作vi。两个顶点vi和vj相关联称作顶点vi和顶点vj之间 有一条边,图中的第k条边记作ek,ek = (vi,vj)或。

有向图和无向图:在有向图中,顶点对是有序的,顶点对称为顶点x到顶点y的一条 边(弧),和是两条不同的边,比如下图G3和G4为有向图。

在无向图中,顶点对(x, y) 是无序的,顶点对(x,y)称为顶点x和顶点y相关联的一条边,这条边没有特定方向,(x, y)和(y,x) 是同一条边,比如下图G1和G2为无向图。注意:无向边(x, y)等于有向边和。

完全图:在有n个顶点的无向图中,若有n * (n-1)/2条边,即任意两个顶点之间有且仅有一条边, 则称此图为无向完全图,比如上图G1;在n个顶点的有向图中,若有n * (n-1)条边,即任意两个 顶点之间有且仅有方向相反的边,则称此图为有向完全图,比如上图G4。

邻接顶点:在无向图中G中,若(u, v)是E(G)中的一条边,则称u和v互为邻接顶点,并称边(u,v)依 附于顶点u和v;在有向图G中,若是E(G)中的一条边,则称顶点u邻接到v,顶点v邻接自顶 点u,并称边与顶点u和顶点v相关联。

顶点的度:顶点v的度是指与它相关联的边的条数,记作deg(v)。在有向图中,顶点的度等于该顶 点的入度与出度之和,其中顶点v的入度是以v为终点的有向边的条数,记作indev(v);顶点v的出度 是以v为起始点的有向边的条数,记作outdev(v)。因此:dev(v) = indev(v) + outdev(v)。

注 意:对于无向图,顶点的度等于该顶点的入度和出度,即dev(v) = indev(v) = outdev(v)。

路径:在图G = (V, E)中,若从顶点vi出发有一组边使其可到达顶点vj,则称顶点vi到顶点vj的顶 点序列为从顶点vi到顶点vj的路径。

路径长度:对于不带权的图,一条路径的路径长度是指该路径上的边的条数;对于带权的图,一 条路 径的路径长度是指该路径上各个边权值的总和。

简单路径与回路:若路径上各顶点v1,v2,v3,…,vm均不重复,则称这样的路径为简单路 径。若路 径上第一个顶点v1和最后一个顶点vm重合,则称这样的路径为回路或环。

子图:设图G = {V, E}和图G1 = {V1,E1},若V1属于V且E1属于E,则称G1是G的子图

连通图:在无向图中,若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任 意一 对顶点都是连通的,则称此图为连通图。

强连通图:在有向图中,若在每一对顶点vi和vj之间都存在一条从vi到vj的路径,也存在一条从vj 到 vi的路径,则称此图是强连通图。

生成树:一个连通图的最小连通子图称作该图的生成树。有n个顶点的连通图的生成树有n个顶点 和n-1条边

三、邻接矩阵、邻接表的存储

因为图中既有节点,又有边(节点与节点之间的关系),因此,在图的存储中,只需要保存:节点和 边关系即可。节点保存比较简单,只需要一段连续空间即可,那边关系该怎么保存呢?

1、邻接矩阵

因为节点与节点之间的关系就是连通与否,即为0或者1,因此邻接矩阵(二维数组)即是:先用一 个数组将定点保存,然后采用矩阵来表示节点与节点之间的关系。

注意: 1. 无向图的邻接矩阵是对称的,第i行(列)元素之和,就是顶点i的度。有向图的邻接矩阵则不一 定是对称的,第i行(列)元素之后就是顶点i 的出(入)度。

2. 如果边带有权值,并且两个节点之间是连通的,上图中的边的关系就用权值代替,如果两个 顶点不通,则使用无穷大代替

3. 用邻接矩阵存储图的有点是能够快速知道两个顶点是否连通,缺陷是如果顶点比较多,边比 较少时,矩阵中存储了大量的0成为系数矩阵,比较浪费空间,并且要求两个节点之间的路 径不是很好求。

//邻接矩阵
namespace Matrix
{template<class V,class W ,W MAX_W = INT_MAX, bool Direction = false >class Graph{public:typedef Graph<V, W, MAX_W, Direction> Self;Graph() = default;Graph(const V* vertexs, size_t n){for (int i = 0; i < n; i++){_vertexs.push_back(vectexs[i]);//将名字存进来_vIndexMap[vertexs[i]] = i;//将名字的下标映射出来}_matrix.resize(n);//开辟n*n矩阵for (auto e : _matrix){e.resize(n, MAX_W);//初始化为最大值,也就是无边}}size_t GetVertexIndex(const V& v){auto ret = _matrix.find(v);//在map里面找v的映射的下标if (ret != _matrix.end()){return ret->secont;}else{throw invalid_argument("不存在顶点");return -1;}}void _AddEdge(size_t srci, size_t dsti, const W& w){_matrix[srci][dsti] = w;if (Direction == false)//说明无向边{_matrix[dsti][srci] = w;}}void AddEdge(const V& src, const V& dst, const W& w){//连接两个节点//向知道他们的下标size_t srci = GetVertexIndex(src);size_t dsti = GetVertexIndex(dat);//然后存储到矩阵中_AddEdge(srci, dsti, w);}void BFS(const V& src){}void _DFS(size_t srcIndex, vector<bool>& visited){}void DFS(const V& src){}void Print(){}private://需要一个矩阵存储元素之间的关系vector<vector<W>> _matrix;//存储需要存储的数据名,因为矩阵的下标都是数字vector<V> _vertexs; //映射下标,用来通过名字找下标的map<V, int> _vIndexMap;};
}

写过注释了,就不展开解释了。

2、邻接表

邻接表:使用数组表示顶点的集合,使用链表表示边的关系。

1. 无向图邻接表存储

注意:无向图中同一条边在邻接表中出现了两次。如果想知道顶点vi的度,只需要知道顶点 vi边链表集合中结点的数目即可。

2. 有向图邻接表存储

注意:有向图中每条边在邻接表中只出现一次,与顶点vi对应的邻接表所含结点的个数,就 是该顶点的出度,也称出度表,要得到vi顶点的入度,必须检测其他所有顶点对应的边链 表,看有多少边顶点的dst取值是i。

// 临接表
namespace LinkTable
{//邻接表的结构和哈希桶相似template<class W>struct LinkEdge{int _srcIndex;int _dstIndex;W _w;LinkEdge<W>* _next;LinkEdge(const W& w): _srcIndex(-1), _dstIndex(-1), _w(w), _next(nullptr){}};template<class V, class W, bool Direction = false>class Graph{public:Graph(const V* vertexs, size_t n)//和邻接矩阵初始化一样{_vertexs.reserve(n);for (size_t i = 0; i < n; ++i){_vertexs.push_back(vertexs[i]);_vIndexMap[vertexs[i]] = i;}_linkTable.resize(n, nullptr);}size_t GetVertexIndex(const V& v)//找名字的下标{auto ret = _vIndexMap.find(v);if (ret != _vIndexMap.end()){return ret->second;}else{throw invalid_argument("不存在的顶点");return -1;}}void AddEdge(const V& src, const V& dst, const W& w){//找到两个名字映射的下标size_t srcindex = GetVertexIndex(src);size_t dstindex = GetVertexIndex(dst);//有向边 // 0 1Edge* sd_edge = new Edge(w);//实例化节点sd_edge->_srcIndex = srcindex;sd_edge->_dstIndex = dstindex;sd_edge->_next = _linkTable[srcindex];_linkTable[srcindex] = sd_edge;// 1 0// 无向图if (Direction == false){Edge* ds_edge = new Edge(w);ds_edge->_srcIndex = dstindex;ds_edge->_dstIndex = srcindex;ds_edge->_next = _linkTable[dstindex];_linkTable[dstindex] = ds_edge;}}private:map<string, int> _vIndexMap;	//通过字符串找下标vector<V> _vertexs;			 // 顶点集合vector<Edge*> _linkTable;    // 边的集合的临接表};void TestGraph(){string a[] = { "张三", "李四", "王五", "赵六" };Graph<string, int> g1(a, 4);g1.AddEdge("张三", "李四", 100);g1.AddEdge("张三", "王五", 200);g1.AddEdge("王五", "赵六", 30);}
}

原理和哈希桶一样

哈希桶

四、图的遍历

给定一个图G和其中任意一个顶点v0,从v0出发,沿着图中各边访问图中的所有顶点,且每个顶 点仅被遍历一次。"遍历"即对结点进行某种操作的意思。

请思考树以前是怎么遍历的,此处可以直接用来遍历图吗?为什么?

1、广度优先遍历

大家看这两张图片还有遍历这个概念有没有想到树的遍历,我们树分为前、中、后序遍历,还有层序遍历,而这个广度优先遍历就有层序遍历的感觉。

其实二叉树就是特殊的图,而不能说图就是树。

回忆树的层序遍历,思考:如何防止节点被重复遍历

力扣的二叉树的层序遍历:

102. 二叉树的层序遍历

代码: 

class Solution {
public:vector<vector<int>> levelOrder(TreeNode* root) {int sizen=0;queue<TreeNode*> q;if(root){q.push(root);sizen++;}vector<vector<int>> vv;int leve=0;while(!q.empty()){vector<int> v;while(sizen--){TreeNode* front=q.front();q.pop();v.push_back(front->val);if(front->left){q.push(front->left);}if(front->right){q.push(front->right);}}vv.push_back(v);sizen=q.size();}return vv;}
};

原理我们运用了队列,出栈时,将自己的孩子节点带进队列;

好了回归广度优先遍历

	void BFS(const V& src){size_t srcindex = GetVertexIndex(src);vector<bool> visited;//创建一个数组用来记录哪些数据是被访问过的,避免了重复访问visited.resize(_vertexs.size(), false);queue<int> q;//创建队列q.push(srcindex);//想让根节点出队列visited[srcindex] = true;//根被访问过了标记一下size_t d = 1;size_t dSize = 1;//核心算法while (!queue.empty()){printf("%s的%d度好友:", src.c_str(), d);while (dSize--){size_t front = q.front();//记录队头下表,也就是出队元素下标q.pop();for (size_t i = 0; i < _vertexs.size(); ++i){if (visited[i] == false && _matrix[front][i] != MAX_W)//如果没有被访问,并且是连接节点就入队列{printf("[%d:%s] ", i, _vertexs[i].c_str());visited[i] = true;//访问过了标记q.push(i);//}}}}cout << endl;dSize = q.size();++d;cout << endl;}

2、深度优先遍历

这就是树的递归遍历的思想

		void _DFS(size_t srci, vector<bool>& visited){cout << srci << ":" << _vertexs[srci] << endl;//打印节点信息visited[srci] = true;//访问记录// 找一个srci相邻的没有访问过的点,去往深度遍历for (size_t i = 0; i < _vertexs.size(); ++i){if (_matrix[srci][i] != MAX_W && visited[i] == false)//遍历数组所对应的行,也就有边的节点,看那么没有访问{_DFS(i, visited);}}}void DFS(const V& src){size_t srci = GetVertexIndex(src);//找下标vector<bool> visited(_vertexs.size(), false);//建立标识表,用于记录访问过的节点_DFS(srci, visited);}

 



http://www.ppmy.cn/embedded/138268.html

相关文章

基于微信小程序的在线学习平台+LW示例参考

1.项目介绍 系统角色&#xff1a;管理员、普通用户功能模块&#xff1a;管理员&#xff08;用户管理、名师管理、视频管理、在线学习管理、论坛交流、试卷管理、试题管理、考试管理等&#xff09;&#xff0c;普通用户&#xff08;查看相关信息、学习、考试相关等功能&#xf…

开启鸿蒙开发之旅:交互——点击事件

前一节我们写好了”提醒事项“APP首页的静态页面&#xff0c;光有静态页面肯定是不够的&#xff0c;更重要的是交互&#xff0c;今天这一节我将来学习最常用的交互事件——点击事件。 点击事件 说明&#xff1a;组件被点击时触发的事件 作用&#xff1a;监听&#xff08;感知&a…

【STM32】项目实战——OV7725/OV2604摄像头颜色识别检测(开源)

本篇文章分享关于如何使用STM32单片机对彩色摄像头&#xff08;OV7725/OV2604&#xff09;采集的图像数据进行分析处理&#xff0c;最后实现颜色的识别和检测。 目录 一、什么是颜色识别 1、图像采集识别的一些基本概念 1. 像素&#xff08;Pixel&#xff09; 2. 分辨率&am…

PHP 语法基础

PHP 语法基础 PHP&#xff08;Hypertext Preprocessor&#xff09;是一种广泛使用的开源服务器端脚本语言&#xff0c;特别适用于网页开发&#xff0c;并且可以嵌入HTML中使用。PHP的语法混合了C、Java、Perl以及PHP自创的语法&#xff0c;易于学习和使用。本文将详细介绍PHP的…

算法定制LiteAIServer摄像机实时接入分析平台玩手机打电话检测算法:智能监控的新篇章

在现代社会&#xff0c;随着智能手机的普及&#xff0c;无论是在工作场所还是公共场所&#xff0c;玩手机或打电话的行为日益普遍。然而&#xff0c;在某些特定环境下&#xff0c;如工厂生产线、仓库、学校课堂等&#xff0c;这些行为可能会影响到工作效率、安全或教学秩序。为…

HelloMeme 上手即用教程

HelloMeme是一个集成空间编织注意力的扩散模型&#xff0c;用于生成高保真图像和视频。它提供了一个代码库&#xff0c;包含实验代码和预训练模型&#xff0c;支持PyTorch和FFmpeg。用户可以通过简单的命令行操作来生成图像和视频。 本文将详细介绍&#xff0c;如何在GPU算力租…

Python_爬虫1_Requests库入门

目录 Requests库 7个主要方法 Requests库的get()方法 Response对象的属性 爬取网页的通用代码框架 理解requests库的异常 HTTP协议及Requests库方法 HTTP协议 HTTP协议采用URL作为定位网络资源的标识。 HTTP协议对资源的操作 理解PATCH和PUT的区别 HTTP协议与Requse…

童年的快乐,矫平机为玩具打造安全品质

童年的快乐&#xff0c;矫平机为玩具打造安全品质 每个人的童年都充满了欢笑和快乐&#xff0c;玩具作为这段时光中不可或缺的伙伴&#xff0c;其安全性和品质尤为重要。矫平机在这个领域扮演着重要角色&#xff0c;它确保了玩具材料的平整和安全&#xff0c;为孩子们的童年增…