四. 数据结构之图形结构

内容整理于鱼c工做室教程
数组

1. 图的基本概念

1.1 图的概念

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

4917487-e17aec0d5b5ca274.png

1.2 图的基本概念

顶点: 线性表中咱们把数据元素叫元素,树中叫结点,在图中数据元素咱们则称之为顶点(Vertex)。线性表能够没有数据元素,称为空表,树中能够没有结点,叫作空树,而图结构在咱国内大部分的教材中强调顶点集合V要有穷非空。优化

线性表中,相邻的数据元素之间具备线性关系,树结构中,相邻两层的结点具备层次关系,而图结构中,任意两个顶点之间均可能有关系,顶点之间的逻辑关系来表示,边集能够是空的。设计

无向边:若顶点Vi到Vj之间的边没有方向,则称这条边为无向边(Edge),用无序偶(Vi,Vj)来表示。3d

4917487-7c55a6f0a97f4899.png

有向边:若从顶点Vi到Vj的边有方向,则称这条边为有向边,也成为弧(Arc),用有序偶来表示,Vi称为弧尾,Vj称为弧头。blog

4917487-3967c088c5a52212.png

上图G2是一个有向图,G2={V2,E2},其中V2={A,B,C,D},E2={<B,A>,<B,C>,<C,A>,<A,D>}教程

简单图:在图结构中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。递归

4917487-221dd74cf88a68db.png

无向彻底图:在无向图中,若是任意两个顶点之间都存在边,则称该图为无向彻底图。含有n个顶点的无向彻底图有n*(n-1)/2条边。索引

4917487-8534b9eb0de89edf.png

有向彻底图:在有向图中,若是任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向彻底图。含有n个顶点的有向彻底图有n*(n-1)条边。图片

4917487-7a7b2dbcb21cd991.png

稀疏图和稠密图:这里的稀疏和稠密是模糊的概念,都是相对而言的,一般认为边或弧数小于n*logn(n是顶点的个数)的图称为稀疏图,反之称为稠密图。

有些图的边或弧带有与它相关的数字,这种与图的边或弧相关的数叫作(Weight),带权的图一般称为(Network)。

4917487-a67b32c43d597c90.png

假设有两个图G1=(V1,E1)和G2=(V2,E2),若是V2⊆V1,E2⊆E1,则称G2为G1的子图(Subgraph)。

4917487-8178befc86920f3d.png

图的顶点与边之间的关系:

(1) 无向图的顶点与边之间的关系:

对于无向图G=(V,E),若是边(V1,V2)∈E,则称顶点V1和V2互为邻接点(Adjacent),即V1和V2相邻接。

边(V1,V2)依附(incident)于顶点V1和V2,或者说边(V1,V2)与顶点V1和V2相关联。

顶点V的度(Degree)是和V相关联的边的数目,记为TD(V),以下图,顶点A与B互为邻接点,边(A,B)依附于顶点A与B上,顶点A的度为3。

4917487-b076a227f7e1c5f1.png

(2) 有向图的顶点与边之间的关系:

对于有向图G=(V,E),若是有∈E,则称顶点V1邻接到顶点V2,顶点V2邻接自顶点V1。

以顶点V为头的弧的数目称为V的入度(InDegree),记为ID(V),以V为尾的弧的数目称为V的出度(OutDegree),记为OD(V),所以顶点V的度为TD(V)=ID(V)+OD(V)。

下图顶点A的入度是2,出度是1,因此顶点A的度是3。

4917487-72bdcd11392fd324.png

简单路径:序列中顶点不重复出现的路径称为简单路径,除了第一个顶点和最后一个顶点以外,其他顶点不重复出现的回路,称为简单回路或简单环。

下图左侧是简单环,右侧不是简单环:

4917487-71a62bedf7cf8ee0.png

连通图:在无向图G中,若是从顶点V1到顶点V2有路径,则称V1和V2是连通的,若是对于图中任意两个顶点Vi和Vj都是连通的,则称G是连通图(ConnectedGraph)。

