目录
文章目录
前言
每种数据结构都有不同的用武之地,同样的图也不例外,下面我们就开始对图的研究吧。
1.图的概述
在线性表中,每个元素之间只有一个直接前驱和一个直接后继。在树形结构中,数据元素之间是层次关系,并且每一层上的数据元素可能和下一层中多个元素相关,但只能和上一层中一个元素相关。 但这仅仅都只是一对一,一对多的简单模型,如果要描述多对多的复杂关系就需要图数据结构了!
1.1图的表示
1.图G是由两个集合V和E组成,记为G = (V, E),其中V是顶点的有限非空集合,E是V中顶点偶对的有限集,这些顶点偶对称之为边(弧)
2.V是一个有限的的非空集合,我们也称之为顶点集合,其元素称之为顶点或者点。V ={v1,v2,v3,v4,v5}。我们用|V|来表示顶点的数目。
3.E是由V中的点组成的无序对构成的集合的边集,其元素称之为边,且同一点对在E中可以重复出 现多次(如果比标上边的重数的话,每一点对只要出现一次就行了)。用|E|表示边数。
4.图可以用图形表示,顶点集V中元素用平面上的一个黑点表示,边集E中元素用一条连接V中相应 点对的任意形状的线表示。现实中,点集合代表事物或对象的全体,边集代表点之间的联系或者相互作用。即图是描述事物之间联系或相互作用状态的一个概念。
1.2图的基础术语
1.2.1简单图
在图结构中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。
1.2.2无向图
在图G中,如果代表边的顶点偶对是无序的,则称G为无向图。
若关系< V,V >无方向性,则称此时的图为无向图,关系用(V,V),称之为一条边(edge)。
1.2.3有向图
设 V、V为图中的两个顶点,若关系< V,V >存在方向性,则称相应的图为有向图。V为弧尾,V为弧头。
有向图的边是有明确方向的,例如下图的就表示一个有向图,每一条边都是有明确方向的
有向无环图(Directed Acyclic Graph,简称DAG)
如果有一个有向图,从任一顶点出发无法经过若干条边回到该顶点,那么它就是一个有向无环图
在图G中,如果表示边的顶点偶对是有序的,则称G为有向图。一个图要么为无向图,要么为有向图。不存在部分有向或者部分无向的情况。混合图(Mixed Graph)表示的是边可能有向,可能无向。
1.2.4完全图
如果图中的每两个顶点之间,都存在一条边,我们就称这个图为完全图。n位点数
完全有向图:有n(n-1)条边 完全无向图:有n(n-1)/2条边
两倍关系,相当于完全无向图的每条边是有向图的边及其边的反方向边组成。
1.2.5端点、邻接点
在⼀个无向图中,若存在⼀条边(i,j),则称顶点i和顶点j为该边的两个端点。并称它们互为邻接点。
在⼀个有向图中,若存在⼀条边<i,j>,则称顶点i和顶点j为该边的两个端点。它们互为邻接点。此时,顶点i为起点。顶点j为终点。
1.2.6顶点的度、入度和出度
顶点的度:在无向图中,顶点所具有的边的数目
入度和出度:出度、入度使用于有向图。
出度(Out-degree)
一个顶点的出度为x,是指有x条边以该顶点为起点(以当前节点,出发的边的数量,就表示出度值)
入度(In-degree)
一个顶点的入度为x,是指有x条边以该顶点为终点(以当前节点为重点的边,就表示入度)
一个顶点的入度和出度的和称为该顶点的度。在一个具有e条边的图中:度之和为2e。
1.2.7子图
设有两个图G=(V,E)和G'=(V', E'),若V'是V的子集。则称G'是G的子图。
1.2.8路径(顶点序列)和路径长度(边的数目)
在一个图G=(V, E)中,从顶点i到顶点j的一条路径是一个顶点序列(i, i1, i2, ..., im, j),若此图G是无向图,则边(i, i1), (i1, i2), ...(im, j) 属于E(G);若此图是有向图,则<i, i1>, <i1, i2>, ...<im, j> 属于E(G)。
路径长度是指一条路径上经过的边的数目。若一条路径上除开始点和结束点可以相同外,其余顶点均不相同,则称此路径为简单路径。
1.2.9回路或环
如果一条路径上的开始点与结束点为同一个顶点,则称此路为回路或者为环。开始点和结束点相同的简单路径被称为简单回路或者简单环。
如果经过图中各边一次且恰好一次的环路,称之为欧拉环路,也就是其长度恰好等于图中边的总数, { C, A, B, A, D, C, D, B, C}就是一条欧拉环路。
如果是经过图中的各顶点一次且恰好一次的环路,称作哈密尔顿环路,其长度等于构成环路的边数。 {C, A, D, B, C}就是⼀条哈密尔顿环路。
1.2.10连通、连通图和连通分量
如果顶点x和y之间存在可相互抵达的路径(直接或间接的路径),则称x和y是连通的。如果图G中任意两个顶点都连通,则称G为连通图,否则称为非连通图。
无向图G中的极大连通子图称为G的连通分量。
对于连通图只有一个极大连通子图,就是它本身(是唯一的)。
非连通图有多个极大连通子图。(非连通图的极大连通子图叫做连通分量,每个分量都是一个连通图)。之所以称为极大是因为如果此时加入⼀个不在图的点集中的点都会导致它不再连通。
至于极小连通子图,首先只有连通图才有极小连通子图这个概念。就像一个四边形,四个节点四条
边,其实三条边就能连通了,所以四个节点三条边,就OK了,就是在能连通的前提下,把多余的边去掉。
1.2.11强连通图和强连通分量
在有向图G中,若从顶点i到顶点j有路径,则称从顶点i到顶点j是连通的。
若图G中的任意两个顶点i和顶点j都连通,即从顶点i到顶点j和从顶点j到顶点i都存在路径,则称图G是强连通图。有向图G中的极大强连通子图称为G的强连通分量。显然,强连通图只有一个强连通分量,即自身,非强连通图有多个强连通分量。
1.2.12稠密图、稀疏图
当一个图接近完全图的时候,称之为稠密图;相反,当一个图含有较少的边数,则称之为稀疏图。
一般对于这个边的个数,说法比较多,通常认为边小于nlogn(n是顶点的个数)的图称之为稀疏图,反之称为稠密图。
1.2.13权和网
图中的每一条边都可以附有一个对应的数,这种与边相关的数称为权。权可以表示从一个顶点到另一个顶点的距离或者花费的代价。边上带有权的图称为带权图,也称之为网。也叫有权图(Weighted Graph)权值不仅仅可以是整数,还可以是小数,负数。根据情况而定,甚至还可以是自定义对象。
1.2.14连通图的生成树
所谓连通图的生成树是一个极小的连通子图,它含有图中全部的n个结点,但是只有构成树的n-1条边。
二.图的存储
图在内存中存储方式有很多种,最经典的包括邻接矩阵、邻接表、逆邻接表和十字链表。
图的存储结构相比较线性表与树来说就复杂很多。对于线性表来说,是一对一的关系,所以用数组或者链表均可简单存放。 树结构是一对多的关系,所以我们要将数组和链表的特性结合在一起才能更好的存放。
那么图,是多对多的情况,图上的任何一个顶点都可以被看作是第一个顶点,任一顶点的邻接点之间也不存在次序关系。因为任意两个顶点之间都可能存在联系,因此无法以数据元素在内存中的物理位置来表示元素之间的关系(内存物理位置是线性的,图的元素关系是平面的)。如果用多重链表来描述倒是可以做到,但是纯粹多多重链表导致的浪费是无法想像的(如果各个顶点的度数相差太大,就会造成巨大的浪费)。
1.邻接矩阵
图的邻接矩阵是用两个数组来表示,一个一维数组存储图中的顶点信息,一个二维数组(我们将这个数组称之为邻接矩阵)存储图中的边的信息。
1.1无向图邻接矩阵
我们可以设置两个数组,顶点数组为vertex[4]={V0,V1,V2,V3},边数组arc[4] [4]为对称矩阵(0表示不存在顶点间的边, 1表示顶点间存在边)。
1.1.1对称矩阵
所谓对称矩阵就是n阶矩阵的元素满足a[i] [j] = a[j] [i] (0<=i,j<=n)。即从矩阵的左上角到右下角的主对角线为轴,右上角的元与左下角相对应的元全都是相等的。
有了这个二维数组组成的对称矩阵,我们就可以很容易地知道图中的信息:判定任意两顶点是否有边无边;可以轻松知道某个顶点的度,其实就是这个顶点Vi在邻接矩阵中第i行(或第i列)的元素之和;求顶点Vi的所有邻接点就是将矩阵中第i行元素扫描⼀遍, arc[i] [j]为1就是邻接点喽。
1.2有向图邻接矩阵
无向图的边构成了一个对称矩阵,貌似浪费了一半的空间,那如果是有向图来存放,会不会把资源
都利用得很好呢?
可见顶点数组vertex[4]={V0,V1,V2,V3},弧数组arc[4] [4]也是一个矩阵,但因为是有向图,所以这个矩阵并不对称,例如由V0到V3有弧,得到arc[0] [3]=1,而V3到V0没有弧,因此arc[3] [0]=0。
另外有向图是有讲究的,要考虑入度和出度,顶点V1的入度为1,正好是第V1列的各数之和,顶点V1的出度为2,正好是第V1行的各数之和。
1.3带权图的邻接矩阵
带权图中的每一条边上带有权值,邻接矩阵中的值则为权值,当两个顶点之间没有弧时,则用无穷大表示。
这里“∞”表示⼀个计算机允许的、大于所有边上权值的值。这个时候我们会发现一个问题,就是空间浪费问题。尤其是面对边数相对比较少的稀疏图来说,这种结构无疑是存在对存储空间的极大浪费。因此我们可以考虑另外一种存储结构方式,例如把数组与链表结合在⼀起来存储,这种方式在图结构也适用,我们称为邻接表(Adjacency List)。
1.4代码实现
#include<stdio.h>
#define MaxVertices 100
#define MaxWeight 32768//带权图 点不邻接的时候 无穷大(16位int范围-32768~+32767)
typedef struct{
int Vertices[MaxVertices];//顶点的数组信息
int Edge[MaxVertices][MaxVertices];//边的个数
int numV;//顶点的个数
int numE;//边的个数
}AdjMatrix;
void CreateGraph(AdjMatrix* G)
{
int n,e;
int vi,vj,w;
//先输入图的顶点个数与边的个数
scanf("%d,%d",&n,&e);
G->numV=n;
G->numE=e;
//初始化
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(i==j)
G->Edge[i][j]=0;
else
G->Edge[i][j]=MaxWeight;
}
}
//把顶点放在数组中去
for(int i=0;i<G->numV;i++)
scanf("%d",&G->Vertices[i]);
//输入边的信息,如果是带权图输入权值
for(int i=0;i<G->numE;i++){
scanf("%d%d%d",&vi,&vj,&w);
G->Edge[vi][vj]=w;
//无向图时加上这行
G->Edge[vj][vi]=w;
}
}
2.邻接表
邻接表的处理方法是这样:图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过数组可以较容易地读取顶点信息,更加方便。图中每个顶点Vi的所有邻接点构成一个线性表,由于邻接点的个数不确定,所以我们选择用单链表来存储。
2.1无向图邻接表
2.2有向图邻接表
若是有向图,邻接表结构也是类似的,我们先来看下把顶点当弧尾建立的邻接表,这样很容易就可以得到每个顶点的出度:
但也有时为了便于确定顶点的入度或以顶点为弧头的弧,我们可以建立一个有向图的逆邻接表:
2.3 带权网络的邻接表
邻接表固然优秀,但也有不足,例如对有向图的处理上,有时候需要再建立一个逆邻接表~这个时候,我们可以想一下,我们是否可以把邻接表和逆邻接表结合起来呢?当然可以,这个就是十字链表。
2.4代码实现
#include<stdio.h>
//边表信息
typedef struct EdgeNode
{
int adjvex//邻接表的节点
struct EdgeNode* next;
int weight;//如果是带权图要用
}EdgeNode;
//顶点信息
typedef struct VertexNode
{
char data;
struct EdgeNode* firstedge;
}VertexNode;
//邻接表的结构
typedef struct GraphadjList
{
VertexNode adjList adjList[100];//顶点表的节点数组
int numV,numE;//顶点个数和边的个数
}
//无向图
void CreatALGraph(GraphadjList* G)
{
int vi,vj;
EdgeNode* e;
//先读入顶点信息和边信息
scanf("%d%d",&G->numV,&G->numE);
//初始化节点信息
for(int i=0;i<G->numV;i++)
{
scanf("%c",&G->adjList[i].data);
getchar();
G->adjList[i].firstedge=NULL;
}
//建立边表
for(int i=0;i<G->numE;i++)
{
//输入每条边的端点
scanf("%d%d",&vi,&vj);
e=(EdgeNode*)malloc(sizeof(EdgeNode));
e->adjvex=vj;
e->next=G->adjList[vi].firstedge;
G->adjList[vi].firstedge=e;
e = (EdgeNode*)malloc(sizeof(EdgeNode));
e->adjvex = vi;
e->next = G->adjList[vj].firstedge;
G->adjList[vj].firstedge = e;
}
}
3.十字链表的表示
十字链表的好处就是因为把邻接表和逆邻接表整合在了一起,这样既容易找到以Vi为尾的弧,也容易找到以Vi为头的弧,因而容易求得顶点的出度和入度。十字链表除了结构复杂一点外,其实创建图算法的时间复杂度是和邻接表相同的,因此,在有向图的应用中,十字链表也是非常好的数据结构模型。这个时候,还有一个问题,如果使用邻接表存储结构,但是对边的操作比较频繁,怎么办?如果我们在无向图的应用中,关注的重点是顶点的话,那么邻接表是不错的选择,但如果我们更关注的是边的操作,比如对已经访问过的边做标记,或者删除某一条边等操作,邻接表的确显不那么方便了。
#include<stdio.h>
#include<stdlib.h>
//边集的数据
typedef struct ArcBox
{
int tailvex, headvex;//弧尾、弧头所在的位置
struct ArcBox* hlink, tlink;//弧尾相同、弧头相同的下一个弧
int weight;
}ArcBox;
//顶点的数据
typedef struct VexNode
{
int data;//真实的数据
ArcBox* firstin, * firstout;//出度指针 入度指针
}VexNode;
typedef struct{
VexNode xlist[20];
int numV, numE;
}OLGraph;
void CreateDG(OLGraph* G)
{
//输入顶点数和边数
scanf("%d%d", &(G->numV), &(G->numE));
for (int i = 0; i < G->numV; i++)
{
scanf("%d", &(G->xlist[i].data));
G->xlist[i].firstin = NULL;
G->xlist[i].firstout = NULL;
}
//构建十字链表
for (int i = 0; i < G->numE; i++)
{
int v1, v2;
scanf("%d%d", &v1, &v2);
//查找相对应的下标
ArcBox* p = (ArcBox*)malloc(sizeof(ArcBox));
p->tailvex = v1;
p->headvex = v2;
//采用头插法插入新的p结点
p->hlink = G->xlist[v2].firstin;
p->tlink = G->xlist[v1].firstout;
G->xlist[v2].firstin = G->xlist[v1].firstout = p;
}
}
4.邻接多重表
邻接表对边的操作显然很不方便,因此,我们可以仿照十字链表的方式,对边表结构进行改装,重新定义的边表结构如下:
其中iVex和jVex是与某条边依附的两个顶点在顶点表中的下标。 iLink指向依附顶点iVex的下一条边, jLink指向依附顶点jVex的下一条边。
#include<stdio.h>
//邻接多重表
//边表集合
typedef struct node
{
int ivex, jvex;
struct node* vi, *vj;
}ArcNode;
//结点
typedef struct
{
char vertex;
ArcNode* firstEdge;
}VNode;
typedef struct
{
VNode Dvex[50];
int numV, numE;
}Graph;
void creat(Graph* G)
{
//先输入顶点数和边数
scanf("%d%d", &(G->numV), &(G->numE));
ArcNode* new_node;
for (int i = 0; i < G->numV; i++)
{
scanf("%d", &G->Dvex[i].vertex);
G->Dvex[i].firstEdge = NULL;
}
for (int i = 0; i < G->numE; i++)
{
//输入边对应的下标
int vi, vj;
new_node = (ArcNode*)malloc(sizeof(ArcNode));
new_node->ivex = vi;
new_node->jvex = vj;
new_node->vi = G->Dvex[vi].firstEdge;
G->Dvex[vi].firstEdge = new_node;
new_node->vj = G->Dvex[vj].firstEdge;
G->Dvex[vj].firstEdge = new_node;
}
}
5.边集数组
边集数组是由两个一维数组构成,一个是存储顶点的信息,另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成。
三.图的遍历
1.深度优先遍历
深度优化遍历( Depth First Search ),也有称为深度优化搜索 ,简称为 DFS。DFS,先序遍历、中序遍历和后序遍历都属于深度优先遍历的方式,因为这些遍历方式本质上都归结于栈。为了讲清楚DFS,我们先来看两个概念。
1.1右手原则
在没有碰到重复顶点的情况下,分叉路口始终是向右手边走,每路过一个顶点就做一个记号
1.2左手原则
在没有碰到重复顶点的情况下,分叉路口始终是向左手边走,每路过一个顶点就做一个记号。我们这边都以右手原则来进行深度优先搜索。话不多说,直接开搜。
原则上,我们可以从图中的任意一个顶点开始,进行深度优先搜索,假设我们从A顶点开始,遍历过程如下。
第一步:从顶点A开始,将顶点A标记为以访问结点
第二步:根据约定的右手原则,访问顶点B,并将B标记为已访问顶点
第三步:右手原则,访问C
第四步:右手原则,访问顶点D
第五步:右手原则,访问E
第六步:右手原则,访问F
第七步:右手原则,应该先访问顶点F的邻接顶点A,但发现A已被访问,则访问除A之外的最右侧顶点G。
第八步:右手原则,先访问顶点B,顶点B已被访问;再访问顶点D,顶点D已经被访问;最后访问顶点H。
第九步:发现顶点H的邻接顶点均已被访问,则退回到顶点G;
第十步:顶点G的邻接顶点均已被访问,则退回到顶点F;
第十一步:顶点F的邻接顶点已被访问,则退回到顶点E;
第十二步:顶点E的邻接顶点均已被访问,则退回到顶点D;
第十三步:顶点D的邻接顶点I尚未被访问,则访问顶点I;
第十四步:顶点I的邻接顶点均已被访问,则退回到顶点D;
顶点D的邻接顶点均已被访问,退回到顶点C;顶点C的邻接顶点均已被访问,则退回到顶点B;顶点B的邻接顶点均已被访问,则退回到顶点A,顶点A为起始顶点,深度优先搜索结束。
上面所说的这些过程不正就是递归的过程吗?
为了更清楚的理解图的深度优先搜索和⼆叉树的前序遍历、中序遍历、后序遍历均属于⼀类方法,我们对最终的遍历结果图做一定的位置调整:
细心的一定发现,这就是我们的前序遍历过程呀!相信看到这里的一定对深度优先搜索豁然开朗了。为了更加清楚图的深度优先搜索,我们将上面的过程总结为以下三个步骤:
1、首先选定一个未被访问过的顶点V作为起始顶点(或者访问指定的起始顶点V),并将其标记为已访问过;
2、然后搜索与顶点V邻接的所有顶点,判断这些顶点是否被访问过,如果有未被访问过的顶点W;再选取与顶点W邻接的未被访问过的一个顶点并进行访问,依次重复进行。当一个顶点的所有的邻接顶点都被访问过时,则依次回退到最近被访问的顶点。若该顶点还有其他邻接顶点未被访问,则从这些未被访问的顶点中取出一个并重复上述过程,直到与起始顶点V相邻接的所有顶点被访问过为止。
3、若此时图中依然有顶点未被访问,则再选取其中一个顶点作为起始顶点并进行遍历,转(2)步骤。反之,则遍历结束。
/*
创建无权无向图并深度优先搜索--基于邻接矩阵存图
*/
#include <stdio.h>
#include <stdlib.h>
#define MAXN 100
typedef struct ArcCell {
char vexnum[MAXN]; //顶点
int arcnum[MAXN][MAXN]; //弧
int n, e; //顶点数, 弧数
}Graph;
int Visit[MAXN]; //定义Visit来判断顶点是否被访问,并初始化(全局变量默认为0)
void CreateGraph(Graph* G) { //创建图 ,此处注意&G
int s, t;
scanf("%d %d", &G->n, &G->e);
getchar();//读掉回车
for (int i = 0; i < G->n; i++) {
scanf("%c", &G->vexnum[i]);
}
for (int i = 0; i < G->n; i++) { //初始化数据
for (int j = 0; j < G->n; j++) {
G->arcnum[i][j] = 0;
}
}
for (int i = 0; i < G->e; i++) { //创建图的邻接矩阵
scanf("%d %d", &s, &t);
G->arcnum[s][t] = 1;
G->arcnum[t][s] = 1;
}
}
//开始搜索 找一个开始结点
/*
开始结点是人定义的
Graph G 需要遍历哪一个图
int i 遍历的起始结点从哪里开始
标记结点
1、遍历未被标记的结点
2、如果遍历到某一个邻接点是已被标记 我要继续找和我相邻的另外一个结点
*/
void DFSTraverse(Graph G, int i)//找邻接点
{
printf("%c", G.vexnum[i]);
for (int j = 0; j < G.n; j++)
{
if (G.arcnum[i][j] && !Visit[j])
{
Visit[j] = 1;
DFSTraverse(G, j);
}
}
}
void DFS(Graph G)//遍历
{
for (int i = 0; i < G.n; i++)//对整个图的结点进行深搜
{
if (!Visit[i])
{
Visit[i] = 1;
DFSTraverse(G, i);
}
}
}
int main()
{
Graph G;
CreateGraph(&G);
DFS(G);
return 0;
}
/*
测试用例:
9
15
ABCDEFGHI
0 1
0 5
1 6
5 6
2 1
1 8
2 8
6 7
2 3
3 8
3 7
3 4
4 7
4 5
3 6
*/
2.广度优先遍历
广度优先遍历(Breadth First Search),又称为广度优先搜索,简称BFS。树的层序遍历方式便是一种广度优先搜索方式。为了清晰地理解广度优先搜索,我们同样以深度优先搜索的例子一起走一遍,这不过,我们对图中顶点的位置做了调整,这样看起来更清楚。
假定从顶点A开始进行广度优先搜索,则遍历的顺序为:
第⼀步:访问顶点A
第二步:访问顶点A的所有未被访问的邻接顶点,顶点B和顶点F
第三步:访问顶点B和顶点F的所有未被访问的邻接顶点,顶点C、I、G、E;
第四步:访问顶点C、I、G、E 的所有邻接顶点中未被访问的顶点,顶点D和顶点H。
这个时候我们发现,广度优先遍历的步骤好少呀!
这里只是给你们展示了一层一层遍历的过程,并没有展示每一层具体如何被访问,这就要考虑到BFS 的实现了。那么要实现对图的广度优先遍历,显然和树的层序遍历⼀样,采用队列来实现。
#include <stdio.h>
#include <stdlib.h>
#define MAX_VERTEX 100
#define inf 65535 //表示两点之间没有边相连
typedef enum {false, true }bool;//c语言没有bool,要自己声明
int visit[100]; //标记顶点是否被访问
/**带权无向图的邻接链表的建立--基于邻接表**/
//边表结点数据结构
typedef struct EdgeNode
{
int adjvex; //存储该顶点对应的下标
int weight;
struct EdgeNode *next; //指向下一个邻接点
}EdgeNode;
//顶点表结点数据结构
typedef struct VertexNode
{
char Vertex; //存储顶点信息
EdgeNode *FistEdge; //边表头指针
}VertexNode;
//邻接链表图的数据结构
typedef struct
{
VertexNode adjList[100];
int VertexNumber,EdgeNumber; //顶点数和边数
}GraphAdjList;
/**无向图邻接链表的建立**/
void Create_no_direction_LinkList_Graph(GraphAdjList *G)
{
int i,j,w,k;
// printf("请输入无向图邻接链表的顶点数和边数:\n");
scanf("%d %d",&G->VertexNumber,&G->EdgeNumber);
//输入顶点信息,建立顶点表
//printf("顶点表的建立:输入顶点信息,如ABCDEF.....\n");
//char ch;
//while( ( ch = getchar() != '\n' ) );
getchar();
for(i=0;i<G->VertexNumber;i++)
{
scanf("%c",&G->adjList[i].Vertex);
G->adjList[i].FistEdge = NULL;
}
// printf("边表的建立:输入边(vi,vj)的顶点下标,权值统一为1,如:0 1 1(权值)\n");
for(k=0;k<G->EdgeNumber;k++)
{
scanf("%d %d %d",&i,&j,&w);
EdgeNode *e;
e = (EdgeNode*)malloc(sizeof(EdgeNode));
e->weight = w;
e->adjvex = j;
e->next = G->adjList[i].FistEdge; //头插法将下标为j的顶点插入与之相连的下标为i的结点链表中
G->adjList[i].FistEdge = e;
//无向图,因此是对称的,同样的操作将下标为i的顶点插入与之相连的下标为j的结点的链表中
e = (EdgeNode*)malloc(sizeof(EdgeNode));
e->weight = w;
e->adjvex = i;
e->next = G->adjList[j].FistEdge;
G->adjList[j].FistEdge = e;
}
// 打印检查
/* printf("---------------------构造出来的无向图邻接链表的边信息如下---------------------\n");
for(i=0;i<G->VertexNumber;i++)
{
EdgeNode *p;
p = G->adjList[i].FistEdge;
printf("%d\t",i);
while(p != NULL)
{
printf("%d ",p->adjvex);
p = p->next;
}
printf("\n");
}*/
}
/**BFS会用到队列这个数据结构**/
/**顺序表实现循环队列**/
typedef struct
{
char data[MAX_VERTEX];
int front; //头指针
int rear; //尾指针,队列非空则指向队尾最后一个元素后一个位置
}SqQueue;
//队列初始化
void InitQueue(SqQueue *Q)
{
Q->front = 0;
Q->rear = 0;
}
//入队
bool EnQueue(SqQueue *Q, char e)
{
//判断队列是否满
if( ( Q->rear+1 ) % MAX_VERTEX == Q->front )
return false;
Q->data[Q->rear]=e;
Q->rear = (Q->rear+1)%MAX_VERTEX;
return true;
}
//出队---删除队首元素,并赋给e
char* DeQueue(SqQueue *Q, char *e)
{
//判断队列是否为空
if( Q->front == Q->rear )
return NULL;
*e = Q->data[Q->front];
Q->front = (Q->front+1)%MAX_VERTEX;
return e;
}
//队列判空
bool isEmptyQueue(SqQueue *Q)
{
return Q->front == Q->rear?true:false;
}
//无向图邻接链表BFS
void BFS_Travel(GraphAdjList G)
{
int i,j,mark;
char data;
SqQueue Q;
EdgeNode *p;
//初始化visit数组
for(i=0;i<G.VertexNumber;i++)
visit[i] = false;
//初始化队列
InitQueue(&Q);
//对每个顶点进行BFS
//printf("此无向图邻接链表BFS结果为:\n");
for(i=0;i<G.VertexNumber;i++)
{
if(!visit[i])
{
visit[i] = true;
EnQueue(&Q,G.adjList[i].Vertex);
while(!isEmptyQueue(&Q))
{
DeQueue(&Q,&data);
printf("%c ",data);
//根据删除顶点实时更新追踪该顶点下标,以便正确找到与之相连的其他顶点
for(j=0;j<G.VertexNumber;j++)
if(G.adjList[j].Vertex==data)
mark = j;
p = G.adjList[mark].FistEdge;
//找寻与此顶点相连且没访问过的其他顶点
while(p)
{
if(!visit[p->adjvex])
{
visit[p->adjvex] = true;
EnQueue(&Q,G.adjList[p->adjvex].Vertex);
}
p = p->next;
}
}
}
}
}
int main()
{
GraphAdjList G;
Create_no_direction_LinkList_Graph(&G);
BFS_Travel(G);
return 0;
}
/*
测试用例:
9
15
ABCDEFGHI
0 1 1
0 5 1
1 6 1
5 6 1
2 1 1
1 8 1
2 8 1
6 7 1
2 3 1
3 8 1
3 7 1
3 4 1
4 7 1
4 5 1
3 6 1
*/
3.BFS的缺点适用情况
BFS的一般步骤:
1、将初始点(一个或多个)加入队列
2、从队列头取出点,判断初始点的周边点,将符合条件的点加入队列尾部
3、重复2操作,直至队列为空。(一般每个点只加入队列一次)
BFS的缺点:
在搜索过程中,BFS必须要保存搜索过程中的状态,用于判重。
BFS的适用情况:
一般在树或者图中,用BFS的可能性比较大。
BFS是用来搜索最短径路的解是比较合适的,比如求最少步数的解,最少交换次数的解,因为BFS搜索过程中遇到的解一定是离根最近的,所以遇到一个解,一定就是最优解,此时搜索算法可以终止。这个时候不适宜使用DFS,因为DFS搜索到的解不一定是离根最近的,只有全局搜索完毕,才能从所有解中找出离根的最近的解。(当然这个DFS的不足,可以使用迭代加深搜索ID-DFS去弥补)
4.DFS的缺点以及适用情况
DFS的优点
空间优劣上,DFS是有优势的,DFS不需要保存搜索过程中的状态,而BFS在搜索过程中需要保存搜索过的状态,而且一般情况需要一个队列来记录。
因为根据栈的思想,DFS在搜索一个点以后,会弹出该点,就不需要保存已经搜索过的点。而BFS是必定保存搜索过的点的。
DFS的缺点
因为DFS含有栈的思想,因此经常用递归解决问题,但是如果不给递归限制深度,往往会超过时间与空间复杂度的。
二维数组的题目,N小于20的,适用DFS。而一般 N<= 200,N<=1000这种,一定不可能用DFS去
做。而且并不只是整个题目不能用DFS,其中的每一步也不能使用DFS。上面的N指的应该是递归深度。
DFS的适用情况
DFS适合搜索全部的解,因为要搜索全部的解,那么BFS搜索过程中,遇到离根最近的解,并没有什么用,所以搜素全部解的问题,DFS显然更加合适。当求解目标,必须要走到最深(例如树,走到叶子节点)才能得到一个解,这种情况适合用深搜。
总结
图的遍历方式包括深度优先搜索(DFS)和广度优先搜索(BFS),其中 DFS 使用递归或栈进行实现,而BFS则采用队列进行实现。对比树的四种遍历方式,前序遍历、中序遍历和后序遍历均类似于DFS,而层序遍历类似于 BFS,前中后序也均可采用栈的方式进行实现,层序遍历可以采用队列的方式进行实现。这样看来,知识的融会贯通多么重要,总体而言,掌握下面的两条链,你便可以解决好多问题。
DFS → 前中后序 → 栈 → 线性表
BFS → 层序遍历 → 队列 → 链表