数据结构—图(上)

news2025/1/13 13:41:23

文章目录

  • 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->v
    g[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.coloradmin.cn/o/1358719.html

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

相关文章

Linux:apache优化(3)—— 页面缓存时间

作用&#xff1a;通过 mod_expires 模块配置 Apache&#xff0c;使网页能在客户端浏览器缓存一段时间&#xff0c;以避免重复请求&#xff0c;减轻服务端工作压力。启用 mod_expires 模块后&#xff0c;会自动生成页面头部信息中的 Expires 标签和 CacheControl 标签&#xff0…

COCO Dataset Format

COCO (Common Objects in Context) dataset数据集是一个广泛应用于目标检测、语义分割的数据集&#xff0c;包含330K 图片数据 与 2.5 million 个目标实体。 1.数据集下载 !wget http://images.cocodataset.org/zips/train2017.zip -O coco_train2017.zip !wget http://image…

宝塔面板yum安装指南

1、执行 yum install -y wget && wget -O install.sh https://download.bt.cn/install/install_6.0.sh && sh install.sh ed8484bec2、QA 提示抱歉&#xff0c;连接宝塔官网失败&#xff0c;请切换节点后重试服务器终端 分别执行这2条命令 mv /www/server/pa…

1.4 day4 IO进程线程

使用两个子进程进行文件拷贝&#xff0c;父进程进行资源回收 #include <myhead.h> int main(int argc, const char *argv[]) {//创建一个文件描述符并以只读的方式打开int fd-1;if((fdopen("./test.bmp",O_RDONLY))-1){perror("open error");return…

私有云平台搭建openstack和ceph结合搭建手册

OpenStack与云计算 什么是云&#xff1f; 如何正确理解云&#xff0c;可以从以下几个方面。 云的构成。 用户&#xff1a;对用户而言是透明无感知的&#xff0c;不用关心底层构成&#xff0c;只需要知道利用云完成自己任务即可。 云提供商&#xff1a;对云资产管理和运维。 云…

高并发下的计数器实现方式:AtomicLong、LongAdder、LongAccumulator

一、前言 计数器是并发编程中非常常见的一个需求&#xff0c;例如统计网站的访问量、计算某个操作的执行次数等等。在高并发场景下&#xff0c;如何实现一个线程安全的计数器是一个比较有挑战性的问题。本文将介绍几种常用的计数器实现方式&#xff0c;包括AtomicLong、LongAd…

工业城市的废水监控系统

前言 很多工业城市的废水排放量较大&#xff0c;已造成城市地表水的严重污染。各城市的环境监测中心站肩负着对城市地表环境水质及污染源排放废水的监测工作&#xff0c;很多城市相继形成了以市站为网头&#xff0c;与区站、行业站构成一体的废水监测网。 为提高水质监测能力建…

【Java】RuoYi-Vue-Plus 多数据源整合TDengine时序数据库——服务端自动建库建表

目录 环境准备整合TDengine 数据源1. 添加驱动依赖2. 添加数据源配置3. 添加Mapper4. 添加建表sql脚本5. Controller 测试效果 环境准备 RuoYi-Vue-Plus v5.1.2JDK17Maven 3.6.3Redis 5.XMySQL 5.7TDengine 2.6.0.34 客户端 整合TDengine 数据源 1. 添加驱动依赖 注意&…

redis安装与配置

目录 1. 切换到 root 用户 2. 搜索安装包 3. 安装 redis 4. 查看 redis 是否正常存在 5. 修改ip 6. 重新启动服务器 7. 连接服务器 1. 切换到 root 用户 通过 su 命令切换到 root 用户。 2. 搜索安装包 apt search redis 这里安装的是下面的版本&#xff1a; 3. 安装 …

Python接口自动化 —— 什么是接口测试、为什么要做接口测试(详解)

什么是接口测试 接口测试是测试系统组件间接口的一种测试。接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点。测试的重点是要检查数据的交换&#xff0c;传递和控制管理过程&#xff0c;以及系统间的相互逻辑依赖关系等。  一般来说&#xff0c;测试接…

pod进阶版(2)

startupProbe启动探针 如果探测失败,pod的状态是notready&#xff0c;启动探针会重启容器 启动探针没有成功之前&#xff0c;后续的探针都不会执行。启动探针成功之后&#xff0c;在pod的后续生命周期不会用启动探针 exec方式 正确示范 apiVersion: v1 kind: Pod metadata…

linux 使用log4cpp记录项目日志

为什么要用log4cpp记录项目日志 在通常情况下&#xff0c;Linux/UNIX 每个程序在开始运行的时刻&#xff0c;都会打开 3 个已经打开的 stream. 分别用来输入&#xff0c;输出&#xff0c;打印错误信息。通常他们会被连接到用户终端。这 3 个句柄的类型为指向 FILE 的指针。可以…

MobaXterm SSH 免密登录配置

文章目录 1.简介2.SSH 免密登录配置第一步&#xff1a;点击 Session第二步&#xff1a;选择 SSH第三步&#xff1a;输入服务器地址与用户名第四步&#xff1a;设置会话名称第五步&#xff1a;点击 OK 并输入密码 3.密码管理4.小结参考文献 1.简介 MobaXterm 是一个功能强大的终…

shell,对输出的结果去掉空格和换行符号,grep忽略特定字符

对原始的执行命令&#xff0c;直接后面加 |tr -d \n | tr -d |tr -d \n 用来去除换行符 |tr -d 用来出去空格 grep去除特定的字符的行&#xff0c;直接 -v&#xff0c;后接字符。比如&#xff1a; 现在设法去掉含有"ini"的行&#xff0c;执行&#xff1a; …

对象克隆学习

假如说你想复制一个简单变量。很简单&#xff1a; int apples 5; int pears apples; 不仅仅是int类型&#xff0c;其它七种原始数据类型(boolean,char,byte,short,float,double.long)同样适用于该类情况。 但是如果你复制的是一个对象&#xff0c;情况就有些复杂了。 …

美洽获评2023年度“最佳数字化服务商”,产品优势赋能企业智能转型

日前&#xff0c;由知名学习交流平台人人都是产品经理举办的“2023年度评选”活动圆满落幕&#xff0c;美洽凭借在企业服务领域的技术实力与优秀实践成果脱颖而出&#xff0c;入围年度产品评选榜单&#xff0c;获评2023年度“最佳数字化服务商”。 人人都是产品经理成立于2010年…

邮件营销发件人名称设置指南:提升邮件信誉度与点击率的技巧

正如品牌名称之于产品&#xff0c;发件人名称之于电子邮件。它有助于提高识别度、熟悉度和清晰度。一封邮件的发件人名称可以是话题的开端&#xff0c;也可以是邮件内容的破坏者。 关于电子邮件的发件人名称&#xff0c;你可能经常不得不思考两件事。 我应该用什么名字&#…

怎么分解一张二维码图片?二维码解码在线处理技巧

相信对于制作二维码很多小伙伴还知道怎么操作&#xff0c;那么分解二维码图片的方法有知道的吗&#xff1f;二维码解码也是在日常生活中经常会需要使用的一个功能&#xff0c;当我们需要将一张图片上的二维码分解使用时&#xff0c;那么最方便快捷的方法就是使用二维码解码器工…

【S32K 进阶之旅】 NXP S32K3 以太网 RMII 接口调试(1)

前言 大联大世平集团推出了一款基于 NXP 车规级 MCU S32K344 的开发板——花名“Cavalry”&#xff0c;它使用 BGA257 封装的 32 位 ArmCortex-M7 S32K344 作为主控芯片&#xff0c;在69.6*130mm 的小体积开发板上搭载了 SBC 电源管理芯片、CAN 收发器、LIN 收发器、FLASH 存储…

Rhinoceros 8.2(犀牛8.2)安装教程

Rhinoceros 8.2下载链接&#xff1a;https://docs.qq.com/doc/DUmhMYmNyUGl6ZVpU 1.选中下载的压缩包&#xff0c;右键解压到“Rhinoceros 8.2”文件夹 2.选中“rhino_en-us_8.2.23346.13001.exe”右键以管理员身份运行 3.点击“小齿轮“ 4.选择安装路径&#xff0c;取消勾选“…