图的存储结构与遍历

一、定义

        (graph)是一种比线性表、树更为复杂的数据结构。在线性表中,数据元素之间呈线性关系,即每个元素只有一个直接前驱和一个直接后继。在树型结构中,数据元素之间有明显的的层次关系,即每个结点只有一个直接前驱,但可有多个直接后继,而在图结构中,每个结点即可有多个直接前驱,也可有多个直接后继,因此,树结构是图结构的一种特殊情形。当一个树结构中允许同一结点出现在不同分支上时,该树结构实际上就是一个图结构。图的最早应用可以追溯到十八世纪数学家欧拉(EULer)利用图解决了著名的哥尼斯堡桥的问题,为图在现代科学技术领域的应用奠定了基础。

        (1)基本概念

          V是顶点的非空有穷集合,在图结构中,不允许没有顶点;

       图分为有向图和无向图,无向图的边(A,B)也可以写成(B,A),有向图的边(也称弧)表示为<A,B>,但不可写成<B,A>,弧有弧头弧尾之分;

          不考虑结点的自返圈,即结点到其自身的边,且同一条边不重复出现,这样的图称为简单图,下面要讨论的均为简单图;

          有很少条边或弧的图称为稀疏图,反之称为稠密图(相对而言);

        (2)完全图

         在一个有n个顶点的图中,若每个顶点到其他(n-1)个顶点都连有一条边,则图称为完全图。无向完全图边数为n*(n-1)/2,有向完全图边数为n*(n-1)。

        (3)邻接点、相关边

         对于无向图G=(V,E),若(V1,V2)∈E,则称V1和V2互为邻接点(adjacent),即V1和V2相邻接,而边(V1,V2)则是与结点V1和V2“相关联的边”。在有向图G=(V,A)中,若<V1,V2>∈A,则称结点V1邻接到结点V2,结点V2邻接于V1,而边〈V1,V2〉是与结点V1,V2相关联的。

        (4)顶点的度、入度、出度

        顶点的度(degree)是和V相关联的边的数目,计为TD(V)。在有向图G=(V,A)中,如果弧<V1,V2>∈A,则以V1为头的弧的数目称为V1的入度(indegree),记为ID(V1);以V1为尾的弧的数目称为V1的出度(outdegree),记为OD(V1);顶点的度为TD(V1)=ID(V1)+OD(V1)。

        (5)路径、回路

       无向图G=(V,E)中,从顶点V到顶点V’的路径(Path)是一个顶点序列(V=Vi0,Vi1,Vi2,……,Vim=V’),其中(Vij-1,Vij)∈E,1<=j<=m.如果是有向图,则路径也是有向的,顶点序列满足(Vij-1,Vij)∈E,1<=j<=n,路径长度是路径上的边或弧的数目。

       第一个顶点和最后一个顶点相同的路径称为回路或环(cycle),序列中顶点不重复的称为简单路径。除了第一个顶点和最后一个顶点外,其余顶点不重复的回路,称为简单回路或简单环。

        (6)子图

       假设有两个图G={V,{E}}和G’={V’,{E’}},如果V’包含于V,E’包含于E,则称G’是G的子图。

        (7)连通和强连通

        在无向图G中,如果从顶点V到顶点V’有路径,则称V和V’是连通的。如果对于图G中任意两个顶点Vi,Vj ∈V都是连通的,则称为G是连通图(connected graph).连通分量指的是无向图中极大连通子图(子图+子图要连通+连通子图含最大顶点数+包含依附于这些顶点的所有边)。

        在有向图G中,如果对于每一对Vi,Vj∈V,Vi<>Vj,从Vi到Vj和从Vj到Vi都存在路径,则称G是强连通图。有向图中极大强连通子图称作为有向图G的强连通分量。

        (8)生成树、有向树

        一个连通图的生成树,它含有图中全部顶点,但只有足以构成一棵树的n-1条边。如果边多于n条会形成环,少于n-1个顶点无法构成连通图;

        如果一个有向图恰有一个顶点入度为0,其余顶点入度均为1(只有一个根顶点,其余顶点双亲只有一个),则是一棵有向树;

        (9)权、网

       在图的边或弧上,有时标有与它们相关的数,这种与图的边或弧相关的数称作权(Weight)。这些权可以表示从一个顶点到另一个顶点的距离或代价。这种带权的图常称作网(network)。