下图左侧不是连通图,右侧是连通图:

4917487-a0cd9591a825010f.png

1.3 图的存储结构

图的存储结构相比较线性表与树来讲就复杂不少。咱们回顾下,对于线性表来讲,是一对一的关系,因此用数组或者链表都可简单存放。树结构是一对多的关系,因此咱们要将数组和链表的特性结合在一块儿才能更好的存放。那么咱们的图,是多对多的状况,另外图上的任何一个顶点均可以被看做是第一个顶点,任一顶点的邻接点之间也不存在次序关系。

由于任意两个顶点之间均可能存在联系,所以没法以数据元素在内存中的物理位置来表示元素之间的关系(内存物理位置是线性的,图的元素关系是平面的)。若是用多重链表来描述却是能够作到,但在几节课前的树章节咱们已经讨论过,纯粹用多重链表致使的浪费是没法想像的(若是各个顶点的度数相差太大,就会形成巨大的浪费)。

1.3.1 邻接矩阵

(无向图)

考虑到图是由顶点和边或弧两部分组成,合在一块儿比较困难,那就很天然地考虑到分为两个结构来分别存储。顶点由于不区分大小、主次,因此用一个一维数组来存储是狠不错的选择。而边或弧因为是顶点与顶点之间的关系,一维数组确定就搞不定了,那咱们不妨考虑用一个二维数组来存储。

图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。

4917487-b2a97d9e29914a8a.png

咱们能够设置两个数组,顶点数组为vertex[4]={V0,V1,V2,V3},边数组arc[4][4]为对称矩阵(0表示不存在顶点间的边,1表示顶点间存在边)。

对称矩阵:所谓对称矩阵就是n阶矩阵的元知足a[i][j]=a[j][i](0<=i,j<=n)。即从矩阵的左上角到右下角的主对角线为轴,右上角的元与左下角相对应的元全都是相等的。

有了这个二维数组组成的对称矩阵,咱们就能够很容易地知道图中的信息:

要断定任意两顶点是否有边无边就很是容易了;

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

求顶点Vi的全部邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]为1就是邻接点咯。

(有向图)

无向图的边构成了一个对称矩阵,貌似浪费了一半的空间,那若是是有向图来存放,会不会把资源都利用得很好呢?

4917487-651a9caee5b1c607.png

可见顶点数组vertex[4]={V0,V1,V2,V3},弧数组arc[4][4]也是一个矩阵,但由于是有向图,因此这个矩阵并不对称,例如由V1到V0有弧,获得arc[1][0]=1,而V0到V1没有弧,所以arc[0][1]=0。

另外有向图是有讲究的,要考虑入度和出度,顶点V1的入度为1,正好是第V1列的各数之和,顶点V1的出度为2,正好是第V1行的各数之和。

(网)

在图的术语中,咱们提到了网这个概念,事实上也就是每条边上带有权的图就叫网。

4917487-1784caf0f5622d85.png

1.3.2 邻接表

(无向图)

邻接矩阵看上去是个不错的选择,首先是容易理解,第二是索引和编排都很舒服~

可是咱们也发现,对于边数相对顶点较少的图,这种结构无疑是存在对存储空间的极大浪费。

4917487-d96853fda3200b9a.png

所以咱们能够考虑另一种存储结构方式,例如把数组与链表结合一块儿来存储,这种方式在图结构也适用,咱们称为邻接表(AdjacencyList)。

邻接表的处理方法是这样:

图中顶点用一个一维数组存储,固然,顶点也能够用单链表来存储,不过数组能够较容易地读取顶点信息,更加方便。图中每一个顶点Vi的全部邻接点构成一个线性表,因为邻接点的个数不肯定,因此咱们选择用单链表来存储。

4917487-4a19ce5feb3c065e.png

(有向图)

如果有向图,邻接表结构也是相似的,咱们先来看下把顶点当弧尾创建的邻接表,这样很容易就能够获得每一个顶点的出度:

4917487-a3e73fcb9ea890b7.png

(网)

对于带权值的网图,能够在边表结点定义中再增长一个数据域来存储权值便可:

4917487-035af8729525bd68.png

1.3.3 十字链表

邻接表当然优秀,但也有不足,例如对有向图的处理上,有时候须要再创建一个逆邻接表~那咱们思考了:有没有可能把邻接表和逆邻接表结合起来呢?答案是确定的,这就是咱们如今要谈的十字链表(Orthogonal List)。为此咱们从新定义顶点表结点结构:

4917487-ecfdadd6aafc5bc5.png
4917487-a2a9676a5d57796d.png

1.3.4 邻接多重表

讲了有向图的优化存储结构,对于无向图的邻接表,有没有问题呢?若是咱们在无向图的应用中,关注的重点是顶点的话,那么邻接表是不错的选择,但若是咱们更关注的是边的操做,好比对已经访问过的边作标记,或者删除某一条边等操做,邻接表就显得不那么方便了。

到底有多烦?小甲鱼用图片告诉你:

4917487-526c55e2949e193b.png
4917487-ed91be2687f9e72a.png

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

4917487-52ba4fa26fbd149c.png

1.3.5 边集数组

4917487-31270857a645886a.png

边集数组是由两个一维数组构成,一个是存储顶点的信息,另外一个是存储边的信息,这个边数组每一个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成。

1.4 图的遍历

树的遍历咱们谈了四种方式,你们回忆一下,树由于根结点只有一个,而且全部的结点都只有一个双亲,因此不是很难理解。可是谈到图的遍历,那就复杂多了,由于它的任一顶点均可以和其他的全部顶点相邻接,所以极有可能存在重复走过某个顶点或漏了某个顶点的遍历过程。对于图的遍历,若是要避免以上状况,那就须要科学地设计遍历方案,一般有两种遍历次序方案:它们是深度优先遍历和广度优先遍历。

1.4.1 深度优先遍历

深度优先遍历(DepthFirstSearch),也有称为深度优先搜索,简称为DFS。

4917487-01bbc409cef1928b.png

它的具体思想相似于课程开头讲的找钥匙方案,不管从哪一间房间开始均可以,将房间内的墙角、床头柜、床上、床下、衣柜、电视柜等挨个寻找,作到不放过任何一个死角,当全部的抽屉、储藏柜中所有都找遍,接着再寻找下一个房间。如今请你们一块儿来想办法走如下这个迷宫:

4917487-c67d53cdbd70cfe3.png

咱们能够约定右手原则:在没有碰到重复顶点的状况下,分叉路口始终是向右手边走,每路过一个顶点就作一个记号。迷宫走完了,全部的顶点也遍历过了,这就是深度优先遍历!反应快的童鞋必定会感受深度优先遍历其实就是一个递归的过程嘛~若是再细心观察,你会发现整个遍历过程就像是一棵树的前序遍历!


1.4.2 广度优先遍历

广度优先遍历(BreadthFirstSearch),又称为广度优先搜索,简称BFS。

若是以以前咱们找钥匙的例子来说,运用深度优先遍历意味着要先完全查找完一个房间再开始另外一个房间的搜索。但咱们知道,钥匙放在沙发地下等犄角旮旯的可能性极低,所以咱们运用新的方案:先看看钥匙是否放在各个房间的显眼位置,若是没有,再看看各个房间的抽屉有没有。这样逐步扩大查找的范围的方式咱们称为广度优先遍历。

广度优先遍历是连通图的一种遍历策略。其基本思想以下:

一、从图中某个顶点V0出发,并访问此顶点;

二、从V0出发,访问V0的各个不曾访问的邻接点W1,W2,…,Wk;而后,依次从W1,W2,…,Wk出发访问各自未被访问的邻接点;

4917487-675d62041b566486.png

三、重复步骤2,直到所有顶点都被访问为止。