数据结构 Chap 6/图

一、定义        

        图(Graph)由顶点集(Vertex)和边集(Edge)组成。|V|表示图G中顶点个数,也称图G的阶。

|E|表示图G中边的条数。

注意:线性表可以是空表,树可以为空树,但是图不可为空,即V一定是非空集。

顶点可以没有边,但是边不能没有顶点。

二、一些基本概念

1.无向图、有向图

        无向图:若E为无向边的有限集合。边是顶点的无序对,记为(v,w)或(w,v)。

        有向图:若E为有向边(也称弧)的有限集合。弧是顶点的有序对,记为<v,w>,其中v,w是顶点,v称弧尾,w为弧头,<v,w>称为从顶点v到w的弧,也称v邻接到w或w邻接自v。【注意方向】

<v,w>\neq <w,v>

2.简单图、多重图

        简单图:不存在重复边绕回自己的边【数据结构只探讨这个】

        多重图:图G中某两个结点之间的边数多余一条,也允许顶点通过同一条边和自己关联。

3.顶点的度、入度、出度

        无向图中,顶点v的度是指依附于该顶点的边的条数,记为TD(v)。

        在具有n个顶点、e条边的无向图中,总度数一定为2e,因为每条边给左顶点一个度,右顶点一个度。即无向图的全部顶点的度之和为边数的两倍。


        有向图中,入度是以顶点v为终点的有向边的数目,记为ID(v)。出度是以顶点v为起点的有向边的数目,记为OD(v)。

        顶点v的度等于其入度和出度之和,即TD(v)=ID(v)+OD(v)。在具有n个顶点、e条边的有向图中,入度之和等同于出度之和等于e,也就是边的数量。【很好理解,一条有向边,肯定提供一个入度、一个出度】

4.顶点之间的关系描述

路径:顶点A到D的一条路径是指顶点序列ABCD。

回路:第一个顶点和最后一个顶点相同的路径成为回路/环。

简单路径:路径序列中,顶点不重复出现的路径。

简单回路:除了首尾两个顶点外,其余顶点不重复出现的回路。

路径长度:路径上的边的数目。

点到点的距离:从顶点u出发到顶点v的最短路径。若不存在该路径,则称距离为无穷大。

连通:无向图中,从A到B有路径。

强连通:有向图中,从A到B有路径,且从B到A也有路径。

5.连通图、强连通图

        连通图:无向图中,任意两个顶点间都有路径。

        考点:对于n个顶点的无向图G,若为连通图,则至少有n-1条边。

        若G为非连通图,则最多有C_{n-1}^{2}条边。把一个点排外,然后大家两两连接。


        强连通图:有向图中,任意两个顶点都是强连通的。

        考点:对于n个顶点的有向图G,若为强连通图,则最少有n条边(形成回路)

6.子图

        在有向图/无向图中,取其中的一些顶点、边构成新的图。【注意满足图的条件】

        取出全部顶点的子图,成为生成子图。        

7.连通分量【重要!】

        定义:无向图中的极大连通子图称为连通分量。【最大的可以连通的子图,不唯一】

        子图必须连通,且包含尽可能多的顶点和边。

8.强连通分量

        ABCDE互相强连通。而F不能进入ABCDE,是因为B到F可以,但是F没有到B的路径,无法和他们强连通,所以ABCDE已经是最大的连通的子图了。

9.生成树【同一个连通图中,撇去多余的边】

        无向图中,连通图的生成树是包含图中全部顶点的一个极小连通子图。【村庄修路问题/答案不唯一】

若图中顶点数为n,则生成树一定含有n-1条边。如果少一条就无法连通,多一条就会形成回路。

10.生成森林【非连通图中,撇去多余的边】

        在非连通图中,连通分量(内部的连通图)的生成树构成了非连通图的生成森林。

11.带权图/网

12.几种特殊形态的图

(1)无向完全图/有向完全图

 (2)树是什么图

13.小结

三、图的存储

1.邻接矩阵法

(1)意义及代码实现:

 如果元素为1,表示从X到Y有一步路径,如果为0则表示无法到达。

无向图:邻接矩阵一定是对称矩阵。【所以这里可以使用压缩存储】

有向图:邻接矩阵不一定是对称矩阵。

(2)如何通过邻接矩阵求图的度

无向图:所在的行或者列中,含有1的个数。

有向图:行表示出度,列表示入度

时间复杂度O(n)=O(|v|)

(3)邻接矩阵法存储带权图(网)

到达自己:可以用无穷表示,也可以用0表示。

 (4)邻接矩阵法的性能分析

空间复杂度较高,为O(n^2),只与顶点数有关,和实际的边数无关。

适用于存储稠密图【边越多,空间的使用率越高】

 例如上图:

花了1步,从A走到C的路径一共有1条。

花了3步,从B走到B的路径一共有3条。

2.邻接表法【顺序+链式存储】

3.十字链表、邻接多重表

(1)十字链表法->有向图

缺点:邻接矩阵【边多,空间复杂度高】

邻接表【找顶点的入边/入度不方便,需要遍历】

        为了克服这些困难,采用十字链表表法,既可以实现边的存储空间为O(|V|),又能很方便地找到入边。

(2)邻接多重表->无向图

缺点:邻接矩阵【空间复杂度高】

邻接表【每条边对应两条冗余信息,删除插入麻烦,时间复杂度高】

四、图的基本操作

 

五、图的遍历

        以某种顺序访问所有结点,并且所有结点只会被访问一次。

1.BFS(Breadth First Search)

        类似于树的层次遍历:需要一个辅助队列。

#define Max_Vertex_Num 100
bool visited[Max_Vertex_Num];
Graph G;
void BFSTraverse(Graph G)
{
	for (int i = 0; i < G.vexnum; ++i)//从0号顶点开始遍历
		visited[i] =false;//初始化所有顶点。
	InitQueue(Q);
	for (int i = 0; i < G.vexnum; ++i)
		if (!visited[i])
			BFS(G, i);//执行完第一次后,第一个连通分量的所有顶点均被遍历,开始第二次for循环
}
void BFS(Graph G, int v)//这里v表示顶点序号,往后进行广度优先遍历
{
	visit(v);//访问初始顶点
	visited[v] = true;//被访问后的顶点标记为true
	Enqueue(Q, v);//被访问的顶点入队
	while (!isEmpty(Q))
//非空->删除初始顶点->访问相邻所有顶点且入队->删除队列首顶点且遍历其所有(未被访问的)相邻顶点...
	{
		DeQueue(Q, v);//删除队列的第一个顶点,下面找他的所有邻居
		for (w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w))
			//这里的0表示序号,只要有邻接点就能入,没有时,返回的是-1;
			if (!visited[w])//判断邻接点是否被访问过
			{
				visit(w);
				visited[w] =true;
				EnQueue(Q, w);
			}
	}
}

(1)定义:从一个顶点出发,先访问它的下一层所有孩子,再从其中一个孩子出发访问其下一层的所有孩子,以此类推,直到访问完所有顶点。

(2)时间复杂度:访问顶点的时间+访问边的时间

空间复杂度:源于辅助队列。最坏情况:如果只有两层,一次就能带入所有顶点进来,那么队列需要很大。

(3)广度优先生成树

        如果是邻接矩阵表示法:则得到的广度优先生成树唯一。

        如果是邻接表的表示法:则得到的广度优先生成树不唯一

2.DFS(Depth First Search)

