学习思维导图:
# 图
## 图的基本概念
## 图的存储结构及基本操作
- 邻接矩阵
- 邻接表
- 邻接多重表、十字链表
## 图的遍历
- 深度优先搜索
- 广度优先搜索
## 图的基本应用
- 最小生成树
- 最短路径
- 拓扑排序
- 关键路径
学习思维导图:
# 图
## 图的基本概念
## 图的存储结构及基本操作
- 邻接矩阵
- 邻接表
- 邻接多重表、十字链表
## 图的遍历
- 深度优先搜索
- 广度优先搜索
## 图的基本应用
- 最小生成树
- 最短路径
- 拓扑排序
- 关键路径
图是由顶点和边组成的非线性数据结构。顶点有时也被称为节点,而边是连接图中任意两个节点的线或弧。更正式地说,图是由一组顶点(V)和一组边(E)组成的。图用G(E, V)表示。
树和图的区别?
树是受限制的图类型,只是有更多的规则。每棵树都是一个图,但不是所有的图都是树。链表、树和堆都是图的特殊情况。
图的邻接矩阵(Adjacency Matrix)是一种常用的图表示方法,特别适用于稠密图,它以矩阵的形式表示图的连接关系。在邻接矩阵中,行和列分别代表图的顶点,矩阵的元素表示顶点之间是否相邻或者边的权重。
图的邻接表(Adjacency List)是一种常见的图表示方法,特别适用于稀疏图,它使用链表或数组的形式来表示图的连接关系。每个顶点都对应一个链表,链表中存储与该顶点相邻的其他顶点。
邻接表的主要思想是为每个顶点创建一个链表,链表中的每个节点表示与该顶点相邻的另一个顶点。对于无向图,通常需要为每一条边创建两个链表节点,分别表示两个相邻的顶点。
#define MAX_VERTICES 100
int adjMatrix[MAX_VERTICES][MAX_VERTICES]; // 邻接矩阵
void initializeMatrix(int vertices) {
for (int i = 0; i < vertices; i++) {
for (int j = 0; j < vertices; j++) {
adjMatrix[i][j] = 0; // 初始化所有元素为0
}
}
}
void addEdge(int start, int end) {
adjMatrix[start][end] = 1; // 添加边,将对应位置的元素设为1
adjMatrix[end][start] = 1; // 无向图需要将对称位置的元素也设为1
}
// 邻接表节点结构
struct Node {
int data;
struct Node* next;
};
// 图的结构
struct Graph {
int vertices;
struct Node** adjacencyList;
};
// 创建新节点
struct Node* createNode(int data) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = data;
newNode->next = NULL;
return newNode;
}
// 创建图
struct Graph* createGraph(int vertices) {
struct Graph* graph = (struct Graph*)malloc(sizeof(struct Graph));
graph->vertices = vertices;
graph->adjacencyList = (struct Node**)malloc(vertices * sizeof(struct Node*));
for (int i = 0; i < vertices; i++) {
graph->adjacencyList[i] = NULL;
}
return graph;
}
// 添加边
void addEdge(struct Graph* graph, int start, int end) {
// 添加边到起始顶点的链表
struct Node* newNode = createNode(end);
newNode->next = graph->adjacencyList[start];
graph->adjacencyList[start] = newNode;
// 对于无向图,需要添加一条反向边
newNode = createNode(start);
newNode->next = graph->adjacencyList[end];
graph->adjacencyList[end] = newNode;
}
深度优先搜索(Depth-First Search,简称 DFS)是一种用于图的遍历和搜索的算法,它从图中的一个起始顶点开始,尽可能深地访问顶点,直到无法继续前进,然后回溯到之前的顶点,继续深入其他分支。DFS 通常用递归或栈来实现,确保在深度方向上优先遍历。
以下是 DFS 算法的一般步骤:
int visited[MAX_VERTICES];
void dfs(int start, int vertices) {
visited[start] = 1;
printf("%d ", start);
for (int i = 0; i < vertices; i++) {
if (adjMatrix[start][i] == 1 && !visited[i]) {
dfs(i, vertices);
}
}
}
void dfs(struct Graph* graph, int vertex, int* visited) {
visited[vertex] = 1;
printf("%d ", vertex);
struct Node* temp = graph->adjacencyList[vertex];
while (temp) {
int adjVertex = temp->data;
if (!visited[adjVertex]) {
dfs(graph, adjVertex, visited);
}
temp = temp->next;
}
}
void dfsTraversal(struct Graph* graph, int startVertex) {
int* visited = (int*)malloc(graph->vertices * sizeof(int));
for (int i = 0; i < graph->vertices; i++) {
visited[i] = 0;
}
dfs(graph, startVertex, visited);
free(visited);
}
广度优先搜索(Breadth-First Search,简称 BFS)是一种用于图的遍历和搜索的算法,它从图中的一个起始顶点开始,逐层地访问与该顶点相邻的顶点,然后再访问这些相邻顶点的邻居,以此类推。BFS 通常用队列来实现,确保按照广度优先的顺序访问顶点。
以下是 BFS 算法的一般步骤:
// 从 start 开始遍历图
void bfs(int start, int vertices) {
enqueue(start);
visited[start] = 1;
while (front != -1) {
int currentVertex = dequeue();
printf("%d ", currentVertex);
for (int i = 0; i < vertices; i++) {
if (adjMatrix[currentVertex][i] == 1 && !visited[i]) {
enqueue(i);
visited[i] = 1;
}
}
printf("\n");
}
}
void bfs(struct Graph* graph, int startVertex) {
int* visited = (int*)malloc(graph->vertices * sizeof(int));
for (int i = 0; i < graph->vertices; i++) {
visited[i] = 0;
}
int queue[graph->vertices];
int front = -1, rear = -1;
queue[++rear] = startVertex;
visited[startVertex] = 1;
printf("BFS traversal starting from vertex %d: ", startVertex);
while (front != rear) {
int currentVertex = queue[++front];
printf("%d ", currentVertex);
struct Node* temp = graph->adjacencyList[currentVertex];
while (temp) {
int adjVertex = temp->data;
if (!visited[adjVertex]) {
queue[++rear] = adjVertex;
visited[adjVertex] = 1;
}
temp = temp->next;
}
}
printf("\n");
free(visited);
}
最小生成树(Minimum Spanning Tree,MST)是在一个连接所有顶点的无向图中,通过选择一部分边而形成的树,使得树的总权重(或成本)最小。
最小生成树满足以下特点:
Prim 算法的主要思想是从一个初始顶点开始,逐步扩展生成树,每次选择与当前生成树相邻且权重最小的边所连接的顶点,并将该顶点加入生成树。算法的运行过程可以概括为以下步骤:
Kruskal 算法是一种用于求解最小生成树(Minimum Spanning Tree,MST)的贪心算法。Kruskal 算法的基本思想是从边的权重最小的边开始,逐步构建生成树,确保不会形成环路。
以下是 Kruskal 算法的一般流程:
Dijkstra 算法的基本思想是从起始顶点开始,逐步扩展已知的最短路径,直到到达所有其他顶点。算法维护一个集合,其中包含已知的最短路径,以及一个优先队列(最小堆)用于选择下一个要探索的顶点。算法的主要步骤如下:
function Dijkstra(Graph, source):
for each vertex v in Graph.Vertices:
dist[v] ← INFINITY
prev[v] ← UNDEFINED
add v to Q
dist[source] ← 0
while Q is not empty:
u ← vertex in Q with min dist[u]
remove u from Q
for each neighbor v of u still in Q:
alt ← dist[u] + Graph.Edges(u, v)
if alt < dist[v]:
dist[v] ← alt
prev[v] ← u
return dist[], prev[]
Floyd-Warshall 算法是一种用于求解图中所有顶点对之间最短路径的动态规划算法。它适用于有向图和带有权重的边,可以找到从一个顶点到另一个顶点的最短路径,并计算所有顶点对之间的最短路径。
以下是 Floyd-Warshall 算法的一般流程:
dist
,其中 dist[i][j]
表示从顶点 i 到顶点 j 的最短路径距离。dist[i][j]
设置为正无穷(表示无法直接从顶点 i 到顶点 j),但当 i 等于 j 时,将 dist[i][j]
设置为 0(表示从顶点 i 到自身的距离为 0)。(u, v)
,如果存在从顶点 u 到顶点 v 的直接路径,则将 dist[u][v]
设置为边的权重。(i, j)
,遍历一个中间顶点 k
(从 1 到顶点的数量),检查是否存在一条从顶点 i 经过顶点 k 到顶点 j 的路径距离更短。具体步骤如下:dist[i][j]
大于 dist[i][k] + dist[k][j]
,则更新 dist[i][j]
为 dist[i][k] + dist[k][j]
。dist
包含了所有顶点对之间的最短路径距离。dist
来重构具体的最短路径。拓扑排序是一种用于有向图中的顶点排序的算法,它使得图中的每一条有向边都从前面的顶点指向后面的顶点。拓扑排序主要应用于有向无环图(DAG)或 AOV 网,这种图没有环路,因此可以对其进行拓扑排序。
AOV 网和 AOE 网
算法流程
拓扑排序的结果序列可能不唯一,因为有多个入度为 0 的顶点可以作为起始顶点。如果图中存在环路,那么无法进行拓扑排序,因为无法找到入度为 0 的顶点作为起始顶点。
拓扑排序的时间复杂度通常为 $O(V + E)$ ,其中 $V$ 是顶点的数量, $E$ 是边的数量。这个算法非常适合于解决任务调度和依赖关系问题,因为它可以确定任务的执行顺序或依赖关系的合理性。
概要总结:找到所有最早发生时间和最晚发生时间相同的结点,连接这些结点即可得到关键路径。
具体步骤:
关键路径的特点:
当表达式中存在共享的子表达式时,二叉树可能不是存储这些表达式的最有效方式,因为它会重复存储共享的子表达式。在这种情况下,可以使用有向无环图(Directed Acyclic Graph, DAG)来表示表达式,这样可以避免重复,并且更有效地表示表达式。
比如对于表达式 (x + y) * ((x + y) / x)
,用二叉树和DAG的表示如下图: