目录
- 图的基本概念
- 图的存储
- 图的遍历
- 最小生成树
- 最短路径
- 拓扑排序与关键路径
- 图的应用-真题
1. 图的基本概念
1.1 图的定义

图是由顶点的有穷非空集合和顶点之间边的集合组成的。
通记为G(V,E),其中G表示一个图,V是图G中顶点的集合,E是边的集合。
V(G)和E(G)通常分别表示图G的顶点集合和边集合。
注:对于图这种数据结构,不允许没有顶点,但边集合可以为空。
1.2 无向图与有向图

注:有向图中弧线有箭头的一端叫做弧头,另一端叫做弧尾。
1.3 简单图与多重图

注:学习数据结构中,只讨论简单图,不讨论多重图。
1.4 完全图
(1)完全图:具有最多边数的简单图。
(2)对于一个具有n个顶点的无向完全图,边数量的最大值为n(n-1)/2。
(3)对于一个具有n个顶点的有向完全图,边数量的最大值为n(n-1)。

1.5 路径和路径长度

1.6 简单路径

1.7 顶点的度

1.8 度与边的关系
(1)在无向图中,假设具有n个顶点,e条边。图中所有顶点度之和等于边数的两倍。
(2)对于有向图,所有顶点的出度之和与入度之和相等,弧头弧尾的数量也相等。
1.9 子图

1.10 连通图

1.11 连通分量
无向图中的极大连通子图称为连通分量。
注:极大的意思是这个连通子图已经大到不能再加任何新的点了,加了就不连通了。

1.12 强连通图和强连通分量

注:强连通图和强连通分量类比连通图和连通分量即可。
1.13 生成树
生成树:对于一张连通无向图,从中选出一些边,使得图中所有顶点都被连接起来,且没有形成任何回路,所构成的子图就叫做这张图的生成树。
注:生成树中,若顶点为n,则边有n-1条。

1.14 边的权和网

1.15 真题
(1)

解析:题目说的是随便怎么连都可以是连通的。6个顶点的完全图是15条边,说明如果只给15条边,存在某种情况使得7个顶点没有连通,这是7个顶点处于断开状态的上限,只需再加一条边,一定可以连通。
2. 图的存储
2.1 无向图-邻接矩阵存储
(1)

注:图中的边数组就是邻接矩阵。
(2)代码
c
typedef char VertexType;//顶点类型
typedef int EdgeType;//边的权值类型(0或1)
#define MAXSIZE 100//最大顶点数
//图的邻接矩阵结构体定义
typedef struct
{
VertexType vertex[MAXSIZE];//顶点数组
EdgeType arc[MAXSIZE][MAXSIZE];//邻接矩阵
int vertex_num;
int edge_num;
} Mat_Grph;
//创建图的邻接矩阵
void create_graph(Mat_Grph* G)
{
G->vertex_num = 4;
G->edge_num = 5;
//顶点命名
G->vertex[0] = '0';
G->vertex[1] = '1';
G->vertex[2] = '2';
G->vertex[3] = '3';
//初始化邻接矩阵
for (int i = 0; i < G->vertex_num; i++)
{
for (int j = 0; j < G->vertex_num; j++)
{
G->arc[i][j] = 0;
}
}
//添加边(无向图,因此要对称赋值)
G->arc[0][1] = 1; G->arc[1][0] = 1;
G->arc[0][2] = 1; G->arc[2][0] = 1;
G->arc[0][3] = 1; G->arc[3][0] = 1;
G->arc[1][2] = 1; G->arc[2][1] = 1;
G->arc[2][3] = 1; G->arc[3][2] = 1;
}
//输出邻接矩阵
void print_matrix(Mat_Grph G)
{
printf("邻接矩阵表示如下:\n");
for (int i = 0; i < G.vertex_num; i++)
{
for (int j = 0; j < G.vertex_num; j++)
{
printf("%d ", G.arc[i][j]);
}
printf("\n");
}
}
int main()
{
Mat_Grph G;
create_graph(&G);
print_matrix(G);
return 0;
}
2.2 有向图-邻接矩阵存储
(1)

(2)代码
只有两处与无向图不一样,已标注。
c
typedef char VertexType;//顶点类型
typedef int EdgeType;//边的权值类型(0或1)
#define MAXSIZE 100//最大顶点数
//图的邻接矩阵结构体定义
typedef struct
{
VertexType vertex[MAXSIZE];//顶点数组
EdgeType arc[MAXSIZE][MAXSIZE];//邻接矩阵
int vertex_num;
int edge_num;
} Mat_Grph;
//创建图的邻接矩阵
void create_graph(Mat_Grph* G)
{
G->vertex_num = 4;
G->edge_num = 4;//有向边数为4(与无向图不一样)
//顶点命名
G->vertex[0] = '0';
G->vertex[1] = '1';
G->vertex[2] = '2';
G->vertex[3] = '3';
//初始化邻接矩阵
for (int i = 0; i < G->vertex_num; i++)
{
for (int j = 0; j < G->vertex_num; j++)
{
G->arc[i][j] = 0;
}
}
//添加有向边(与无向图不一样)
G->arc[0][3] = 1;
G->arc[1][0] = 1;
G->arc[1][2] = 1;
G->arc[2][0] = 1;
G->arc[2][1] = 1;
}
//输出邻接矩阵
void print_matrix(Mat_Grph G)
{
printf("邻接矩阵表示如下:\n");
for (int i = 0; i < G.vertex_num; i++)
{
for (int j = 0; j < G.vertex_num; j++)
{
printf("%d ", G.arc[i][j]);
}
printf("\n");
}
}
int main(int argc, char const* argv[])
{
Mat_Grph G;
create_graph(&G);
print_matrix(G);
return 0;
}
2.3 带权值的有向图-邻接矩阵存储
(1)

邻接矩阵中,0代表自己到自己,没有连接用无穷表达,有连接就写权值。
(2)代码
与无向图代码有区别的地方已经标出。
c
typedef char VertexType;//顶点类型
typedef int EdgeType;//边的权值类型(0或1)
#define MAXSIZE 100//最大顶点数
#define Max 0x7fffffff//用一个极大值表示无穷,即没有边(与无向图不一样)
//图的邻接矩阵结构体定义
typedef struct
{
VertexType vertex[MAXSIZE];//顶点数组
EdgeType arc[MAXSIZE][MAXSIZE];//邻接矩阵
int vertex_num;
int edge_num;
} Mat_Grph;
//创建图的邻接矩阵
void create_graph(Mat_Grph* G)
{
G->vertex_num = 4;
G->edge_num = 5;//5条边(与无向图不一样)
//顶点命名
G->vertex[0] = '0';
G->vertex[1] = '1';
G->vertex[2] = '2';
G->vertex[3] = '3';
//初始化邻接矩阵(与无向图不一样)
for (int i = 0; i < G->vertex_num; i++)
{
for (int j = 0; j < G->vertex_num; j++)
{
G->arc[i][j] = Max;
if (i = j)
{
G->arc[i][j] = 0;
}
}
}
//添加有向边(与无向图不一样)
G->arc[0][3] = 3;
G->arc[1][0] = 5;
G->arc[1][2] = 2;
G->arc[2][0] = 6;
G->arc[2][1] = 4;
}
//输出邻接矩阵(与无向图不一样)
void print_matrix(Mat_Grph G)
{
printf("邻接矩阵表示如下(Max表示无边):\n");
for (int i = 0; i < G.vertex_num; i++)
{
for (int j = 0; j < G.vertex_num; j++)
{
if (G.arc[i][j] == Max)
printf("Max ");
else
printf("%3d ", G.arc[i][j]);
}
printf("\n");
}
}
int main(int argc, char const* argv[])
{
Mat_Grph G;
create_graph(&G);
print_matrix(G);
return 0;
}
2.4 无向图-邻接表存储
注:
假设无向图G的边为:A-B、B-C、A-C。
A的邻接点:B、C。
B的邻接点:A、C。
C的邻接点:A、B。
若为有向图(边A→B、B→C):
A的邻接点:B。
B的邻接点:C。
注:表=链表。边表=存放边信息的链表。
(1)

(2)代码
c
#define MAXVEX 100;//最大顶点数
//定义边表结点结构体
typedef struct EdgeNode {
int adjvex;//表示邻接点下标
struct EdgeNode* next;
}EdgeNode;
//定义顶点表结点结构体
typedef struct VertexNode {
char data;//顶点信息
EdgeNode* firstedge;//指向边表的头指针
}VertexNode,AdjList[MAXVEX];//AdjList adjList = VertexNode adjList[MAXVEX]
//图的结构体
typedef struct {
AdjList adjList;
int numVertexes;//顶点数
int numEdges;//边数
}GraphAdjList;
//创建图(无向图)
void createGraph(GraphAdjList* G)
{
G->numVertexes = 4;
G->numEdges = 5;
//顶点命名(V0~V3)
for (int i = 0; i < G->numVertexes; i++)
{
G->adjList[i].data = '0' + i;
G->adjList[i].firstedge = NULL;
}
//边集合(无向边,需添加两次)
int edges[5][2] = {
{0,1},{0,2},{0,3},{1,2},{2,3}
};
for (int i = 0; i < G->numEdges; i++)
{
int from = edges[i][0];
int to = edges[i][1];
//from->to
EdgeNode* e1 = (EdgeNode*)malloc(sizeof(EdgeNode));
e1->adjvex = to;
e1->next = G->adjList[from].firstedge;
G->adjList[from].firstedge = e1;
//to->from(因为是无向图)
EdgeNode* e2 = (EdgeNode*)malloc(sizeof(EdgeNode));
e2->adjvex = from;
e2->next = G->adjList[to].firstedge;
G->adjList[to].firstedge = e2;
}
}
//打印邻接表
void printGraph(GraphAdjList G)
{
printf("图的邻接表如下:\n");
for (int i = 0; i < G.numVertexes; i++)
{
printf("V%c -> ", G.adjList[i].data);
EdgeNode* p = G.adjList[i].firstedge;
while (p)
{
printf("%d -> ", p->adjvex);
p = p->next;
}
printf("\n");
}
}
int main()
{
GraphAdjList G;
createGraph(&G);
printGraph(G);
return 0;
}
2.5 有向图-邻接表存储
(1)

(2)代码
只需要把无向图中 to -> from 的部分删除即可。
2.6 逆邻接表
在有向图中,邻接表只能表达顶点出边的情况,不能表达入边的情况。逆邻接表可以表达入边的情况。
(1)

(2)代码
把"有向图-邻接表存储"中的 from -> to,改为 to->from即可。
2.7 带权值的图-邻接表存储
(1)

(2)代码
在EdgeNode结点中加入一个变量weight即可。
2.8 十字链表
在有向图中,想既表达入边也表达出边,就可以用十字链表来存储该图。


tailvex 和 headvex 是弧尾和弧头的意思。
2.9 邻接多重表
在无向图中,若有5条边,那么用邻接表来存储时,会有10个结点,空间利用上会冗余,此时用邻接多重表可以解决这个问题,5条边就5个结点。
(1)

(2)

3.图的遍历
3.1 深度优先遍历
(1)


(2)代码
c
typedef char VertexType;
typedef int EdgeType;
#define MAXSIZE 100
typedef struct
{
VertexType vertex[MAXSIZE];
EdgeType arc[MAXSIZE][MAXSIZE];
int vertex_num;
int edge_num;
}Mat_Grph;
int visited[MAXSIZE];
void create_graph(Mat_Grph* G)
{
G->vertex_num = 9;
G->edge_num = 15;
G->vertex[0] = 'A';
G->vertex[1] = 'B';
G->vertex[2] = 'C';
G->vertex[3] = 'D';
G->vertex[4] = 'E';
G->vertex[5] = 'F';
G->vertex[6] = 'G';
G->vertex[7] = 'H';
G->vertex[8] = 'I';
for (int i = 0; i < G->vertex_num; i++)
{
for (int j = 0; j < G->vertex_num; j++)
{
G->arc[i][j] = 0;
}
}
G->arc[0][1] = 1;
G->arc[0][5] = 1;
G->arc[1][2] = 1;
G->arc[1][6] = 1;
G->arc[1][8] = 1;
G->arc[2][3] = 1;
G->arc[2][8] = 1;
G->arc[3][4] = 1;
G->arc[3][6] = 1;
G->arc[3][7] = 1;
G->arc[3][8] = 1;
G->arc[4][5] = 1;
G->arc[4][7] = 1;
G->arc[5][6] = 1;
G->arc[6][7] = 1;
for (int i = 0; i < G->vertex_num; i++)
{
for (int j = 0; j < G->vertex_num; j++)
{
G->arc[j][i] = G->arc[i][j];
}
}
}
void dfs(Mat_Grph G, int i)
{
visited[i] = 1;
printf("%c\n", G.vertex[i]);
for (int j = 0; j < G.vertex_num; j++)
{
if (G.arc[i][j] == 1 && visited[j] == 0)
{
dfs(G, j);
}
}
}
int main()
{
Mat_Grph G;
create_graph(&G);
for (int i = 0; i < G.vertex_num; i++)
{
visited[i] = 0;
}
dfs(G, 0);
return 0;
}
3.2 广度优先遍历
(1)

(2)代码
把深度优先遍历中的dfs换成bfs即可。
c
void bfs(Mat_Grph G)
{
int i = 0;
visited[i] = 1;
printf("%c\n", G.vertex[i]);
queue[rear] = i;
rear++;
while (front != rear)
{
i = queue[front];
front++;
for (int j = 0; j < G.vertex_num; j++)
{
if (G.arc[i][j] == 1 && visited[j] == 0)
{
visited[j] = 1;
printf("%c\n", G.vertex[j]);
queue[rear] = j;
rear++;
}
}
}
}
4.最小生成树
注:带权连通图,任意环中包含的边的权值不同时,MST(最小生成树)唯一。(充分不必要条件)
4.1 最小生成树-普里姆(Prim)
关注"点"
(1)


(2)代码

c
void prim(Mat_Grph* G)
{
int i, j, k;
int min;
int weight[MAXSIZE];
int vex_index[MAXSIZE];//vex_index 的值表示出发点,下标表示到达点
weight[0] = 0;
vex_index[0] = 0;
for (i = 1; i < G->vertex_num; i++)
{
weight[i] = G->arc[0][i];
vex_index[i] = 0;
}
for (i = 1; i < G->vertex_num; i++)
{
min = MAX;
j = 0;
k = 0;
while (j < G->vertex_num)
{
if (weight[j] != 0 && weight[j] < min)
{
min = weight[j];
k = j;
}
j++;
}
printf("(%c,%c)\n", G->vertex[vex_index[k]], G->vertex[k]);
weight[k] = 0;
for (j = 0; j < G->vertex_num; j++)
{
if (weight[j] != 0 && G->arc[k][j] < weight[j])
{
weight[j] = G->arc[k][j];
vex_index[j] = k;
}
}
}
}
4.2 最小生成树-克鲁斯卡尔(Kruskal)
关注"边"
(1)

(2)

c
typedef struct
{
int begin;
int end;//begin和end表达的是顶点的下标
int weight;
}Edge;
void sortEdges(Edge edges[], int edge_num)
{
for (int i = 0; i < edge_num; i++)
{
for (int j = i + 1; j < edge_num; j++)
{
if (edges[i].weight > edges[j].weight)
{
swap(edges, i, j);
}
}
}
}
int find(int* parent,int index)
{
while (parent[index] > 0)
{
index = parent[index];
}
return index;
}
void Kruskal(Mat_Grph G)
{
Edge edges[MAXEDGE];
int k = 0;
for (int i = 0; i < G.vertex_num; i++)
{
for (int j = i + 1; j < G.vertex_num; j++)
{
if (G.arc[i][j] < MAX)
{
edges[k].begin = i;
edges[k].end = j;
edges[k].weight = G.arc[i][j];
k++;
}
}
}
sortEdges(edges, G.edge_num);
int parent[MAXSIZE];//parent 数组,值表达一个顶点,下标表达另一个顶点
for (int i = 0; i < G.vertex_num; i++)
{
parent[i] = 0;
}
int n, m;
for (int i = 0; i < G.edge_num; i++)
{
n = find(parent, edges[i].begin);
m = find(parent, edges[i].end);
if (n != m)
{
parent[n] = m;
printf("(%c,%c) %d\n", G.vertex[edges[i].begin],G.vertex[edges[i].end], edges[i].weight);
}
}
}
4.3 两个算法的比较

5.最短路径
单源最短路径:从某一个顶点出发,到图中所有其他顶点的最短路径。
任意两点之间的最短路径:即图中任意两个顶点之间的最短路径。
5.1 最短路径-迪杰斯特拉(Dijkstra)
确定一个起点,算出该起点到其他所有点的最短路径。
(1)

(2)代码

c
//返回下一次要观察哪一个顶点
int choose(int distance[], int found[], int vertex_num)
{
int min = MAX;
int minPos = -1;
for (int i = 0; i < vertex_num; i++)
{
if (distance[i] < min && found[i] == 0)
{
min = distance[i];
minPos = i;
}
}
return minPos;
}
void dijkstra(Mat_Graph G, int begin)
{
int found[MAXSIZE];//顶点是否已经走过
int path[MAXSIZE];//路径
int distance[MAXSIZE];//begin到达每一个顶点的最短距离
for (int i = 0; i < G.vertex_num; i++)
{
found[i] = 0;
path[i] = -1;
distance[i] = G.arc[begin][i];
}
found[begin] = 1;
distance[begin] = 0;
int next;//下一个要观察的顶点
for (int i = 1; i < G.vertex_num; i++)
{
next = choose(distance, found, G.vertex_num);
found[next] = 1;
for (int j = 0; j < G.vertex_num; j++)
{
if (found[j] == 0)
{
if (distance[next] + G.arc[next][j] < distance[j])
{
distance[j] = distance[next] + G.arc[next][j];
path[j] = next;
}
}
}
}
for (int i = 1; i < G.vertex_num; i++)
{
printf("V0 -> v%d : %d\n", i, distance[i]);
int j = i;
printf("V%d <- ", i);
while (path[j] != -1)
{
printf("V%d <- ", path[j]);
j = path[j];
}
printf("V0\n");
}
}
5.2 最短路径-弗洛伊德(Floyd)
得到任意两个点之间的最短路径。
(1)核心思想:
尝试让每一个顶点去做"中转站",看看能不能让两个点之间的路径变得更短。假设图中有n个顶点,那么就依次尝试用顶点0,1,2,...,n-1作为"中转站"去更新任意两个顶点之间的路径长度。
(2)
distance数组:

path数组:

6. 拓扑排序与关键路径
6.1 拓扑排序
注:在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先级,这样的有向图为顶点表示活动的网,我们称为AOV网。
(1)思路
持续寻找入度为0的顶点,然后入栈、出栈。
(2)实现拓扑排序的算法使用邻接表,而不是邻接矩阵。

左边是邻接矩阵,右边是邻接表。
(3)代码

①

②
c
typedef struct
{
VertexType vertex[MAXSIZE];
EdgeType arc[MAXSIZE][MAXSIZE];
int vertex_num;
int edge_num;
}Mat_Graph;
typedef struct EdgeNode
{
int edge_vex;
struct EdgeNode* next;
}EdgeNode;
typedef struct VertexNode
{
int in;
VertexType data;
EdgeNode* head;
}VertexNode;
typedef VertexNode Adj_List[MAXSIZE];
typedef struct
{
Adj_List adj_list;
int vertex_num;
int edge_num;
}Adj_Graph;
typedef Adj_Graph* Adj_List_Graph;
void topological_sort(Adj_List_Graph ALG)
{
EdgeNode* e;
int curr;
int k;
for (int i = 0; i < ALG->vertex_num; i++)
{
if (ALG->adj_list[i].in == 0)
{
push(i);
}
}
while (is_empty() != 0)
{
curr = pop();
printf("V%d -> ", ALG->adj_list[curr].data);
e = ALG->adj_list[curr].head;
while (e != NULL)
{
k = e->edge_vex;
ALG->adj_list[k].in--;
if (ALG->adj_list[k].in == 0)
{
push(k);
}
e = e->next;
}
}
}
6.2 关键路径
注:在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,称为AOE网。
(1)etv:事件最早发生时间。

(2)ltv:事件最晚发生时间。

(3)代码:

代码见视频。
7. 图的应用-真题
7.1

注:存储稀疏图,用邻接表比邻接矩阵更省空间。
7.2

7.3

7.4

7.5

选 c。
7.6

这里问的是活动d的最早和最迟开始时间,而不是事件2的。注意千万不可混淆。
7.7