#define Max_Vertex_Num 100
bool visited[Max_Vertex_Num];
Graph G;
void DFSTraverse(Graph G)
{
	for (int i = 0; i < G.vexnum; ++i)
		visited[i] =false;
	for (int i = 0; i < G.vexnum; ++i)
		if (!visited[i])
			DFS(G, i);//执行完第一次后,第一个连通分量的所有顶点均被遍历,开始第二次for循环
}
void DFS(Graph G, int v)//这里v表示顶点序号,往后进行广度优先遍历
{
	visit(v);
	visited[v] = true;	
	for (w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w))
	{
		if (!visited[w])
			DFS(G, v);
	}
}

(1)定义:一旦出现新结点,就先访问一个新结点。

(2)时间复杂度:访问顶点的时间+访问边的时间

         空间复杂度:源于递归工作栈【DFS调用自己的次数】。最坏情况:如果树很深,要层层递归带入所有顶点进来,那么递归次数会增加很多。

(3)深度优先生成树:

同样地,如果是邻接矩阵表示法:则得到的深度优先生成树唯一。

               如果是邻接表的表示法:则得到的深度优先生成树不唯一

3.图的遍历与连通性

(1)无向图:进行BFS\DFS的次数与连通分量数有关

(2)有向图:具体问题具体分析。若是强连通图(起始顶点到其他顶点都有路径,则只需调用1次BFS\DFS),则肯定只需调用一次。

六、最小生成树及最短路径问题

1.最小生成树定义:

        包含所有顶点的一个极小连通子图。【边尽可能少,但是保持连通】

2.Prim算法 vs Kruskal算法

(1)Prim算法实现思想

 (2)Kruskal算法实现思想

 3.最短路径问题

(1)单源最短路径【BFS及Dijkstra算法】

        a.BFS算法【无权图】

#include <string>
#include <limits>
#define Max_Vertex_Num 100
Graph G;
void BFS_MIN_Distance(Graph G, int u)
{
	for (i = 0; i < G.vexnum;i++)
	{
		d[i] = INT_MAX;//记录到起始顶点的距离
		path[i] = -1;//记录前驱节点
	}
	//访问起始顶点
	d[u] = 0;
	visited[u] = true;
	EnQueue(Q, u);
	while (!isEmpty)
	{
		DeQueue(Q, u);
		for (w = FirstNeighbor(G, u); w >= 0; w = NextNeighbor(G, u, w))
		{
			if (!visited[i])
				d[w] = d[u] + 1;
			path[w] = u;
			visited[w] = true;
			EnQueue(Q, u);
		}//if
	}//while
}

 b.迪杰斯特拉算法

        (贪心算法&并非最短路径)【带权图、无权图】

Dijkstra算法_dijkstra算法的优点_Hello.Reader的博客-优快云博客

过程:步步为营

1.每次需从尚未找到最短路径的点【final==false】出发,找distance最短的点纳入。2.此时标记该点的final为true。

3.然后再遍历下一层,更改distance

不能处理带有负权边的图】

          例(1):按照Dijkstra的思想,首先0点加入集合,然后1点加入集合,然后3点加入集合,最后2加入集合,此时0->2更新为99,但是此刻已经无法更新0->1和0->3了。

例(2):

【只能解决单源最短路径问题】

(2)各顶点间的最短路径【Floyd算法】

        (动态规划)【带权图、无权图】

基本思想:

a.先不中转,都是直达,遍历的得到最短路径表。【path表的值均为-1】

b.在上表基础上,可以从V0中转更新最短路径表A;【path表也更新,值为上一个来源点】。

c.在上表基础上,可以从V0、V1中转更新最短路径表A...

d.直到遍历完去所有顶点,可以从所有点中转,得到最新的最短路径表A和路径表path。

Floyd算法可以求带负权值的边,但是无法解决带有“带负权值边的回路”,因为有可能值会越转越小。

 总结:

 BFS算法源于广度优先算法,其时间复杂度取决于用何种存储结构。(时间复杂度同深度优先)

【邻接矩阵:找顶点,遍历邻接顶点O(V^2)】

【邻接表:找顶点,找邻接边O(V+E)】

4.有向无环图描述表达式

(1)有向无环图定义:

(2)描述表达式:

 3.练习

 5.拓扑排序

(1)预备知识:

AOV网:Activity On Vertex Network,用顶点表示活动的网。

用DAG图(Directed Acyclic Graph)有向无环图表示一个工程。顶点表示活动,有向边<Vi,Vj>表示活动Vi先于Vj发生。

(2)拓扑排序定义

        找到做事的先后顺序。

(3)如何实现:

a.从AOV网中选择一个没有前驱(入度为0)的顶点并输出。

b.从网中删除该顶点和所有以它为起点的有向边。

c.重复a和b操作直到当前AOV网为空或者当前网不存在无前驱的顶点为止【顶点都有前驱】

注: 加粗文字表示有回路,此时无法进行拓扑排序。在代码最后也有相应的判断程序。

a.(正)拓扑排序

#include <iostream>
using namespace std;
#include <string>
#include <list>
#include <queue>

/****************类声明*********************/
class Graph
{
	int V;							//顶点个数
	list<int>* adj;					//邻接表,也可用数组代替
	queue<int> q;					//维护一个入度为0的顶点的集合【队列/栈】
	int* indegree;					//记录每个顶点的入度,数组实现
public:
	Graph(int V);					//构造函数
	~Graph();						//析构函数
	void addEdge(int v, int w);		//添加边
	bool topological_sort();		//拓扑排序
};
Graph::Graph(int V)
{
	this->V = V;
	adj = new list<int>[V];
	indegree = new int[V];
	for (int i = 0; i < V; i++)
		indegree[i] = 0;
}
Graph::~Graph()
{
	delete[] adj;
	delete[] indegree;
}
void Graph::addEdge(int v, int w)
{
	adj[v].push_back(w);
	++indegree[w];
}
bool Graph::topological_sort()
{
	for (int i = 0; i < V; i++)
		if (indegree[i] == 0)
			q.push(i);				//入度为0的顶点入队
	int count = 0;					//计数,记录当时【已经从队列输出】的顶点数目
	while (!q.empty())
	{
		int v = q.front();			//从队列取出一个顶点
		q.pop();
		cout << v << "\t";			//【出栈即输出】,也就是得到这个入度为0的点
		++count;
		list<int>::iterator beg = adj[v].begin();	//iterator beg是迭代器,从头开始
		for (; beg != adj[v].end(); ++beg)     //让与该结点相连的下一个结点入度-1,如何实现?
			if (!(--indegree[*beg]))
				q.push(*beg);
	}
	if (count < V)					//未输出全部顶点:有向图中包含回路,无法实现拓扑排序
		return false;
	else
		return true;
}

 b.逆拓扑排序

        1.定义:让出度为0的先出【事情的最后一件事先出】,从而得到逆拓扑排序。

        2.存储方式:这里删除出度为0,还要删除指向它的边。如果采用邻接表的话,则需要遍历所有顶点;而邻接矩阵只需遍历这一列,比较方便。

        此外,还可以采用逆邻接表的方式,这样就只需遍历这一条边【也就是需要预处理一下】

        3.DFS(深度优先)算法实现逆拓扑排序【将访问的功能消失,让最深处的顶点,退栈时输出】

 6.关键路径

(1)定义:从源点到汇点的 有向路径可能有多条,所有路径中,具有最大路径长度的路径成为关键路径【决定整个工程的最短时间/距离】,而把关键路径上的活动称为关键活动【边】

【关键路径可能有多条】

(2)寻找关键路径

正着推:求最大

反着推:求最小

a.求事件(顶点)的最早发生时间

b.求事件(顶点)的最晚发生时间

c.根据顶点最早发生时间,标定所有活动(每条边)的最早发生时间【保持一致】。

d.根据顶点最晚发生时间减去该活动的运行时间,标定所有活动(每条边)的最晚发生时间。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值