本文首先介绍了图的入门概念,然后介绍了图的邻接矩阵和邻接表两种存储结构、以及深度优先遍历和广度优先遍历的两种遍历方式,最后提供了Java代码的实现。
图,算作一种比较复杂的数据结构,因此建议有一定数据结构基础的人再来学习!
1 图的定义和相关概念
定义
图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。图在数据结构中是中多对多的关系,而树则是1对多的关系,树就是一种特别的没有闭环的图。
顶点
图中的顶点就是节点的意思,不过图中任意的节点都算作顶点。将顶点集合为空的图称为空图。图中任意两个顶点之间都可能存在关系,顶点之间的逻辑关系用边来表示,边集可以是空的。
无向图
若顶点vi到vj之间的边没有方向,则称这条边为无向边(Edge),用无序偶对(vi,vj)来表示。如果图中任意两个顶点之间的边都是无向边,则称该图为无向图(Undirected graphs)。
有向图
若从顶点vi到vj的边有方向,则称这条边为有向边,也称为弧(Arc)。如果图中任意两个顶点之间的边都是有向边,则称该图为有向图(Directed graphs)。
完全图、稠密图、稀疏图
具有n个顶点,n(n-1)/2条边的图,称为完全无向图;具有n个顶点,n(n-1) 条弧的有向图,称为完全有向图。完全无向图和完全有向图都称为完全图。。
当一个图接近完全图时,则称它为稠密图,当一个图中含有较少的边或弧时,则称它为稀疏图。
权、网
有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权(Weight)。这些权可以表示从一个顶点到另一个顶点的距离或耗费。这种带权的图通常称为网(Network)。
子图
若有两个图G=(V1,E1), G2=(V2,E2), 满足如下条件:V2⊆V1 ,E2⊆ E1,即V2为V1的子集,E2为E1的子集,则 称图G2为图G1的子图。
下图中带底纹的图均为左侧无向图与有向图的子图。
临界点、度
相邻且有边直接连接的两个顶点称为邻接点。顶点所连边的数目称为度,在有向图中,由于边有方向,则顶点的度分为入度和出度。
简单路径、连通图
图中顶点间存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始点则称为环,当中不重复叫简单路径。若任意两顶点都是连通的,则图就是连通图,有向则称强连通图。图中有子图,若子图极大连通则就是连通分量,有向的则称强连通分量。
生成树
无向图中连通且n个顶点n-1条边叫生成树。有向图中一顶点入度为0其余顶点入度为1的叫有向树。一个有向图由若干棵有向树构成生成森林。
2 图的存储结构
由于图的结构比较复杂,任意两个顶点之间都可能存在联系,因此无法以单一的结构来表示。图最常见的表示形式为邻接链表和邻接矩阵,它们都是采用复合的结构来表示图。
2.1 邻接矩阵
邻接矩阵(Adjacency Matrix):采用两个数组来存储图,一个一维数组存储图顶点信息,一个二维数组存储图边或弧的信息,二维数组可以看作矩阵,这也是“邻接矩阵”名字的由来。这是最简单的图的存储方式,但是存在空间浪费的情况。
设图G有n个顶点,则邻接矩阵是一个n×n的方阵,定义为:
矩阵的对角线始终为0,因为顶点不能和自己连接。无向图的数据是对称的,有向图就一定了。
下图就是采用邻接矩阵表示的无向图。
下图就是采用邻接矩阵表示的有向图。
有向图讲究入度与出度,顶点0的入度为3,正好是第1列各数之和。顶点0的出度为3,即第1行的各数之和。一个点的入度是点所表示的列的各数的和,出度就是个点所表示的行的各数的和。
下图就是采用邻接矩阵表示的带权有向图。
注意,边二维数组中的数字表示权,没有关系的顶点(没有权)使用”/”表示。
2.2 邻接表
邻接表(Adjacency List):采用数组和链表存储,一个数组存储顶点,同时顶点想外拉出链表表示边或者弧,链表称为边表,如度的边表称为入边表,出度的边表称为出边表。邻接表的实现只关心存在的边,不关心不存在的边,因此没有空间浪费。
下图就是采用邻接表表示的无向图。
下图就是采用邻接表表示的有向图。
下图就是采用邻接矩阵表示的带权有向图。
邻接表在表示稀疏图时非常紧凑而成为了通常的选择,相比之下,如果在稀疏图表示时使用邻接矩阵,会浪费很多内存空间,遍历的时候也会增加开销。如果图是稠密图,邻接表的优势就不明显了,那么就可以选择更加方便的邻接矩阵。
3 图的遍历
3.1 深度优先遍历
深度优先遍历(Depth First Search),也有称为深度优先搜索,简称为DFS。类似于树的先序遍历。基本思想是“一条路走到黑”,然后往回退,回退过程中如果遇到没探索过的支路,就进入该支路继续深入,直到所有顶点都被访问到。
假设初始状态是图中所有顶点均未被访问,则从某个顶点v出发,首先访问该顶点,然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和v有路径相通的顶点都被访问到。但此时还有可能有其他分支我们没有访问到,若此时尚有其他顶点未被访问到,需要回溯,另选一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。显然,深度优先搜索是一个递归的过程。
对如下左边无向图从0顶点开始进行深度优先遍历之后一种结果为:0、1、4、5、3、2,如右图。
从起始点0开始遍历,在访问了0后,选择其邻接点1。因为1未曾访问过,则从1出发进行深度优先遍历。依次类推,接着从4、5、3出发进行遍历。在访问了3后,由于3的邻接点都已被访问,则遍历回退到5。此时5的另一个邻接点2未被访问,则遍历又从5到2,再继续进行下去,于是得到节点的线性顺序为:0、1、4、5、3、2,如右图中红色箭头线为其深度优先遍历顺序。
对如下左边有向图从0顶点开始进行深度优先遍历之后一种结果为:0、1、4、2、5、3,如右图。
有向图的深度优先遍历顶点的领边需要考虑顶点的出度对应的顶点。该顶点的出度对应的顶点算作邻接点。
从起始点0开始遍历,在访问了0后,选择其出度邻接点1、2。这里选择1进行访问,从1出发进行有向图深度优先遍历。依次类推,在访问了4后,由于4的出度邻接点0、1都已被访问,则遍历回退直到0。此时0的另一个邻接点2未被访问,则遍历又从0到2,再继续进行下去,于是得到节点的线性顺序为:0、1、4、2、5、4,如右图中红色箭头线为有向图深度优先遍历顺序。
3.2 广度优先遍历
广度优先遍历(Depth First Search) ,也有称为广度优先搜索,简称BFS,类似于树的层序遍历。基本思想是尽最大程度辐射能够覆盖的节点,并对其进行访问。
从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使得“先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问,直至图中所有已被访问的顶点的邻接点都被访问到。如果此时图中尚有顶点未被访问,则需要另选一个未曾被访问过的顶点作为新的起始点,重复上述过程,直至图中所有顶点都被访问到为止。换句话说,广度优先搜索遍历图的过程是以v为起点,由近至远。
对如下左边无向图从0顶点开始进行深度优先遍历之后一种结果为:0、1、2、3、4、5,如右图。
从起始点0开始遍历,在访问了0后,寻找邻接点,找到了1、2、3,一次遍历,访问完1、2、3之后,再依次访问它们的邻接点。首先访问1的邻接点4,再访问2的邻接点5。因此访问顺序是:0、1、2、3、4、5。
对如下左边有向图从0顶点开始进行广度优先遍历之后一种结果为:0、1、2、4、5、3,如右图。
有向图的广度优先遍历顶点的领边同样需要考虑顶点的出度对应的顶点。该顶点的出度对应的顶点算作邻接点。
从起始点0开始遍历,在访问了0后,选择其出度邻接点1、2,然后访问1、2;访问完1,2之后,再依次访问它们的邻接点。首先访问1的邻接点4依次类推,再访问2的邻接点5。访问完5之后,再依次访问它们的邻接点,最后访问5的邻接点3。此时所有顶点访问完毕。因此访问顺序是:0、1、2、4、5、3。
4 图的实现
关于图的实现,Guava中的com.google.common.graph模块已经提供了图的各种实现,而且都非常完美,这里只提供四个简单实现。带权重的图的实现,将在后面的最小生成树和最短路径部分提供实现。
4.1 无向图的邻接表实现
/**
* 无向图邻接链表简单实现
* {@link UndirectedAdjacencyList#UndirectedAdjacencyList(E[], E[][])} 构建无向图
* {@link UndirectedAdjacencyList#DFS()} 深度优先遍历无向图
* {@link UndirectedAdjacencyList#BFS()} 广度优先遍历无向图
* {@link UndirectedAdjacencyList#toString()} ()} 输出无向图
*
* @author lx
*/
public class UndirectedAdjacencyList {
/**
* 顶点类
*
* @param
*/
private class Node {
/**
* 顶点信息
*/
E data;
/**
*