数据结构—图(上)

文章目录

  • 12.图(上)
    • (1).图的基本概念
      • #1.图的基本定义
      • #2.边的分类
      • #3.数据结构的一些规定
      • #4.子图
      • #5.完全图
      • #6.路径
      • #7.连通性和连通分量
      • #8.度
    • (2).图的存储方式
      • #1.邻接矩阵
      • #2.邻接表
    • (3).图的遍历
      • #1.深度优先搜索(Depth First Search)
        • i.走个迷宫
        • ii.DFS的思想
        • iii.代码实现
      • #2.广度优先搜索(Breadth First Search)
        • i.你爱吃蛋糕吗?
        • ii.BFS的思想
        • iii.代码实现
      • #3.求连通分量
    • 小结

12.图(上)

  图是我们在《数据结构》这门课程中遇到的最后一个类型的结构了,图是一种多对多的结构,从广义的角度上来看,实际上线性表、树这两种结构也能算作图,不过在数据结构当中我们认为图还是一个多对多的结构,那么这样的结构相比于一对一一对多的结构会更加难以表示一点

(1).图的基本概念

#1.图的基本定义

  图其实跟我们平时说的图片还是有一些区别的,一个图 G G G由顶点集 V V V和边集 E E E构成,我们一般记为 G = ( V , E ) G=(V,E) G=(V,E),例如下面的这个图:
p46

#2.边的分类

  边和点共同构成了图,一条边由两个顶点构成,我们在这张图中的边是有方向的,这种边叫做有向边,表示为 < A , B > <A,B> <A,B>;如果边是没有方向的,则称为无向边,表示为 ( A , B ) (A,B) (A,B);只有有向边的图,称为有向图,而只包含无向边的图称为无向图,在数据结构中,我们认为图要么是有向图,要么是无向图

  所以我们就可以读取一下这张图:它的顶点集 V = { V 1 , V 2 , V 3 , V 4 } V=\{V_1, V_2, V_3, V_4\} V={V1,V2,V3,V4},然后边集 E = { < V 1 , V 2 > , < V 1 , V 3 > , < V 4 , V 1 > , < V 3 , V 4 > } E=\{<V_1,V_2>, <V_1,V_3>, <V_4,V_1>,<V_3,V_4>\} E={<V1,V2>,<V1,V3>,<V4,V1>,<V3,V4>}

#3.数据结构的一些规定

  在数据结构中我们规定:顶点不能有指向自己的边,这种边称为自环,同时我们也规定,无向图的两个顶点之间不能有多于一条的边,有向图的两个顶点之间可以有两条方向相反的边,不可以有两条方向相同的边

#4.子图

  首先我们有两个图 G = ( V , E ) , G ′ = ( V ′ , E ′ ) G=(V,E), G'=(V',E') G=(V,E),G=(V,E),它们同为无向图或有向图,且满足 V ′ ⊆ V , E ′ ⊆ E V'\subseteq V, E'\subseteq E VV,EE,则称 G ′ G' G G G G子图,所以子图就是从原图中取了一部分顶点和对应的一部分边形成的新图

#5.完全图

  每对顶点之间都有一条边的无向图,称为完全无向图;而如果每对顶点 i i i j j j之间都有边 < i , j > <i,j> <i,j> < j , i > <j,i> <j,i>的有向图,称为完全有向图

#6.路径

  在无向图 G = ( V , E ) G=(V,E) G=(V,E)中,从顶点v到顶点w之间的路径是一个由顶点组成的顶点序列 ( v 0 , v 1 , . . . , v k ) (v_0, v_1,...,v_k) (v0,v1,...,vk),其中 v 0 = v , v k = w , ( v 1 , v i + 1 ) ∈ E ( G ) ( 0 ≤ j < k ) v_0=v,v_k=w,(v_1,v_{i+1})\in E(G) \ (0\leq j<k) v0=v,vk=w,(v1,vi+1)E(G) (0j<k)

  用 ( v 0 , v 1 , . . . , v k ) (v_0, v_1, ..., v_k) (v0,v1,...,vk)表示这条路径,它的长度为k,如果只有一个顶点,则称v到自身的路径长度为0,若 G G G是有向图,则路径是有方向的, < v 0 , v 1 , . . . , v k > <v_0,v_1,...,v_k> <v0,v1,...,vk>,其中 v 0 = v , v k = w , < v i , v i + 1 ∈ E ( G ) ( 0 ≤ j < k ) v_0=v,v_k=w,<v_i,v_{i+1}\in E(G) \ (0\leq j < k) v0=v,vk=w,<vi,vi+1E(G) (0j<k)