二、图的存储结构

       如果采用类似二叉树结构的多重链表形式实现图将会带来许多问题,如果统一按照最大度的顶点来设计会造成很多浪费,毕竟各个顶点的度之间相差很大。于是引入下列几种常见的表示方法:

        (1)邻接矩阵

          图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(邻接矩阵)存储图中的边或弧的信息。设图G有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:
《图的存储结构与遍历》

         看一个实例,下图左就是一个无向图。

《图的存储结构与遍历》

       从上面可以看出,无向图的边数组是一个对称矩阵。所谓对称矩阵就是n阶矩阵的元满足aij = aji。即从矩阵的左上角到右下角的主对角线为轴,右上角的元和左下角相对应的元全都是相等的。

    从这个矩阵中,很容易知道图中的信息。
 
(1)要判断任意两顶点是否有边无边就很容易了;
(2)要知道某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行或(第i列)的元素之和;
(3)求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]为1就是邻接点; 

   而有向图讲究入度和出度,顶点vi的入度为1,正好是第i列各数之和。顶点vi的出度为2,即第i行的各数之和。若图G是网图,有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:
《图的存储结构与遍历》

   这里的wij表示(vi,vj)上的权值。无穷大表示一个计算机允许的、大于所有边上权值的值,也就是一个不可能的极限值。为什么不用0呢?因为权值在个别情况下有可能是0,甚至可能是负值;下面左图就是一个有向网图,右图就是它的邻接矩阵。
《图的存储结构与遍历》

   那么邻接矩阵是如何实现图的创建的呢?代码如下。

typedef char VertexType;        //顶点类型应由用户定义
typedef int EdgeType;           //边上的权值类型应由用户定义
#define MAXVEX  100             //最大顶点数,应由用户定义
#define INFINITY    65535       //用65535来代表无穷大
#define DEBUG

typedef struct
{
	VertexType vexs[MAXVEX];        //顶点表
	EdgeType   arc[MAXVEX][MAXVEX]; //邻接矩阵,可看作边
	int numVertexes, numEdges;      //图中当前的顶点数和边数
}Graph;
//定位函数
int locates(Graph *g, char ch)
{
	int i = 0;
	for(i = 0; i < g->numVertexes; i++)
	{
		if(g->vexs[i] == ch)
			break;
	}
	if(i >= g->numVertexes)
		return -1;	
	return i;
}
//建立一个无向网图的邻接矩阵表示
void CreateGraph(Graph *g)
{
	int i, j, k, w;char p,q;
	printf("输入顶点数和边数:\n");
	scanf("%d,%d", &(g->numVertexes), &(g->numEdges));
	printf("输入顶点信息标识:\n");
	for(i = 0; i < g->numVertexes; i++){//读入顶点信息,建立顶点表
		g->vexs[i] = getchar();
		while(g->vexs[i] == '\n')
			g->vexs[i] = getchar();
	}
	//邻接矩阵初始化
	for(i = 0; i < g->numEdges; i++)
		for(j = 0; j < g->numEdges; j++)
			g->arc[i][j] = INFINITY; 
	//读入边,建立邻接矩阵
	printf("输入边(vi,vj)上的下标i,下标j和权值:\n"); 
	for(k = 0; k < g->numEdges; k++)
	{	
		scanf("%c,%c,%d",&p,&q,&w);
		int m = locates(g,p);
		int n = locates(g,q);
		if(-1==m || -1==n){
			cout<<"输入边不存在!"<<endl;
			return;
		}	
		g->arc[m][n] = w;
		g->arc[n][m] = g->arc[m][n];  //因为是无向图,矩阵对称
	}
}
//打印图邻接矩阵
void printGraph(Graph g)
{
	int i, j;
	for(i = 0; i<g.numVertexes; i++)
	{
		printf("%c \n",g.vexs[i]);
		for(j = 0; j<g.numVertexes; j++)
		{
			printf("%d  ",g.arc[i][j]);
		}
		printf("\n");
	}
}

      从代码中可以得到,n个顶点和e条边的无向网图的创建,时间复杂度为O(n + n2 + e),其中对邻接矩阵Grc的初始化耗费了O(n2)的时间。 

       (2)邻接表

       邻接矩阵是不错的一种图存储结构,但是,对于边数相对顶点较少的图,这种结构存在对存储空间的极大浪费。因此,找到一种数组与链表相结合的存储方法称为邻接表。
    邻接表的处理方法是这样的:
    1. 图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过,数组可以较容易的读取顶点的信息,更加方便。
    2. 图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以,用单链表存储,无向图称为顶点vi的边表,有向图则称为顶点vi作为弧尾的出边表。 
    例如,下图就是一个无向图的邻接表的结构。

