图论算法篇:邻接矩阵以及邻接表和链式前向星建图

那么我们从这一篇文章开始就正式进入了图相关算法的学习,那么对于认识图的各种算法之前,那么我们首先得学会建图,但是要在建图之前,我们又得对图这种非常基本非常常见的数据结构有着一定的认识,所以我们就先来简单回顾一下我们图这个数据结构
那么对于我们的图这个数据结构来说,它是一个多对多的非线性的数据结构,那么图可以看成由两部分组成,分别是节点以及边组成,我们首先可以根据我们的边是否有向,可以将我们一个图分成有向图与无向图。其中无向图我们可以理解为双向图,也就是假设节点a与节点b有一条边,那么我们可以认为节点a到节点b有一条有向边,同时节点b到节点a也有一条有向边

我们还可以将图按照边是否带权值分为带权图与不带权图,那么这个权值的具体含义可以是我们地图上各个城市作为节点,那么他们之间的连线的权值就是距离或者说从a城市到达b城市的油量消耗,那么对于我们的图还可以按照节点之间边的连线的数量分为稀疏图和稠密图等等其他各种分类,那么这里我们只需要掌握最常见也就是最关键的两种分类的图即可,也就是有向无向图和带权不带权图

那么回顾了我们的图这个数据结构,那么我们接下来就得知道怎么表示图这个数据结构,我们知道对于线性结构我们可以用数组或者链表来实现线性表,而对于树型结构,那么我们可以用类或者结构体来描述我们树中的一个节点,那么在这个节点中保存了其子节点的指针以及这个节点本身的值,然后通过一个根节点往下遍历,那么这里我们的图有如何来表示呢,那么我们建图就有三种方式,那么就如我们的标题所说,分别是邻接矩阵以及邻接表和链式前向星,那么我会在下文分别介绍这三种建图方式

1.邻接矩阵

那么邻接矩阵的建图方式那就是我们定义一个二维的数组,那么假设我们的节点个数是n个,那么我们就定义n*n的二维数组,那么其中这个二维数组的行下标与列下标都对应着相应的节点标号,那么这里我们有了二维数组,那么我们怎么表示图中各个编号的节点的连接的信息呢?

有向图建立:

那么我们首先初始化我们的二维数组的各个元素为0或者-1,那么如果我们节点a与节点b有一条a到b的有向边,假设a节点对应的编号是0,b节点对应的编号是1,那么我们就在二维数组的第0行第1列(这里第0行可能会引起误会,因为我这里习惯用下标称呼二维数组的行与列,这里的第0行就是数组的第一行)位置处设置为1,那么只要我们a到b有一条边,那么我们就在a所对应的节点编号就作为我们的行下标,b节点编号就作为列下标,那么对应位置处设置为1即可,那么在这个二维数组中元素值为1就代表该位置处对应的行下标所对应的节点到列下标所对应的节点有一条有向边

[1, 0, 0, 0]

[0, 0, 0, 0]

[0, 0, 0, 0]

[0, 0, 0, 0]

代码实现:

c++ 复制代码
// 添加边操作
void addEdge(int u, int v, vector<vector<int>>& graph) {
    graph[u][v] = 1; 
}

无向图建立:

只要我们会了有向图建立,那么无向图建立就十分简单,那么我们上文说过我们的无向图可以视作一个有向图,那么a和b节点有连接,那么我们就认为a和b节点有两条边,分别是a到b方向有一条边和b到a方向上有一条边,所以我们可以按照我们有向图的思路去建图,那么假设这里a和b连接,并且a节点编号是0,b是1的话,那么就在数组(0,1)和(1,0)位置处设置为1

[1, 0, 0, 0]

[1, 0, 0, 0]

[0, 0, 0, 0]

[0, 0, 0, 0]

带权图建立:

那么带权图的话,我们就在我们有向图与无向图的基础上,我们这里数组的元素值0和1表示的是存在或者不存在,那么如果我们表示权值的话,那么我们在设置数组的元素值的时候,我们就不设置为1了,而是直接设置为权值,比如假设节点a的编号为0,节点b的编号为1,有一条a到b且权值为5的有向边,那么这里我们就在数组的(0,1)设置为5即可,那么这里的数组的元素值-1就表示不存在,因为边的权值一般不为负数,如果a和b有一个权值为k的边,那么我们就把相应位置的值设置为k即可

[k, -1, -1, -1]

[-1, -1, -1, -1]

[-1, -1, -1, -1]

