您的位置:首页 > 理论基础 > 数据结构算法

数据结构之图(术语、存储结构、遍历)

2014-06-04 14:51 495 查看

1、相关术语

顶点(Vertex)、弧(Arc)、弧头(初始点)、弧尾(终结点)、边(Edge)、有向图(Directed graph)、无向图(Undigraph)、完全图(Completed grapg)、有向完全图、稀疏图(Sparse graph)、稠密图(Dense graph)、权(weigh)、网(network)、无向网、有向网、子图(Subgraph)、邻接点(Adjacent)、度(Degree)、入度(Indegree)、出度(Outdegree)、路径(path)、回路(环)、简单路径、简单回路(简单环)、连通、连通图(Connected
graph)、连通分量(Connected Component)、强连通图强连通分量(有向图中的极大强连通子图)、生成树、极小连通子图、有向树。

无向图:G=(V, {E})、0≤边≤n(n-1)/2

有向图:G=(V, {A})、0≤弧≤n(n-1)

连通图:在无向图G中,如果图中任意两个顶点vi, vj属于V,vi和vj都是连通的,则图G是连通图。

连通分量:无向图中的极大连通子图

图例:



强连通图:在有向图G中,如果每一对顶点vi, vj属于V且vi不等于vj,从vi到vj与从vj到vi都存在路径,则图G是连通图。

强连通分量:有向图的极大强连通子图。

生成树:一个连通图的生成树是一个极小连通子图,它含有图中全部顶点,但只有足以构成一棵树的n-1条边。

如果在生成树上添加一条边,必定构成一个环:因为这条边使得它依附的那两个顶点之间有了第二条路径。

一个有n个顶点的生成树有且仅有n-1条边。如果一个图有n和顶点和小于n-1条的边,则是非连通图。如果多余n-1条边,则一定有环。但有n-1条边的图不一定是生成树。

2、存储结构

2.1邻接矩阵(数组表示法)

(无向图、有向图、无向网、有向网)

用两个数组来表示图。一个一维数组存储图中数据元素(顶点)的信息,一个二维数组(邻接矩阵)存储图中数据元素之间的关系(边或弧)的信息。

图G是无向图,有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:



下图就是一个无向图:



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

从这个矩阵中,很容易知道图中的信息。

(1)要判断任意两顶点是否有边无边就很容易了;

(2)要计算某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行或(第i列)的元素之和;

(3)求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]=1的vj就是邻接点;

而有向图有入度和出度之分:顶点vi的入度为是第i列各数之和,顶点vi的出度是第i行的各数之和。

图G是网图,有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:



wij表示(vi,vj)或<vi,vj>上的权值。无穷大表示一个计算机允许的、大于所有边上权值的值,也就是一个不可能的极限值。下图是一个有向网图和它的邻接矩阵:


注:这个边数组(邻接矩阵)的对角线应该是无穷大。

可以看出:

(1)第i行权重大于0、小于无穷的个数之和为顶点vi的出度(OD(vi));

(2)第j列权重大于0、小于无穷的个数之和为顶点vj的入度(ID(vj));

(3)第i行和第i列权重大于0、小于无穷的个数之和即为顶点vi的度(TD(vi) = OD(vi) + ID(vj))。

使用邻接矩阵存储图并创建图的代码示例如下:

typedef char VertexType; //顶点类型,由用户自己定义
typedef int  EdgeType;   //边上的权值类型,由用户自己定义

#define MAX_VEX 20  //最大顶点数,由用户定义
#define	INFINITY 65535 //代表无穷大

typedef struct
{
VertexType vexs[MAX_VEX]; //顶点数组
EdgeType   arc[MAX_VEX][MAX_VEX]; //邻接矩阵
int vexNum, arcNum; //图中当前定点数和弧数
}Graph;


//定位顶点v在顶点数组的下标位置,不存在返回-1
int LocateVex(Graph *g, VertexType v)
{
int i = 0;
for (i = 0; i < g->vexNum; i++)
{
if (g->vexs[i] == v)
break;
}
if (i > g->vexNum)
{
fprintf(stderr,"no such vertex.\n");
return -1;
}
return i;
}

//用邻接矩阵表示法,构造有/无向网g
void CreateUDN(Graph *g)
{
int i, j;
EdgeType w;

printf("输入顶点数和边数:\n");
scanf("%d,%d", &(g->vexNum), &(g->arcNum));
//输入顶点
for(i = 0; i < g->vexNum; i++)
{
printf("顶点%d:", i + 1);
g->vexs[i] = getchar();
while(g->vexs[i] == '\n')
{
g->vexs[i] = getchar();
}
}
getchar();
//初始化邻接矩阵
for (i = 0; i < g->arcNum; i++)
{
for (j = 0; j < g->arcNum; j++)
{
g->arc[i][j] = INFINITY;
}
}

printf("输入边(vi, vj)上的顶点vi vj和权值w\n");
for (i = 0; i < g->arcNum; i++)
{
char v1, v2;
//v1 = getchar();
//while (v1 == '\n')
//{
//	v1 = getchar();
//}
//v2 = getchar();
//while (v2 == '\n')
//{
//	v2 = getchar();
//}
//scanf("%d", &w);
scanf("%c %c %d", &v1, &v2, &w);
getchar();
int m = LocateVex(g, v1);
int n = LocateVex(g, v2);
if (m == -1 || n == -1)
{
fprintf(stderr, "no this vertex!\n");
return;
}
g->arc[m]
= w;
#if 0
g->arc
[m] = g->arc[m]
;	//无向图的邻接矩阵对称
#endif
}
}

void PrintGrapth(Graph *g)
{
for (int i = 0; i < g->vexNum; i++)
{
for (int j = 0; j < g->vexNum; j++)
{
printf("%6d", g->arc[i][j]);
}
putchar('\n');
}
}


int main()
{
Graph g;
CreateUDN(&g);
PrintGrapth(&g);
return 0;
}


创建上文中的有向网图,程序运行示例截图:



创建无向网图,运行截图为:



n个顶点和e条边的无向网图的创建,时间复杂度为O(n + n^2 + e·n) = O(n^2 + e·n),其中第一个n是输入n个顶点;对邻接矩阵的初始化耗费了O(n^2)的时间;对每一条边,定位两个顶点的下标花费了O(n),因此是O(e·n)。

2.2邻接表

对于边数相对顶点较少的图,邻接矩阵这种结构会对存储空间造成极大浪费。邻接表(Adjacency List)是图的一种链式存储结构,是数组与链表相结合来存储图。
邻接表的处理方法是这样的:

(1)图中顶点用一个一维数组存储。当然顶点也可以用单链表来存储,不过,数组可以较容易的读取顶点的信息,更加方便。

(2)图中每个顶点vi的所有邻接点构成一个线性表。由于邻接点的个数不定,所以,用单链表存储,无向图称为顶点vi的边表,有向图则称为顶点vi作为弧尾的出边表。

例如,下图就是一个无向图的邻接表的结构。



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



使用邻接表储图并创建网图的代码示例如下:

typedef char VertexType;	//顶点类型,由用户自己定义
typedef int  EdgeType;	//边上的权值类型,由用户自己定义

#define MAX_VEX 20	//最大顶点数,由用户定义

typedef struct EdgeNode	//边表结点
{
int adjvex;	//邻接点域,存储该顶点在顶点表中的下标
EdgeType weight;	//网图权值
struct EdgeNode *next;	//链域,指向下一个邻接点
}EdgeNode;

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

typedef struct
{
AdjList adjList;
int vexNum, arcNum; //图中当前顶点数和弧数
}GraphList;


//定位顶点v在顶表的下标位置,不存在返回-1
int LocateVex(GraphList *g, VertexType v)
{
int i;
for (i = 0; i < g->vexNum; i++)
{
if (v == g->adjList[i].data )
break;
}

if (i > g->vexNum)
{
fprintf(stderr,"no this vertex.\n");
return -1;
}
return i;
}

