《软件技术基础》之《图》


图是常用的重要的一类数据结构,树可以看成是图的特例,树中每个数据元素至多允许一个前驱,只能反映数据元素之间一对多的关系,而图没有该限制,允许数据元素有多个前驱,因此可以反映数据元素之间多对多的关系。

图的概念

图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E)。
其中,G表示一个图,V是图G中顶点的有穷非空集合,E是图G中边的有穷集合。

E是空集时,图G只有顶点没有边。

图的有关术语:

  1. 无向边:若顶点Vi到Vj之间的边没有方向,则称这条边为无向边,用无序偶对(Vi,Vj) 来表示。如下左图,G= (V1,{E1}),其中顶点集合V1={A,B,C,D};边集合E1={ (A,B) ,(B,C),(C,D), (D,A) , (A,C) } 。

  2. 有向边:若从顶点Vi 到Vj的边有方向,则称这条边为有向边,也称为弧。用有序偶〈Vi,Vj>来表示, Vi称为弧尾, Vj称为弧头。 如果图中任意两个顶点之间的边都是有向边,则称该图为有向图。 连接顶点A到D的有向边就是弧,A是弧尾,D是弧头,<A, D>表示弧,注意不能写成<D, A>。如下右图,G= (V2,{E2}),其中顶点集合V2={A,B,C,D}; 弧集合E2={<A,D>,<B,A>,<C,A>,<B,C>}。
    在这里插入图片描述

  3. 简单图:在图中,若不存在顶点到自身的边,且同一条边不重复出现,称该图为简单图。

  4. 无向完全图:在无向图中,若任意两个顶点之间都存在边,称该图为无向完全图。含有n个顶点的无向完全图有(n(n-1)/2)条边。如下图所示:
    在这里插入图片描述

  5. 有向完全图:在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。含有n个顶点的有向完全图有n(n-1)条边,如下图所示:
    在这里插入图片描述

  6. :有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的树叫做权。这些权可以表示一个顶点到另一个顶点的距离或耗费。这种带权的图常称为网。如下图所示:
    在这里插入图片描述

  7. :顶点的度是指关联该顶点的边的数目。

  8. 子图:子图是图的边(及边所关联的顶点)的子集所形成的图。

  9. 路径:图中的路径指的是一系列相邻顶点。简单路径是一条不包含重复顶点的路径。环路是起点和终点相同的路径。如果两盒顶点之间存在一条路径,则称这两个顶点是连通的。如果图中每对顶点之间都有路径相连,则称该图是连通图。如果一个图是非连通的,那么它是由一组连通分量组成。

  10. 有很少条边或弧的图称为稀疏图,反之称为稠密图,这里的概念是相对而言的。

图的存储结构

图最常见的表示形式为邻接链表邻接矩阵。邻接链接在表示稀疏图时非常紧凑而成为了通常的选择,相比之下,如果在稀疏图表示时使用邻接矩阵,会浪费很多内存空间,遍历的时候也会增加开销。但是,这不是绝对的。如果图是稠密图,邻接链表的优势就不明显了,那么就可以选择更加方便的邻接矩阵。

还有,顶点之间有多种关系的时候,也不适合使用矩阵。因为表示的时候,矩阵中的每一个元素都会被当作一个表。

邻接矩阵(数组表示法)

图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。设图G有n个顶点,则邻接矩阵是一个nxn的方阵A,定义为:
在这里插入图片描述
示例:
在这里插入图片描述
在这里插入图片描述
我们知道,每条边上带有权的图叫做网,如果要将这些权值保存下来,可以采用权值代替矩阵中的0、1,权值不存在的元素之间用∞表示,定义为:
在这里插入图片描述

如下图,左图是一个有向网图,右图就是它的邻接矩阵。
在这里插入图片描述
邻接矩阵的特点:

  1. 判定两个顶点Vi与Vj是否关联,只需判断A[i,j]是否为1;
  2. 求顶点的度容易。
    在这里插入图片描述
    邻接矩阵的定义的代码实现:
#define MaxVerterNum 100
typedef char VerterType;
typedef int EdgeType;
typedef struct {
VerterType vexs[MaxVerterNum]; // 存储顶点的一维数组
EdgeType edges[MaxVerterNum][MaxVerterNum]; // 存储邻接矩阵的二维数组
int n,e; // 图当前的顶点数和边数
}MGraph;

邻接链表