[-1, -1, -1, -1]

代码实现:

c++ 复制代码
  void addWeightedEdge(vector<vector<int>>& graph ,int u, int v, int k) {
      graph[u][v] = k; 
}

那么我们知道了如何用邻接矩阵的方式来建图之后,那么我们再来说一下我们邻接矩阵建图的一个优点与缺点,那么我们知道由于我们的邻接矩阵的实现是采取数组的方式来实现的,而数组有着随机访问的特性,所以对于查询(i,m)位置的元素值能做到时间复杂度是O(1),那么意味着我们查询我们这个数组两个节点是否有边是十分高效的,但是邻接矩阵的缺点也很明显,那么如果我们该图的节点个数变多,那么我们这里开辟的数组的大小就会很大,空间复杂度就会很高,并且如果我们该图是稀疏图的话,那么我们很多节点之间没有连线,那么我们邻接矩阵其实有很大一部分空间是被浪费的,因为我们建图更希望得到有边的信息,而这里邻接矩阵的实现则还表示了一些节点没有边的信息,所以对于我本人来说,我是不喜欢也不推荐用邻接矩阵这个方式来建图,我们邻接矩阵的应用场景一般是稠密图并且节点个数还要少.

2.邻接表

那么邻接表就在邻接矩阵的基础上优化了一些邻接矩阵的缺点,我们知道我们邻接矩阵有n个节点要开辟n*n的空间,那么对于一些没有边的信息,我们这里邻接矩阵也表示了,所以我们这里的邻接表就只表示有边的节点的信息,大大优化了存储的空间

那么接下来我来说一下邻接表的原理,那么我们邻接表是用一个指针数组,那么每一个元素是一个指针,其中就指向一个链表,我们一个图有n个节点,那么我们就开辟长度为n的指针数组,当然这个数组的每个位置的下标就对应图中的节点编号,那么这里我们每一个数组元素指向的一个链表就表示与该节点直接相连的节点

但是实际上我们实现我们的邻接表的时候,这里其实不用一个指针数组那么麻烦,这里可以用一个动态的二维数组来实现,那么我们还是开辟一个长度为n的数组,数组的下标对应节点编号。

假设有4个节点(a, b, c, d),编号分别为0, 1, 2, 3,并且有以下边:

a -> b

a -> c

a -> d

b -> c

邻接表表示:

adj[0](节点a):[1, 2, 3](节点b, c, d)

adj[1](节点b):[2](节点c)

adj[2](节点c):[](无边)

adj[3](节点d):[](无边)

那么这里我们就将我们这里与节点a相连的b,c,d的节点编号压入进我们数组下标为0的一个一维数组当中,那么这里同理如果与节点编号为1的直接相连的节点编号有2

那么我们就把与节点编号相连的数组直接压入该下表为1的一维数组中去

那么这里我们的动态的二维数组其实就是模拟链表的一个逻辑结构,这里我们的编号就相当于是指针,因为我们的编号对应数组的下标,那么我们数组由于数随机访问的特性,那么我们可以利用编号来访问到各个节

那么对于有向图来说比如a到b有一条有向边,那么我们就在a节点对应的数组中将b的节点编号压入a对应的一维数组中,而如果是无向图,a和b之间有一条边,那么我们就把b的编号压入a对应的一维数组中,同时也把a的编号压入b的数组中

代码实现:

c++ 复制代码
vector<vector<int>> adj (N);
//无向图
void addEdge(int u, int v) {
    adj[u].push_back(v);
    adj[v].push_back(u);
}
//有向图
void addEdge(int u, int v) {
    adj[u].push_back(v);
}

而对于带权图的话,这里我们的二维动态数组的元素就不再是一个int类型表示节点编号,那么就应该是一个结构体类型或者pair类型来存储了连接的边与对应的权值

代码实现:

c++ 复制代码
vector<vector<pair<int,int>>> adj (N);
void addEdge(int u, int v,int weight) {
    adj[u].push_back({v,weight});
    adj[v].push_back({u,weight});
}

那么对于我们的邻接表来说,这里优=我们只存储了边的信息,那么这就大大优化了空间,并且遍历也十分方便,那么他是一个优秀的一个建图方式,那么我们对于有n个节点,m条边的图来说,我们这里的邻接表的空间复杂度就是o(n+m)

3.链式前向星

那么链式前向星是我比较推荐的一个建图方式,那么在比赛以及面试的时候都十分有用的一个建图方式