#7.连通性和连通分量

  • 如果无向图G中每对顶点v和w都有从v到w的路径,那么称无向图 G G G是连通的,无向图 G G G中的极大连通子图为G的连通分量

  • 如果有向图 G G G中每对顶点v和w都有从v到w的路径,则称有向图 G G G强连通

  • 如果有向图 G G G中每对顶点v和w,有一个由不同顶点组成的顶点序列 < v 0 , v 1 , . . . , v k > <v_0,v_1,...,v_k> <v0,v1,...,vk>,其中 v 0 = v , v k = w v_0=v,v_k=w v0=v,vk=w,且 < v i , v i + 1 > ∈ E <v_i,v_{i+1}>\in E <vi,vi+1>∈E < v i + 1 , v i > ∈ E ( 0 ≤ i < k ) <v_{i+1},v_i>\in E\ (0\leq i<k) <vi+1,vi>∈E (0i<k),那么称有向图 G G G是弱连通的

  • 强连通的有向图一定是弱连通的,有向图 G G G极大强连通子图为图 G G G强连通分量,有向图 G G G极大弱连通子图为图 G G G弱连通分量

  • 如果图 G G G中有一条路径 ( v 0 , v 1 , . . . , v k ) (v_0, v_1,...,v_k) (v0,v1,...,vk),且 v 0 = v k v_0=v_k v0=vk,那么称这条路径为回路(或环)

#8.度

  在 无向图 G G G 中,如果 v ∈ V ( G ) v\in V(G) vV(G),那么v邻接的顶点个数称为顶点v的度
  在 有向图 G G G 中,如果 v ∈ V ( G ) v\in V(G) vV(G),那么邻接到v的顶点个数为顶点v的入度,邻接于v的顶点个数称为顶点v的出度

(2).图的存储方式

  图这一部分的基本概念还是相当多的,接下来我们就要看看怎么在计算机中表示和存储一个图了,我们一般用的是邻接矩阵和邻接表两种形式

#1.邻接矩阵

  对于一个具有 n n n个顶点的无向图,定义矩阵 A n × n A_{n\times n} An×n为:
A ( i , j ) = { 1 , ( i , j ) ∈ E ( G ) , 0 , ( i , j ) ∉ E ( G ) A(i,j) = \begin{cases} 1, (i,j)\in E(G),\\ 0, (i,j)\notin E(G) \end{cases} A(i,j)={1,(i,j)E(G),0,(i,j)/E(G)
  所以可以很容易地知道,顶点i的度为:
∑ j = 1 n A ( i , j ) \sum_{j=1}^nA(i,j) j=1nA(i,j)
  此时的 A ( i , j ) A(i,j) A(i,j)仅代表两个顶点是否连通,如果是带权边,则 A ( i , j ) A(i,j) A(i,j)存储的值是两个顶点之间的边的权重值,此时 A A A定义为:
A ( i , j ) = { w i j , i ≠ j , ( i , j ) ∈ E ( G ) , 0 , i = j , ∞ A(i,j)=\begin{cases} w_{ij}, i\neq j, (i,j)\in E(G),\\ 0, i = j, \\ \infty \end{cases} A(i,j)= wij,i=j,(i,j)E(G),0,i=j,
  毕竟在C++里没有一个真正的 ∞ \infty ,所以我们一般会在程序的最前面定义一个inf用于后续的赋值操作:

constexpr int inf = 0x3f3f3f3f;

  那么对于 n n n个顶点的有向图,有:
A ( i , j ) = { 1 , < i , j > ∈ E ( G ) , 0 , < i , j > ∉ E ( G ) A(i, j) = \begin{cases} 1, <i, j>\in E(G),\\ 0, <i, j>\notin E(G) \end{cases} A(i,j)={1,<i,j>∈E(G),0,<i,j>/E(G)
  所以此时也可以比较轻松地求出顶点i的入度和出度分别为:
i d = ∑ i = 1 n A ( i , j ) , o d = ∑ j = 1 n A ( i , j ) id = \sum_{i=1}^nA(i,j), od = \sum_{j=1}^nA(i,j) id=i=1nA(i,j),od=j=1nA(i,j)
  其中id为入度,od为出度,分别是第j列的和以及第i行的和

  所以邻接矩阵的空间复杂度为 O ( n 2 ) O(n^2) O(n2),毕竟需要存储一个 n × n n\times n n×n的矩阵嘛,邻接矩阵存储图的方式比较方便,对我们来说比较直观,但是假设图中的边数非常少(称为稀疏图),这时候就会有非常多的空间浪费,所以我们还有另一种存储方法—邻接表

#2.邻接表

  邻接表的想法很简单:既然图是由点和边构成的,那么我们对应每个点,把由它为起点的所有边全都存储起来,这样一来,我们就可以有效地减少没有边的部分的空间浪费,当然,如果我们的图足够稠密,邻接表的存法就不如邻接矩阵了,我们看看邻接表的一般定义:

#include <vector>
constexpr int MAXN = 5e3 + 20; // 最大结点数量
struct edge
{int end; // 终点int w;   // 边权
};vector<edge> g1[MAXN]; // 带权图
vector<int> g2[MAXN];  // 无权图

  带权图和无权图最主要的区别就在边权,所以存储带权图的时候我们需要在边上附带上边权的属性,那么无权图当然是没有必要的了

  到这儿你应该也能看出来为什么邻接表更多用于存储稀疏图了,因为当图足够稠密的时候,邻接表就会退化成为邻接矩阵

  因为我们是基于C++的数据结构博客,所以直接用vector实现会很方便,如果你使用C语言,我们可能还需要用到链表,这里大概介绍一下利用链表实现的邻接表:

constexpr int MANX = 5e3 + 20;
struct node
{int end;int w;node* next;
};// 此处忽略若干关于链表的操作定义
using list = node*;list g[MAXN]; // 邻接链表

  无向图和有向图都可以用邻接表来存储,不过如果是无向图,我们对于一条边需要存储两次,例如:

// n为顶点数,m为边数
for (int i = 1; i <= m; i++) {int u, v, w;cin >> u >> v >> w; // 无向边g[u].push_back({v, w}); // u->vg[v].push_back({u, w}); // v->u
}

  因为是无向边,两边实际上都要连起来,否则如果只有一边就会导致两个顶点中单向连通,在后续会出现非常多问题

(3).图的遍历

  就像线性表的遍历、树的遍历,我们存储了一个图之后,首先也要能够把整张图遍历一遍,至少要把所有节点扫一遍吧,那么这就是个技术活了,线性表和树的遍历都可以保证不会走重复的路,但是图的结构太复杂,让我们来走都不一定能保证不重不漏,所以就需要一些方法,DFS和BFS就是两种常见的方法

#1.深度优先搜索(Depth First Search)

i.走个迷宫

  走迷宫的时候有一种原则叫做右手法则,当然不是电磁学的那个,而是说,我们在走迷宫的时候始终沿着右手的方向走,最后总是能够走到出口,当然,因为遍历的那个顶点不是人,它其实不存在左右的的概念,不过我们可以借鉴一下这个过程

ii.DFS的思想

  比如说,我们走到了某个顶点,就向可以走的所有顶点做一个试探,直到走到了死路,我们就认为这次试探结束,然后开始回溯,回溯到上一个有选项的地方,选择另一条路继续进行尝试,你发现,这个东西,实在是有点熟悉啊:我们在树的前序/中序/后序遍历好像好像都是这个思路做的,我们平时在玩有多分支但是可以回溯的游戏的时候,也会在打完了一个结局之后回溯到上一个分支点,选择其他的选项,所以其实,这就是深度优先搜索,我们每次找到一个终点,然后回溯到上一个分叉点选择其它的选项

  不过我们现在考虑是图,有的顶点有可能会被重复访问,所以这时候我们需要一个对应的数组记忆哪一些顶点是已经被访问过的了,如果已经访问过,就不再重新访问

iii.代码实现

  所以我们可以给出一个非常简单的基于邻接表的DFS代码,DFS更多还是一种思想:

#include <iostream>
#include <vector>
using namespace std;
constexpr int MAXN = 5e3 + 20;vector<int> g[MAXN];
bool visited[MAXN]{ 0 };void dfs(int u)
{visit[u] = true; // u为起点cout << u << " ";for (const auto& v : g[u]) {if (!visited[v]) dfs(v);}
}

  对于邻接表,DFS的时间复杂度为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E),而对于邻接矩阵,其时间复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(V2),当然 ∣ E ∣ |E| E最大可以到 ∣ V ∣ 2 |V|^2 V2级别,所以对于稠密图,二者的时间复杂度几乎是一致的

#2.广度优先搜索(Breadth First Search)

i.你爱吃蛋糕吗?

  广度优先搜索就是另一个思路了,我们可以用蛋糕来举个例子,某个国家的F小姐很喜欢吃蛋糕,今天她获得了10个品种的小蛋糕各一份,她应该怎么品尝10种蛋糕呢?当然是想吃什么吃什么,还是要按照一定的顺序来,她当然可以每次吃掉一块蛋糕,然后去吃下一块(类似DFS),也可以每次从头开始,10块蛋糕依次吃一小口,然后这么一直循环,直到把所有蛋糕全都吃完

ii.BFS的思想

  所以这就是广度优先搜索,从起点开始,我们首先找到它连接的顶点(记为I),然后从I中的所有顶点中找到与它们连接的顶点,这么一直循环下去直到最后遍历完所有的点,广度优先搜索实际上就是按照层次完成图的遍历访问

  哦呀,这不就是树的层次遍历吗!说起这个来你应该就很熟悉了,没错,树的层次遍历实际上也就是一种BFS

iii.代码实现

  BFS同样也只是一种思想,这里给出一个基本的基于邻接表实现的BFS:

#include <iostream>
#include <vector>
using namespace std;
constexpr int MAXN = 5e3 + 20;vector<int> g[MAXN];
bool visited[MAXN]{ 0 };void bfs(int u)
{queue<int> q;cout << u << " "; // 从u开始访问visited[u] = true;q.push(u);while (!q.empty()) {int t{ q.front() };q.pop();for (const auto& v : g[t]) {if (!visited[v]) {cout << v << " ";visited[v] = true;q.push(v);}}}
}

  这样就好了,我们从起点开始,每次把它邻接的未访问顶点加入队列中,然后一直循环这个过程,直到所有的顶点都已经被访问过一次,BFS就结束了

  同理,基于邻接表的广度优先搜索时间复杂度仍然是 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E),而邻接矩阵为 O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)

#3.求连通分量

  如果一个无向图是连通图,那么无论从哪个顶点开始遍历(BFS/DFS均可),都可以访问所有顶点;但如果不连通,那么从任何顶点 v v v出发通过遍历**只能访问到 v v v的极大连通子图(即连通分量)**的所有顶点

  因此要求出所有的连通分量,可以对所有顶点进行检验,如果已经被访问,则该顶点已经出现在某个连通分量中,如果没有被访问,则从这个顶点开始遍历,就可以得到另一个连通分量

小结

  图这一篇我还是分成了两个部分,上半部分主要是介绍一些基本的图的概念,而下半部分则主要会集中于几个图的算法,最小生成树、最短路径和拓扑排序三个关键的问题

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

IDAPython详细版(二)

六&#xff1a;操作数 可以使用idc.get_openrand_typed(ea,n)得到操作数的类型。ea是地址&#xff0c;n是索引 这里有8种不同类型的操作数类 0_void 如果一个指令木有任何操作数它将返回0 0_reg 如果一个操作数是一个普通的寄存器将返回此类型。这个值在内部表示为1. o_mem …

数据分析求职-岗位介绍

这是咱们干货开始的第一篇文章&#xff0c;后续我尽量会保持日更的节奏和大家做分享~ 在未来所有分享的内容展开之前&#xff0c;咱们有必要先彻底、深入地了解下数据分析这个岗位。如果你还在犹豫是否要走数据分析的路&#xff0c;或者你已经拿了数据分析的offer想了解下将来…

nuScenes数据集解压

下载Full dataset&#xff08;v1.0&#xff09;的Trainval和Test部分&#xff0c;放到data/nuscenes文件夹下 如果下载了Map expansion&#xff0c;则需要现在nuscenes下新建一个文件夹maps&#xff0c;把nuScenes-map-expansion-v1.3.zip解压到里面&#xff0c;可以使用unzip…

vue+springboot项目上传部署tomcat

下载及安装Tomcat 进入tomcat官网&#xff0c;Tomcat官网 选择需要下载的版本&#xff0c;点击下载下载路径一定要记住&#xff0c;并且路径中尽量不要有中文 下载后是压缩包 .zip&#xff0c;解压后 tomcat系统各个文件夹目录是什么意义&#xff1a; bin&#xff1a;放置的是…

【Java集合篇】接上篇博文--为什么在JDK8中HashMap要转成红黑树

为什么在JDK8中HashMap要转成红黑树 ✔️为什么不继续使用链表✔️为什么是红黑树✔️红黑树的性能优势 ✔️ 拓展知识仓✔️为什么是链表长度达到8的时候转✔️为什么不在冲突的时候立刻转✔️关于为什么长度为8的时候转(源码注释解读)✔️为什么长度为6的时候转回来?✔️双向…

Python+django+selenium搭建简易自动化测试

该平台会集成UI自动化及api自动化&#xff0c;里面也会涉及到一些简单的HTML等前端&#xff0c;当然都是很基础的东西。在以后的博客里&#xff0c;我会一点点的尽量写详细&#xff0c;帮助一些测试小白一起成长&#xff0c;当然我也是个小菜鸡。 第一章 django 搭建平台。 1…

大创项目推荐 深度学习图像修复算法 - opencv python 机器视觉

文章目录 0 前言2 什么是图像内容填充修复3 原理分析3.1 第一步&#xff1a;将图像理解为一个概率分布的样本3.2 补全图像 3.3 快速生成假图像3.4 生成对抗网络(Generative Adversarial Net, GAN) 的架构3.5 使用G(z)生成伪图像 4 在Tensorflow上构建DCGANs最后 0 前言 &#…

如何在CentOS7部署宝塔面板并通过内网穿透实现远程登录面板

文章目录 一、使用官网一键安装命令安装宝塔二、简单配置宝塔&#xff0c;内网穿透三、使用固定公网地址访问宝塔 宝塔面板作为建站运维工具&#xff0c;适合新手&#xff0c;简单好用。当我们在家里/公司搭建了宝塔&#xff0c;没有公网IP&#xff0c;但是想要在外也可以访问内…

【Java基础篇】 try中return A,catch中return Bfinally中return C,最终返回值是什么?

try中return A&#xff0c;catch中return Bfinally中return C&#xff0c;最终返回值是什么? ✔️典型解析✔️拓展知识仓✔️finally 和 return 的关系 ✔️ final、finally、finalize有什么区别✔️final✔️finally✔️ finalize ✔️典型解析 最终的返回值将会是C 因为 fi…

CTFHub | 存储型

0x00 前言 CTFHub 专注网络安全、信息安全、白帽子技术的在线学习&#xff0c;实训平台。提供优质的赛事及学习服务&#xff0c;拥有完善的题目环境及配套 writeup &#xff0c;降低 CTF 学习入门门槛&#xff0c;快速帮助选手成长&#xff0c;跟随主流比赛潮流。 0x01 题目描述…

Vue3-36-路由-路由的元数据信息 meta

什么是 meta 简单的理解&#xff0c;meta 就是路由对象 的一个属性对象&#xff0c; 可以 通过这个 属性给 路由对象添加 一些必要的属性值&#xff0c; 在使用路由对象时可以获取到这个属性型对象&#xff0c;从而进行一些其他的逻辑判断。 meta 这个非常的简单&#xff0c;就…

如何使用Docker本地部署一个开源网址导航页并分享好友公网使用

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…