//用邻接表表示法,构造有/无向网g
void CreateGraph(GraphList *g)
{
int i, j;
EdgeType w;
EdgeNode *e, *f;

printf("输入顶点数和边数:\n");
scanf("%d,%d", &(g->vexNum), &(g->arcNum));

//输入顶点
for (i = 0; i < g->vexNum; i++)
{
printf("顶点%d:", i);
g->adjList[i].data = getchar();
g->adjList[i].firstedge = NULL;
while(g->adjList[i].data == '\n')
{
g->adjList[i].data = getchar();
}
}
getchar();

//建立边表
printf("输入边(vi, vj)上的顶点vi vj和权值w\n");
for (i = 0; i < g->arcNum; i++)
{
char v1, v2;
scanf("%c %c %d", &v1, &v2, &w);
getchar();
int m = LocateVex(g, v1);
int n = LocateVex(g, v2);
if (m == -1 || n == -1)
{
fprintf(stderr, "no this vertex!\n");
return;
}

//向内存申请空间,生成边表结点
e = (EdgeNode*)malloc(sizeof(EdgeNode));
if(e == NULL)
{
fprintf(stderr, "malloc() error.\n");
return;
}
e->adjvex = n;	//邻接序号为n
e->weight = w;	//权值
e->next = g->adjList[m].firstedge; //单链表的头插法
g->adjList[m].firstedge = e;

#if 0
//因为是无向网图
f = (EdgeNode*)malloc(sizeof(EdgeNode));
if(e == NULL)
{
fprintf(stderr, "malloc() error.\n");
return;
}
f->adjvex = m;
f->weight = w;
f->next = g->adjList
.firstedge;
g->adjList
.firstedge = f;
#endif
}
}

void PrintGrapth(GraphList *g)
{
int i;
for (i = 0; i < g->vexNum; i++)
{
printf("顶点%c ", g->adjList[i].data) ;
EdgeNode *e = g->adjList[i].firstedge;
while (e != NULL)
{
printf("—> <%c, %c> %d  ", g->adjList[i].data, g->adjList[e->adjvex].data, e->weight);
e = e->next;
}
putchar('\n');
}
}


int main()
{
GraphList g;
CreateGraph(&g);
PrintGrapth(&g);
return 0;
}


创建上文中的有向网图,程序运行示例截图:



若创建无向网图,运行截图为:



对于n个顶点e条边,本算法的时间复杂度是O(n + e·n),因为输入的顶点信息不是顶点的编号,需要通过查找才能够得到顶点在图中的位置。如果输入的是顶点的编号,那么只需要O(n+e)的时间复杂度。

对于有向图,邻接表可以方便的求某一个顶点的出度,但是不方便求入度,需要遍历整个边表。逆邻接表可以方便求入度,但不方便求出度。

另外关于图的存储结构还有:十字链表、邻接多重表。

3、图的遍历

图的遍历(Traversing Graph):从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次。图的遍历算法是求解图的连通性问题、拓扑排序和求关键路径等算法的基础。

3.1深度优先搜索

深度优先搜索(Depth First Search),简称DFS,其遍历类似树的前序遍历。

它从图中某个结点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中的所有顶点都被访问到为止。

邻接矩阵存储结构的DFS代码:

bool visited[MAX_VEX];//访问标志数组

//邻接矩阵的深度优先递归算法
void DFS(Graph *g, int i)
{
int j;
visited[i] = true;
printf("%c ", g->vexs[i]);	//打印顶点,也可以是其他操作
for (j = 0; j < g->vexNum; j++)
if (g->arc[i][j] > 0 && g->arc[i][j] != INFINITY && !visited[j])
DFS(g, j);
}

//邻接矩阵的深度遍历操作
void DFSTraverse(Graph *g)
{
int i;
for (i = 0; i < g->vexNum; i++)
visited[i] = false;		//初始化所有顶点状态都是未访问过状态
for (i = 0; i < g->vexNum; i++)
if (!visited[i])
DFS(g, i);		   //对未访问的顶点调用DFS.若是连通图,只会执行一次
}