《图的存储结构与遍历》

    从图中可以看出,顶点表的各个结点由data和firstedge两个域表示,data是数据域,存储顶点的信息,firstedge是指针域,指向边表的第一个结点,即此顶点的第一个邻接点。边表结点由adjvex和next两个域组成。adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针。
    对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可。如下图所示。
《图的存储结构与遍历》

#define MAXVEX 1000         //最大顶点数
typedef char VertexType;        //顶点类型应由用户定义
typedef int EdgeType;           //边上的权值类型应由用户定义

typedef struct EdgeNode         //边表结点
{
	int adjvex;             //邻接点域,存储该顶点对应的下标
	EdgeType weigth;        //用于存储权值,对于非网图可以不需要
	struct EdgeNode *next;      //链域,指向下一个邻接点
}EdgeNode;

typedef struct VertexNode       //顶点表结构
{
	VertexType data;        //顶点域,存储顶点信息
	EdgeNode *firstedge;        //边表头指针
}VertexNode, AdjList[MAXVEX];

typedef struct
{
	AdjList adjList;
	int numVertexes, numEdges;  //图中当前顶点数和边数
}GraphList;

int Locate(GraphList *g, char ch)
{
	int i;
	for(i = 0; i < MAXVEX; i++)
	{
		if(ch == g->adjList[i].data)
		{
			break;
		}
	}
	if(i >= MAXVEX)
	{
		fprintf(stderr,"there is no vertex.\n");
		return -1;
	}
	return i;
}

//建立图的邻接表结构
void CreateGraph(GraphList *g)
{
	int i, j, k;
	EdgeNode *e;
	EdgeNode *f;
	printf("输入顶点数和边数:\n");
	scanf("%d,%d", &g->numVertexes, &g->numEdges);
	//输入顶点信息
	for(i = 0; i < g->numVertexes; i++)
	{
		printf("请输入顶点%d:\n", i);
		g->adjList[i].data = getchar();          
		g->adjList[i].firstedge = NULL;          //将边表置为空表
		while(g->adjList[i].data == '\n')
		{
			g->adjList[i].data = getchar();
		}
	}
	//建立边表
	for(k = 0; k < g->numEdges; k++)
	{
		printf("输入边(vi,vj)上的顶点序号:\n");
		char p, q;
		p = getchar();
		while(p == '\n')
		{
			p = getchar();
		}
		q = getchar();
		while(q == '\n')
		{
			q = getchar();
		}
		int m, n;
		m = Locate(g, p);
		n = Locate(g, q);
		if(m == -1 || n == -1)
		{
			return;
		}

		//向内存申请空间,生成边表结点
		e = (EdgeNode *)malloc(sizeof(EdgeNode));
		if(e == NULL)
		{
			fprintf(stderr, "malloc() error.\n");
			return;
		}
		//邻接序号为j
		e->adjvex = n;
		//头插法在头结点处插入e,标示m-n有边
		e->next = g->adjList[m].firstedge;
		g->adjList[m].firstedge = e;
		//对应的n-m也有边
		f = (EdgeNode *)malloc(sizeof(EdgeNode));
		if(f == NULL)
		{
			fprintf(stderr, "malloc() error.\n");
			return;
		}
		f->adjvex = m;
		f->next = g->adjList[n].firstedge;
		g->adjList[n].firstedge = f;
	}
}

void printGraph(GraphList *g)
{
	int i = 0;

	while(g->adjList[i].firstedge != NULL && i < MAXVEX)
	{
		printf("顶点:%c  ", g->adjList[i].data);
		EdgeNode *e = NULL;
		e = g->adjList[i].firstedge;
		while(e != NULL)
		{
			printf("%d  ", e->adjvex);
			e = e->next;
		}
		i++;
		printf("\n");
	}
}

对于无向图,一条边对应都是两个顶点,所以,在循环中,插入是同时进行的。m-n有一条边,n-m也有一条边。且由于是无向图,不用对权值域做处理,事实上对于非网图都不需要对权值做处理。本算法的时间复杂度,对于n个顶点e条边来说,很容易得出是O(n+e)。

            (3)十字链表(有向图存储结构优化)

       对于有向图来说,邻接表是有缺陷的。关心了出度问题,想了解入度就必须要遍历整个图才知道,反之,逆邻接表解决了入度却不了解出度情况。下面介绍的这种有向图的存储方法:十字链表,就是把邻接表和逆邻接表结合起来的。重新定义顶点表结点结构,如下所示。

《图的存储结构与遍历》

      其中firstin表示入边表头指针,指向该顶点的入边表中第一个结点,firstout表示出边表头指针,指向该顶点的出边表中的第一个结点。重新定义边表结构,如下所示。

《图的存储结构与遍历》

      其中,tailvex是指弧起点在顶点表的下表,headvex是指弧终点在顶点表的下标,headlink是指入边表指针域,指向终点相同的下一条边,taillink是指边表指针域,指向起点相同的下一条边。如果是网,还可以增加一个weight域来存储权值。
比如下图,顶点依然是存入一个一维数组,实线箭头指针的图示完全与邻接表相同。就以顶点v0来说,firstout指向的是出边表中的第一个结点v3。所以,v0边表结点hearvex = 3,而tailvex其实就是当前顶点v0的下标0,由于v0只有一个出边顶点,所有headlink和taillink都是空的。

《图的存储结构与遍历》

       重点需要解释虚线箭头的含义。它其实就是此图的逆邻接表的表示。对于v0来说,它有两个顶点v1和v2的入边。因此的firstin指向顶点v1的边表结点中headvex为0的结点,如上图圆圈1。接着由入边结点的headlink指向下一个入边顶点v2,如上图圆圈2。对于顶点v1,它有一个入边顶点v2,所以它的firstin指向顶点v2的边表结点中headvex为1的结点,如上图圆圈3。
十字链表的好处就是因为把邻接表和逆邻接表整合在一起,这样既容易找到以v为尾的弧,也容易找到以v为头的弧,因而比较容易求得顶点的出度和入度。
而且除了结构复杂一点外,其实创建图算法的时间复杂度是和邻接表相同的,因此,在有向图应用中,十字链表是非常好的数据结构模型。

      (4)邻接多重表(无向图存储结构优化)

       如果我们在无向图的应用中,关注的重点是顶点的话,那么邻接表是不错的选择,但如果我们更关注的是边的操作,比如对已经访问过的边做标记,或者删除某一条边等操作,邻接表就显得不那么方便了。

      若要删除(V0,V2)这条边,就需要对邻接表结构中边表的两个结点进行删除操作:

《图的存储结构与遍历》

因此,我们也仿照十字链表的方式,对边表结构进行改装,重新定义的边表结构如下:

《图的存储结构与遍历》

其中iVex和jVex是与某条边依附的两个顶点在顶点表中的下标。iLink指向依附顶点iVex的下一条边,jLink指向依附顶点jVex的下一条边。

与邻接表相比,在邻接多重表里边,边表存放的是一条边,而不是一个顶点。

《图的存储结构与遍历》

如果要删除图中的(V0,V2)这条边,只需要设定表中(V3,V0)边的jLink=NULL,并把表中(V1,V2)边的jLink=NULL即可,基本操作的实现与邻接表类似。

      (5)边集数组

       边集数组是利用一维数组存储图中所有边的一种方式。每个元素用来存储一条边的起点、终点(对于无向图,可选定边的任一端点为起点或终点)和权(若有的话),各边在数组中的次序可任意安排,也可根据具体要求而定。比较节省内存,对于无向图而言任意一条边只需要存储一次即可;边集数组关注的是边的集合,在此查找一个顶点的度需要扫描整个边数组,效率不高,因此它更适合对边依次进行处理的操作,不适合顶点相关的操作,如应用于Kruskal最小生成树算法。
《图的存储结构与遍历》

                                                                             三、图的遍历


        图的遍历和树的遍历类似,希望从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫图的遍历。

        对于图的遍历来说,如何避免因回路陷入死循环,就需要科学地设计遍历方案,通过有两种遍历次序方案:深度优先遍历和广度优先遍历。

     (1) 深度优先遍历


        深度优先遍历,也有称为深度优先搜索,简称DFS。其实,就像是一棵树的前序遍历。
        它从图中某个结点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。若图中尚有顶点未被访问(非连通图有多个连通分量),则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中的所有顶点都被访问到为止。
        我们用邻接矩阵的方式,则代码如下所示。

//邻接矩阵的深度优先递归算法
void DFS(Graph 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(Graph 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);
		}
	}
}

       如果使用的是邻接表存储结构,其DFSTraverse函数的代码几乎是相同的,只是在递归函数中因为将数组换成了链表而有不同,代码如下。

//邻接表的深度递归算法
void DFS(GraphList g, int i)
{
	EdgeNode *p;
	visited[i] = TRUE;
	printf("%c ", g->adjList[i].data);//打印顶点,也可以其他操作
	p = g->adjList[i].firstedge;
	while(p)
	{
		if(!visited[p->adjvex])
		{
			DFS(g, p->adjvex);       //对未访问的邻接顶点递归调用
		}
		p = p->next;
	}
}

//邻接表的深度遍历操作
void DFSTraverse(GraphList g)
{
	int i;
	for(i = 0; i < g.numVertexes; i++)
	{
		visited[i] = FALSE;
	}
	for(i = 0; i < g.numVertexes; i++)
	{
		if(!visited[i])//连通图只会执行一次
		{
			DFS(g, i);
		}
	}
}

        对比两个不同的存储结构的深度优先遍历算法,对于n个顶点e条边的图来说,邻接矩阵由于是二维数组,要查找某个顶点的邻接点需要访问矩阵中的所有元素,因为需要O(n^2)的时间。而邻接表做存储结构时,找邻接点所需的时间取决于顶点和边的数量,所以是O(n+e)。显然对于点多边少的稀疏图来说,邻接表结构使得算法在时间效率上大大提高。对于有向图而言,通道可行与否在邻接表中已经体现出来了,不可行的通道不会出现在出边表中,因此算法上没有变化。

      (2) 广度优先遍历

        广度优先遍历,又称为广度优先搜索,简称BFS。图的广度优先遍历就类似于树的层序遍历了。可以用队列辅助层序遍历,注意到层序遍历是不可能递归实现的。
        邻接矩阵做存储结构时,广度优先搜索的代码如下。

//邻接矩阵的广度遍历算法
void BFSTraverse(Graph 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))     //将队中元素出队列,赋值给m
			{
				int m;
				DeQueue(&q, &m);        
				for(j = 0; j < g.numVertexes; j++)
				{
					//判断其他顶点若与当前顶点m存在边且未访问过
					if(g.arc[m][j] == 1 && !visited[j])
					{
						visited[j] = TRUE;
						printf("%c ", g.vexs[j]);
						EnQueue(&q, j);
					}
				}
			}
		}
	}
} 

          对于邻接表的广度优先遍历,代码与邻接矩阵差异不大, 代码如下。

//邻接表的广度遍历算法
void BFSTraverse(GraphList g)
{
	int i;
	EdgeNode *p;
	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.adjList[i].data);   //打印顶点,也可以其他操作
			EnQueue(&q, i);
			while(!QueueEmpty(q))
			{
				int m;
				DeQueue(&q, &m);
				p = g.adjList[m].firstedge;     //找到当前顶点边表链表头指针
					while(p)
					{
						if(!visited[p->adjvex]) //若此顶点未被访问
						{
							visited[p->adjvex] = TRUE;
							printf("%c ", g.adjList[p->adjvex].data);
							EnQueue(&q, p->adjvex);//将此顶点入队列
						}
						p = p->next;//指针指向下一个邻接点
					}
			}
		}
	}
}

        对比图的深度优先遍历与广度优先遍历算法,会发现,它们在时间复杂度上是一样的,不同之处仅仅在于对顶点的访问顺序不同。可见两者在全图遍历上是没有优劣之分的,只是不同的情况选择不同的算法。

       一般而言,DFS适合找到目标为主要目的的情况,BFS适合不断扩大遍历范围时找到相对最优解的情况。

本文部分参考自:http://blog.chinaunix.net/uid-26548237-id-3483650.html

 

    原文作者:数据结构之图
    原文地址: https://blog.csdn.net/spaceyqy/article/details/39010655
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