那么链式前向星的一个原理就是准备三个一维动态数组,分别是head以及next以及to数组,那么我们该图如果有n个节点,那么我们就准备n长度的head数组,那么数组的下标就对应着节点的编号,然后我们会为我们的每一条边分配一个编号,那么该编号同理对应着我们next数组的下标,而to数组则是我们这条边所连接的一个节点

  • 核心组件:
    • head[]: 每个节点对应的链表的头节点索引。初始化为-1,表示该节点没有边。
    • edge[]: 存储边的"下一个"边索引,形成链表结构。
    • to[]: 存储边连接的目标节点索引。
    • cnt: 边计数器
    • weight[](可选):存储边的权重,对于带权图是必要的

那么我来举一个例子来说明,假设有4个节点(a, b, c, d),编号分别为0, 1, 2, 3,并且有以下边:

a -> b

b -> c

a -> d

b -> d

初始化:

head = [-1, -1, -1, -1]

next = [](动态扩展)

to = [](动态扩展)

weight = [](如果需要权重)

cnt = 0

添加边:

a -> b:

cnt = 0

next[0] = head[0] = -1

head[0] = 0

to[0] = 1(b的编号)

cnt = 1

b -> c:

cnt = 1

next[1] = head[1] = -1

head[1] = 1

to[1] = 2(c的编号)

cnt = 2

a -> d:

cnt = 2

next[2] = head[0] = 0(a之前的头边)

head[0] = 2

to[2] = 3(d的编号)

cnt = 3

b -> d:

cnt = 3

next[3] = head[1] = 1(b之前的头边)

head[1] = 3

to[3] = 3(d的编号)

cnt = 4

最终结构:

head = [2, 3, -1, -1]

next = [-1, 0, -1, 1]

to = [1, 2, 3, 3]

所以我们链式前向星对于每一个节点与它直接相连的边,按照该流程的处理逻辑,也就是我们让该该节点相连的边的编号在next数组中对应的下标位置处保存head对应该节点位置处的值,然后让to数组对应该边的编号保存对应连接的节点,那么这就好比模拟的是一个链表的头插法的一个逻辑来实现的

那么对于带权的图我们采取链式前向星我们就可以在额外开一个一维数组weight,那么weight数组的每一个位置下标对应节点编号,其中元素值就表示该边的权值

代码实现:

c++ 复制代码
void add(vector<int>& head,vector<int>& next,vector<int>& to,int u,int v,int cnt)
{
//u到v有一条有向边
   next[cnt]=head[u];
   head[u]=cnt;
   to[cnt]=v;
}
c++ 复制代码
//遍历我们该节点编号为u直接相连的节点
int ant=head[u];
while(ant!=-1)
{
cout<<"u连接的节点是"<<to[ant];
ant=next[cnt];
}

4.结语

那么这就是本篇文章介绍我们常见的三种建图的方式,那么我们一定要根据图的性质选择合适的建图方法,但是在这三种建图方式中,其实我最不推荐的就是我们第一种邻接矩阵的建图方式,那么在学习完我们的建图之后,那么我们便正式进入了我们图相关的算法学习,那么后面会更新图相关的算法比如拓扑排序以及bfs和迪杰斯特拉算法,那么我会持续更新,希望你多多关注与支持,那么如果本篇文章对你有帮助的话,请多多三连加关注哦。

相关推荐
抽风侠几秒前
Effective C++读书笔记——item49(了解new-handle的行为)
c++·算法
Stanford_11062 分钟前
C++中常用的十大排序方法之3——插入排序
c++·算法·微信小程序·排序算法·微信公众平台·twitter·微信开放平台
da pai ge22 分钟前
个人Dockerfile分享
开发语言·javascript·人工智能·prometheus
望云山19041 分钟前
第二章:16.5 决策树处理连续值特征
算法·决策树·机器学习
小张-森林人1 小时前
当C#邂逅Deepseek, 或.net界面集成deepseek
开发语言·人工智能·c#
暗星涌动1 小时前
Java 与设计模式(16):命令模式
java·设计模式·命令模式
Anarkh_Lee1 小时前
图解JVM-2. 类加载子系统
java·jvm·后端
wangxiaoxiao1 小时前
牛客题解 | 单组_保留小数位数
算法·牛客·牛客题库·校招笔试
MZWeiei1 小时前
排序算法:冒泡排序
数据结构·算法·排序算法
乏味~1 小时前
项目中一些不理解的问题
java