邻接表存储结构的DFS代码:

//邻接矩阵的深度优先递归算法
void DFS(GraphList *g, int i)
{
EdgeNode *e;
visited[i] = true;
printf("%c ", g->adjList[i].data);	//打印顶点,也可以是其他操作
for (e = g->adjList[i].firstedge; e != NULL; e = e->next)
if (!visited[e->adjvex])
DFS(g, e->adjvex);
}

//邻接矩阵的深度遍历操作
void DFSTraverse(GraphList *g)
{
int i;
for (i = 0; i < g->vexNum; i++)
visited[i] = false;		//初始化所有顶点状态都是未访问过状态
for (i = 0; i < g->vexNum; i++)
if (!visited[i])
DFS(g, i);		   //对未访问的顶点调用DFS.若是连通图,只会执行一次
}


对于n个顶点e条边的图来说,邻接矩阵由于是二维数组,要查找某个顶点的邻接点需要访问矩阵中的所有元素,因为需要O(n^2)的时间。而邻接表做存储结构时,找邻接点所需的时间取决于顶点和边的数量,所以是O(n+e)。显然对于点多边少的稀疏图来说,邻接表结构使得算法在时间效率上大大提高。

3.2广度优先搜索

广度优先搜索(Breadth First Search),简称DFS,其遍历类似树的层次遍历。

假设从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点“先于“后被访问的顶点的邻接点”被访问,直至图中所有已被访问的顶点的邻接点都被访问到。若此时图中尚有顶点未被访问,则选中图中一个未曾被访问的顶点 作起始点,重复上述过程直至图中所有顶点都被访问到为止。

//邻接矩阵的BFS
void BFSTraverse(Graph *g)
{
SqQueue q;
int v;
for (v = 0; v < g->vexNum; v++)
visited[v] = false;
InitQueue(q);		 //辅助队列
for (v = 0; v < g->vexNum; v++)
if (!visited[v])	//第v个顶点尚未访问
{
visited[v] = true;
printf("%c ", g->vexs[v]);	 //打印顶点,也可以是其他操作
EnQueue(q, v);	//第v个顶点入队列
while (!QueueEmpty(q))
{
int i;
DeQueue(q, i);
for (int j = 0; j < g->vexNum; j++)
//判断其他顶点若与当前顶点存在边且未访问过
if (g->arc[i][j] > 0 && g->arc[i][j] != INFINITY && !visited[j])
{
visited[j] = true;		 //j为i尚未访问过的邻接顶点
printf("%c ", g->vexs[j]);
EnQueue(q, j);
}  //if
}//while
}//if
}


//邻接表的BFS
void BFSTraverse(GraphList *g)
{
SqQueue q;
int i;
for (i = 0; i < g->vexNum; i++)
visited[i] = false;
InitQueue(q);
for (i = 0; i < g->vexNum; i++)
if (!visited[i])
{
visited[i] = true;
printf("%c ", g->adjList[i].data);
EnQueue(q, i);
while (!QueueEmpty(q))
{
int j;
DeQueue(q, j);
//找到当前顶点边表链表头指针
for (EdgeNode *e = g->adjList[j].firstedge; e != NULL; e = e->next)
if (!visited[e->adjvex])
{
visited[e->adjvex] = true;
printf("%c ", g->adjList[e->adjvex].data);
EnQueue(q, e->adjvex);
}  //if
}//while
}  //if
}


图的深度优先遍历与广度优先遍历算法在时间复杂度上是一样的,不同之处仅仅在于对顶点的访问顺序不同。

参考:数据结构(C语言版)

本文全部测试代码:http://download.csdn.net/detail/u013071074/7445893

蓝色梦魔
2014.6.4,16:20
再参考:经典算法研究系列:四、教你通透彻底理解:BFS和DFS优先搜索算法(此博文系列作者写得非常好)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: