图作为数据结构中的一种重要概念,扮演着连接世界的纽带。与树和二叉树相比,图更加灵活和多样化,它能够描述各种实际问题中的复杂关系,如社交网络中的人际联系、城市交通中的路线规划以及电子网络中的通信路径等。
无论你是初学者还是进阶者,本文将为你提供简单易懂、实用可行的知识点,帮助你更好地掌握图在数据结构和算法中的重要性,进而提升算法解题的能力。接下来让我们开启数据结构与算法的奇妙之旅吧。
目录
图的基本概念
图的存储表示
图的基本操作
图的遍历与连通性
最小生成树
最短路径问题
关键路径问题
图的基本概念
图是由一组顶点(节点)和连接这些顶点的边(关系)组成的非线性数据结构。图用于表示元素之间的关系,通过节点和边来描述图中元素之间的连接情况。
图的定义包括以下几个要素
顶点:图中的节点表示实体或对象,通常用圆圈或方框来表示。每个节点可以包含一些相关信息,如名称、属性等。
边:图中的边表示顶点之间的关系,用于连接不同的顶点。边可以是有向的(箭头表示方向)或无向的(不带箭头表示),也可以带有权重(表示边的权值)。
邻接点:对于一个顶点,与它直接相连的顶点称为邻接点。邻接点之间通过边相连。
路径:路径是指顶点之间经过的一系列边的序列。路径的长度可以通过经过的边的数量来计算。
圈:如果路径的起点和终点是同一个顶点,并且至少经过了一条边,就形成了一个圈。圈也被称为循环。
有向图和无向图:
顶点的度:
对于无向图而言,顶点v的度是指依附于该顶点的边的条数,记为TD(v)。
对于有向图而言,我们要探讨的是顶点的入度和出度:
顶点v的度等于入度和出度之和,即 TD(v) = ID(v) + OD(v)。
入读是以顶点v为终点的有向边的数目,记为ID(V);出度是以顶点v为起点的有向边的数目,记为OD(v)。
顶点与顶点之间的关系描述:
连通图和强连通图:
子图:(研究图的局部)
对于有向图来说子图和生成子图的概念也是一样的:
连通分量:
无向图中的极大连通子图称为连通分量:
有向图中的极大强连通子图称为有向图的强连通分量:
生成树:
连通图的生成树是包含图中全部顶点的一个极小连通子图。
生成森林:
在非连通图中,连通分量的生成树构成了非连通图的生成森林。
几种特殊形态的图:
回顾重点,其主要内容整理成如下内容:
图的存储表示
图可以用多种方式进行存储表示,常见的有两种主要方法:邻接矩阵和邻接表。
邻接矩阵法:
邻接矩阵是一个二维数组,用于表示图中的节点之间的连接关系。对于一个有n个节点的图,邻接矩阵是一个大小为n×n的方阵。如果节点i和节点j之间存在一条边,则邻接矩阵中第i行第j列的元素为1;否则为0。邻接矩阵的优点是可以快速判断任意两个节点之间是否有边,但缺点是当图比较稀疏时会占用大量的空间。
如果我们采用邻接矩阵法存储带权图(网):
邻接表法:
根据有向无向连判断当前结点所连接的下一个结点的索引:
回顾重点,其主要内容整理成如下内容:
十字链表法存储有向图:
邻接多重表存储无向图:
如果删除结点和边的话得到的相应结果如下:
回顾重点,其主要内容整理成如下内容:
图的基本操作
如果我们想知道两个结点之间是否存在边可以通过邻接矩阵或者邻接表的方式:
如果我们想在图中插入顶点x,可以采用如下的方式进行:
如果想在图中删除某个顶点x,可以采用如下的方式进行:
如果想知道图中顶点x的第一个邻接点,若有则返回顶点号,若x没有邻接点或图中不存在x,则返回-1:
图的遍历与连通性
图的遍历是指访问图中所有节点的过程。通过遍历,我们可以对图的结构和节点之间的关系进行全面的了解。常用的图遍历算法包括广度优先搜索(BFS)和 深度优先搜索(DFS)。
广度优先遍历:图的广度优先遍历(Breadth-First Search,BFS)是一种从起始节点开始,先访问离起点最近的节点,然后逐层向外扩展访问的策略。它保证了先访问距离起始节点相近的节点,然后再访问距离稍远的节点。
广度优先遍历序列的案例如下:
下面是用 C 语言实现图的广度优先遍历的基本代码示例:
#include <stdio.h>
#define MAX_NODES 100
typedef struct {
int neighbor[MAX_NODES];
int numNeighbors;
int visited;
} Node;
void bfs(Node graph[], int start, int numNodes) {
int queue[MAX_NODES];
int front = 0, rear = 0;
// 将起始节点加入队列并标记为已访问
queue[rear++] = start;
graph[start].visited = 1;
while (front != rear) {
int current_node = queue[front++]; // 队首节点出队
printf("%d ", current_node); // 访问当前节点
// 遍历当前节点的邻居节点
for (int i = 0; i < graph[current_node].numNeighbors; i++) {
int neighbor = graph[current_node].neighbor[i];
if (!graph[neighbor].visited) { // 如果邻居节点未被访问过
queue[rear++] = neighbor; // 将邻居节点入队
graph[neighbor].visited = 1; // 标记邻居节点为已访问
}
}
}
}
int main() {
// 创建一个示例图
Node graph[MAX_NODES];
// 初始化图中的节点和边
for (int i = 0; i < MAX_NODES; i++) {
graph[i].numNeighbors = 0;
graph[i].visited = 0;
}
// 添加边关系
graph[0].neighbor[graph[0].numNeighbors++] = 1;
graph[0].neighbor[graph[0].numNeighbors++] = 2;
graph[1].neighbor[graph[1].numNeighbors++] = 3;
graph[2].neighbor[graph[2].numNeighbors++] = 4;
graph[2].neighbor[graph[2].numNeighbors++] = 5;
graph[3].neighbor[graph[3].numNeighbors++] = 6;
graph[4].neighbor[graph[4].numNeighbors++] = 7;
// 进行广度优先遍历
bfs(graph, 0, 8); // 假设图中有 8 个节点
return 0;
}
广度优先生成树用于在一个无向图或有向图中从给定的起始节点开始,以广度优先的方式生成一个树状结构,该树包含了从起始节点出发到达所有可达节点的最短路径。
回顾重点,其主要内容整理成如下内容:
深度优先遍历:深度优先搜索(Depth-First Search,DFS)是一种先走尽可能远的策略,即沿着当前路径走到底,直到不能再走下去为止,然后回溯到前一个节点,继续探测其他路径,直到所有节点都被访问过。
深度优先生成树是指通过深度优先搜索算法生成的一棵树状结构,该树包含了从给定起始节点出发到达所有可达节点的最短路径。
下面是用 C 语言实现深度优先遍历的基本代码示例:
#include <stdio.h>
#define MAX_NODES 100
typedef struct {
int neighbor[MAX_NODES];
int numNeighbors;
int visited;
} Node;
void dfs(Node graph[], int current_node) {
printf("%d ", current_node); // 先访问当前节点
graph[current_node].visited = 1; // 标记当前节点为已访问
// 遍历当前节点的邻居节点,递归调用 dfs 函数
for (int i = 0; i < graph[current_node].numNeighbors; i++) {
int neighbor = graph[current_node].neighbor[i];
if (!graph[neighbor].visited) { // 如果邻居节点未被访问过
dfs(graph, neighbor); // 递归访问邻居节点
}
}
}
int main() {
// 创建一个示例图
Node graph[MAX_NODES];
// 初始化图中的节点和边
for (int i = 0; i < MAX_NODES; i++) {
graph[i].numNeighbors = 0;
graph[i].visited = 0;
}
// 添加边关系
graph[0].neighbor[graph[0].numNeighbors++] = 1;
graph[0].neighbor[graph[0].numNeighbors++] = 2;
graph[1].neighbor[graph[1].numNeighbors++] = 3;
graph[2].neighbor[graph[2].numNeighbors++] = 4;
graph[2].neighbor[graph[2].numNeighbors++] = 5;
graph[3].neighbor[graph[3].numNeighbors++] = 6;
graph[4].neighbor[graph[4].numNeighbors++] = 7;
// 进行深度优先遍历
dfs(graph, 0); // 假设从节点 0 开始遍历
return 0;
}
连通分量
连通分量是指一个无向图中的最大连通子图。连通分量由若干个顶点及它们之间的边组成,其中每个顶点都可以通过路径与其他顶点相互到达。
在无向图中,连通分量可以非常直观地理解为图中的一片区域,该区域中的所有顶点都可以彼此到达。每个连通分量都是图的一部分,并且一个图可以有多个连通分量。
在有向图中,连通分量的定义相对复杂一些。有向图的连通性需要考虑顶点之间的有向路径。有向图中的连通分量是指在该图中,每个顶点都存在至少一条有向路径可以到达其他所有顶点。
回顾重点,其主要内容整理成如下内容:
最小生成树
最小生成树是指在无向带权连通图中,寻找一棵包含所有顶点的生成树,并且该树的所有边的权值之和最小。最小生成树有以下特点:
最小生成树可能是多个;最小生成树中的边数等于顶点数减1;最小生成树中不会有回路。
常见的解决最小生成树问题的算法包括 Prim算法和 Kruskal算法。
Prim算法(普里姆):从某一个顶点开始构建生成树;每次将代价最小的新顶点纳入生成树,直到所有的顶点都纳入为止。
这里我们选择p城为根结点,然后依次向外扩张找连线之间代价最小的结点,最终得到最小代价:
Kruskal算法(克鲁斯卡尔):每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选)直到所有的结点都连通。
两种算法的比较:
最短路径问题
图的最短路径问题是指在一个加权有向图或无向图中,寻找两个顶点之间最短路径的问题。最短路径可以通过边的权值和来衡量。最短路径问题主要分为以下两种情况:
单源最短路径:从给定的一个起始节点到图中其他所有节点之间的最短路径。它可以用于解决从一个固定起点到其他节点的最短路径问题。
BFS算法(无权图):
Dijkstra算法(带权图、无权图):
各顶点间的最短路径:计算图中任意两个节点之间的最短路径。它可以用于解决任意节点对之间的最短路径问题。
Floyd算法(带权图、无权图):
回顾重点,其主要内容整理成如下内容:
关键路径问题
关键路径是指项目计划中不能延误的最长路径,它决定了整个项目的最短完成时间。关键路径问题常用于项目管理和工程规划中。
在AOE网中我们要了解以下概念:
根据上文相关例子的举出之后,接下来我们开始计算相应的数值:
求所有事件的最早发生时间:(等最慢的完成才可以)
求所有事件的最迟发生事件:(汇点的最迟发生时间与最早发生时间一致,我们以汇点为触发点你拓扑排序,即减去相应路径上发生的事件从而得到该点的最迟发生时间):
求所有活动的最早发生时间:(通过前面计算处的事件最早发生时间推算出活动最早发生时间):
求所有活动的最晚发生时间:(通过前面计算处的事件最迟发生时间减去相应活动需要的时间得出)
求所有活动的时间余量:(活动最晚发生时间减去活动最早发生时间)
注意:以下是相应的关键活动和关键路径的特性:
回顾重点,其主要内容整理成如下内容: