数据结构:第7讲:图

目录

  1. 图的基本概念
  2. 图的存储
  3. 图的遍历
  4. 最小生成树
  5. 最短路径
  6. 拓扑排序与关键路径
  7. 图的应用-真题

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

相关推荐
林森lsjs1 小时前
零基础吃透二叉树:定义、遍历与高频算法 —数据结构柒
java·开发语言·数据结构·算法·二叉树
船厂电气自动化ai大模型1 小时前
AI大模型与数学·第56课 快速傅里叶变换FFT:DFT高效优化算法,图像、音频、扩散模型工程加速核心工具
数据结构·人工智能·深度学习·算法·机器学习
疯狂打码的少年1 小时前
【数据库技术】数据库三级模式结构(外模式/模式/内模式)
数据库·笔记·oracle
橘子汽水1682 小时前
Leetcode 236,437:二叉树的最近公共祖先,路径总和III
java·数据结构
M78佐菲2 小时前
Linux学习笔记:进程回收、exec函数簇与线程
linux·笔记·学习·算法
商业白皮书2 小时前
用手机怎么拍证件照?光线、机位、姿态和后期制作步骤
笔记
wabs6663 小时前
关于二叉树【力扣144.二叉树的前序遍历的思考】
数据结构·c++·算法·leetcode·二叉树
GlueNa2SiO33 小时前
05-Flask表单处理与文件上传
笔记·python·学习·flask
STQY燊桐启元(深圳)电子科技3 小时前
免涂硅脂新时代,ST‑PCMTC96 与 ST‑PCM180 相变陶瓷片重构功率器件热管理
经验分享·笔记·重构·pcm