对于边数相对顶点较少的图,这种结构是存在对存储空间的极大浪费的,特别是稀疏有向图。所以可以考虑用链表来按需存储。数组与链表相结合的存储方法称为邻接表。

处理办法:

  1. 图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过数组可以较容易地读取顶点信息,更加方便。另外,对于顶点数组中,每个数据元素还需要存储指向第一个邻接点的指针,以便于查找该顶点的边信息。
  2. 图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点vi 的边表,有向图则称为顶点vi作为弧尾的出边表。

如图是一个无向图的连接表结构,有向图则类似。
在这里插入图片描述
无向图邻接表的特点:
3. n个顶点,e条边的无向图,需n个表头结点和2e个链表结点;
4. 顶点Vi的度等于链表i中的链表结点数。

对于带权值的网图,可以在边表结点定义中再增加一个weight 的数据域,存储权值信息即可,如下图所示。
在这里插入图片描述
有向图邻接表的特点:
5. n个顶点,e条弧的无向图,需n个表头结点和e个链表结点;
6. 链表i中的链表结点数为顶点Vi的出度。

有向图逆邻接表
与无向图的邻接表结构一样。只是在链表i上的结点是以Vi为弧头的各弧尾顶点。
在这里插入图片描述
此时,链表i上的结点数为Vi的入度。

邻接表的结构定义和建立算法:

typedef char VertexType; /* 顶点类型应由用户定义 */
typedef int EdgeType; /* 边上的权值类型应由用户定义 */
 
typedef struct EdgeNode /* 边表结点  */
{
	int adjvex;    /* 邻接点域,存储该顶点对应的下标 */
	EdgeType info;		/* 用于存储权值,对于非网图可以不需要 */
	struct EdgeNode *next; /* 链域,指向下一个邻接点 */
}EdgeNode;
 
typedef struct VertexNode /* 顶点表结点 */
{
	VertexType data; /* 顶点域,存储顶点信息 */
	EdgeNode *firstedge;/* 边表头指针 */
}VertexNode, AdjList[MAXVEX];
 
typedef struct
{
	AdjList adjList; 
	int numNodes,numEdges; /* 图中当前顶点数和边数 */
}GraphAdjList;

图的邻接矩阵与邻接表表示的比较

在这里插入图片描述

图的遍历

从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历。
两种常用遍历图的方法:深度优先搜索、广度优先搜索。

深度优先搜索(DFS)

从图中某个顶点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v 有路径相通的顶点都被访问到。

深度优先搜索遍历图的过程为:
在这里插入图片描述
示例:
在这里插入图片描述
深度遍历算法的流程图:
在这里插入图片描述

邻接矩阵的深搜代码如下:

boolean visited[MAXVEX]; /* 访问标志的数组 */
 
/* 邻接矩阵的深度优先递归算法 */
void DFS(MGraph G, int i)
{
	int j;
 	visited[i] = true;
 	printf("%c ", G.vexs[i]);/* 打印顶点,也可以其它操作 */
	for(j = 0; j < G.numVertexes; j++)
		if(G.arc[i][j] == 1 && !visited[j])
 			DFS(G, j);/* 对为访问的邻接顶点递归调用 */
}
 
/* 邻接矩阵的深度遍历操作 */
void DFSTraverse(MGraph G)
{
	int i;
 	for(i = 0; i < G.numVertexes; i++)
 		visited[i] = false; /* 初始所有顶点状态都是未访问过状态 */
	for(i = 0; i < G.numVertexes; i++)
 		if(!visited[i]) /* 对未访问过的顶点调用DFS,若是连通图,只会执行一次 */ 
			DFS(G, i);
}

如果使用邻接表结构,代码如下:

Boolean visited[MAXSIZE]; /* 访问标志的数组 */
 
/* 邻接表的深度优先递归算法 */
void DFS(GraphAdjList GL, int i)
{
	EdgeNode *p;
 	visited[i] = TRUE;
 	printf("%c ",GL->adjList[i].data);/* 打印顶点,也可以其它操作 */
	p = GL->adjList[i].firstedge;
	while(p)
	{
 		if(!visited[p->adjvex])
 			DFS(GL, p->adjvex);/* 对为访问的邻接顶点递归调用 */
		p = p->next;
 	}
}
 
/* 邻接表的深度遍历操作 */
void DFSTraverse(GraphAdjList GL)
{
	int i;
 	for(i = 0; i < GL->numVertexes; i++)
 		visited[i] = FALSE; /* 初始所有顶点状态都是未访问过状态 */
	for(i = 0; i < GL->numVertexes; i++)
 		if(!visited[i]) /* 对未访问过的顶点调用DFS,若是连通图,只会执行一次 */ 
			DFS(GL, i);
}

广度优先搜索(BFS)

如果说图的深度优先遍历类似于树的前序遍历,那么图的广度优先遍历就类似于树的层序遍历。我们将下面的第一幅图稍微变形,变形原则是顶点A放置在最上面一层,如下面的第二幅所示。此时,在视觉上感觉图的形状发生了变化,其实顶点和边的关系还是完全相同的。

广度优先搜索遍历图的过程为:
在这里插入图片描述
邻接矩阵结构的广度优先遍历算法:

/* 邻接矩阵的广度遍历算法 */
void BFSTraverse(MGraph G)
{
	int i, j;
	Queue Q;
	for(i = 0; i < G.numVertexes; i++)
       	visited[i] = FALSE;
    InitQueue(&Q);		/* 初始化一辅助用的队列 */
    for(i = 0; i < G.numVertexes; i++)  /* 对每一个顶点做循环 */
    {
		if (!visited[i])	/* 若是未访问过就处理 */
		{
			visited[i]=TRUE;		/* 设置当前顶点访问过 */
			printf("%c ", G.vexs[i]);/* 打印顶点,也可以其它操作 */
			EnQueue(&Q,i);		/* 将此顶点入队列 */
			while(!QueueEmpty(Q))	/* 若当前队列不为空 */
			{
				DeQueue(&Q,&i);	/* 将队对元素出队列,赋值给i */
				for(j=0;j<G.numVertexes;j++) 
				{ 
					/* 判断其它顶点若与当前顶点存在边且未访问过  */
					if(G.arc[i][j] == 1 && !visited[j]) 
					{ 
 						visited[j]=TRUE;			/* 将找到的此顶点标记为已访问 */
						printf("%c ", G.vexs[j]);	/* 打印顶点 */
						EnQueue(&Q,j);				/* 将找到的此顶点入队列  */
					} 
				} 
			}
		}
	}
}

邻接表的广度优先遍历代码如下:

/* 邻接表的广度遍历算法 */
void BFSTraverse(GraphAdjList GL)
{
	int i;
    EdgeNode *p;
	Queue Q;
	for(i = 0; i < GL->numVertexes; i++)
       	visited[i] = FALSE;
    InitQueue(&Q);
   	for(i = 0; i < GL->numVertexes; i++)
   	{
		if (!visited[i])
		{
			visited[i]=TRUE;
			printf("%c ",GL->adjList[i].data);/* 打印顶点,也可以其它操作 */
			EnQueue(&Q,i);
			while(!QueueEmpty(Q))
			{
				DeQueue(&Q,&i);
				p = GL->adjList[i].firstedge;	/* 找到当前顶点的边表链表头指针 */
				while(p)
				{
					if(!visited[p->adjvex])	/* 若此顶点未被访问 */
 					{
 						visited[p->adjvex]=TRUE;
						printf("%c ",GL->adjList[p->adjvex].data);
						EnQueue(&Q,p->adjvex);	/* 将此顶点入队列 */
					}
					p = p->next;	/* 指针指向下一个邻接点 */
				}
			}
		}
	}
}

DFS与BFS的比较

对比 DFS 和 BFS 可知,DFS 的最大优势在于它的内存开销要远远小于 BFS,因为它不需要存储每一层结点的所有孩子结点指针。根据数据和查找内容的不同, DFS 和 BFS 各有优势。例如,在一个家族树中,需要查找某个人是否仍然健在且假设这个人处于树的末端,那么 DFS 是一个更好的选择,而 BFS 可能需要花费非常长的时间达到最后一层。
DFS 算法能更快的找到目标。现在,如果要寻找一个已经过世很长时间的人,那么这个人可能更接近树的顶端。在这种情况下,BFS 查找比 DFS快。因此,每种算法的优势取决于数据和要查找的内容。
不过如果图顶点和边非常多,不能在短时间内遍历完成,遍历的目的是为了寻找合适的顶点,那么选择哪种遍历就要仔细研究了。深度优先更适合目标比较明确,已找到目标为主要目的的情况,而广度优先更适合在不断扩大遍历范围时找到相对最优解的情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值