在数学中,一个
图(Graph)
是表示物件与物件之间关系的方法,是
图论
的基本研究对象.一个图是由
顶点(Vertex)
与连接这些
顶点
的
边(Edge)
组成的.
图论
作为数学领域中的一个重要分支已经有数百年的历史了.人们发现了图的许多重要而实用的性质,发明了许多重要的算法,给你一个
图(Graph)
你可以联想到许多问题: 两个
顶点
之间是否存在一条链接?如果存在,两个
顶点
之间最短的连接又是哪一条?….
在生活中,到处都可以发现
图论
的应用:
图(Graph)
,十字路口就是
顶点
,公路就是
边
.
图
,它的
顶点
为网页,
边
为超链接.而
图论
可以帮助我们在网络上定位信息.
图论
.
顶点
,你和你的朋友建立的关系则是
边
.分析这些社交网络的性质也是
图论
的一个重要应用.
图
就是由一组
顶点
和一组能够将两个
顶点
相连的
边
组成的.
顶点
通过一条
边
相连接时,这两个
顶点
即为相邻的(也可以说这条
边
依附于这两个
顶点
).
顶点
的
度数
即为依附于它的
边
的总数.
图G
中的
顶点集合V
的大小称为
G
的阶.
顶点
和其自身的
边
.
平行边: 连接同一对
顶点
的两条
边
称为平行边.
桥: 如果去掉一条
边
会使整个
图
变成
非连通图
,则该
边
称为桥.
路径: 当
顶点v
到
顶点w
是连通时,我们用
v->x->y->w
为一条
v
到
w
的路径,用
v->x->y->v
表示一条环.
连通分量
,它由一张
图
的所有边的一个子集组成的
图
(以及依附的所有顶点).
连通图
是一个整体,而
非连通图
则包含两个或多个
连通分量
.
边
的数量在
顶点
总数
V
的一个小的常数倍内,那么该图就为稀疏图,否则为稠密图.
平行边
与
自环
的图称为
多重图
,而不含有
平行边
和
自环
的图称为
简单图
.
树是一张
无环连通图
,互不相连的树组成的集合称为森林.
连通图
的
生成树
是它的一张子图,它含有图中的所有顶点且是一棵树.图的
生成树森林
是它的所有
连通分量
的
生成树
的集合.
图
G
只要满足以下性质,那么它就是一棵树:
G
有
V-1
条边且不含有环.
G
有
V-1
条边且是连通的.
G
是连通的,但删除任意一条边都会使它不再连通.
G
是无环图,但添加任意一条边都会产生一条环.
G
中的任意一对顶点之间仅存在一条简单路径(一条没有重复顶点的路径).
二分图
是一种能够将所有
顶点
分为两部分的图,其中
图
的每条边所连接的两个
顶点
都分别属于不同的部分.
设
G = (V,E)
为一张
无向图
,如果顶点
V
可以分割为两个互不相交的子集
(U,V)
,且图中的每条边
(x,y)
所关联的两个顶点
x
,
y
分别属于这两个不同的顶点集合
(x in U , y in V)
,则
G
为
二分图
.
也可以将
(U,V)
当做一张
着色图
:
U
中的所有顶点为蓝色,
V
中的所有顶点为绿色,每条边所关联的两个
顶点
颜色不同.
无向图
是一种最简单的图模型,它的每条边都没有方向.
实现一张
图
的
API
需要满足以下两个要求:
图
预留出足够的空间.
图
的实现一定要足够快(因为这是所有处理
图
的算法的基础结构).
有以下三种数据结构能够用来表示一张图:
V * V
的布尔矩阵.当顶点
v
和顶点
w
之间有相连接的
边
时,将
v
行
w
列的元素设为
true
,否则为
false
.这种方法不符合第一个条件,当
图
的顶点非常多时,邻接矩阵所需的空间将会非常大.且它无法表示平行边.
Edge
类,它含有两个
int
成员变量来表示所依附的顶点.这种方法简单直接但不满足第二个条件(要实现查询邻接点的函数需要检查图中的所有边).
顶点
为索引的
链表数组
,其中的每个元素都是和该
顶点
相邻的顶点列表(邻接点)
.这种方法同时满足了两个条件,我们会使用这种方法来实现
图
的数据结构.
|
|
上面的这个实现拥有以下特点:
V + E
成正比.
v
的所有邻接点所需的时间和
v
的度数成正比(处理每个邻接点所需的时间为常数).
符号表
来代替由顶点索引构成的数组).
SET
来代替
Bag
来实现邻接表,这种方法也叫
邻接集
).
每种
图
实现的性能复杂度如下表:
处理
图
的基本问题:
v 到 w是否是相连的?
.
深度优先搜索
就是用于解决这样问题的,它会
沿着
图
的
边
寻找和
起点
连通的所有
顶点
.
如其名一样,
深度优先搜素
就是沿着
图
的
深度
来遍历
顶点
,它类似于走迷宫,会沿着一条路径一直走,直到走到尽头时再回退到上一个路口.为了防止迷路,还需要使用工具来标记已走过的路口(在我们的代码实现中使用一个布尔数组来进行标记).
使用递归方法来实现
深度优先搜索
会很简洁,当遇到一个
顶点
时:
|
|
如果是了解
JVM
中函数调用的小伙伴们应该知道,函数都会封装成一个个
栈帧
然后压入
虚拟机栈
,上述的递归实现其实就是在隐式的使用到了
栈
,要想实现非递归,我们需要显式使用
栈
这个数据结构.
|
|
在
图
的应用中,找出
v-w
的可达路径也是常见的问题之一.
我们基于
深度优先搜索
实现寻找路径,并添加一个
edgeTo[]
整形数组来记录路径.例如,在由边
v-w
第一次访问任意
w
时,将
edgeTo[w]
设为
v
来记录这条路径(
v-w
是从起点到
w
的路径上最后一条已知的边).这样搜索到的路径就是一颗以起点为根的树,
edgeTo[]
是一颗由父链接表示的树.
|
|
对于寻找一条最短路径,
深度优先搜索
没有什么作为,因为它遍历整个图的顺序和找出最短路径的目标没有任何关系.这种问题就需要用到
广度优先搜索
.
广度优先搜索
是沿着宽度来进行搜索的.例如,要找到
s
到
v
的最短路径,
从
s
开始,在所有由一条边就可以到达的
顶点
中寻找
v
,如果找不到就继续在与
s
距离两条边的所有顶点中寻找
v
,以此类推
.
如果说
深度优先搜索
是一个人在走迷宫,那么
广度优先搜索
就是一群人一起朝着各个方向去走迷宫.
在
广度优先搜索
中,我们使用一个
队列
来保存所有已被标记过但
邻接表
还未被检查过的
顶点
.先将
起点
放入
队列
,然后重复以下步骤直到
队列
为空:
队列
中的下一个
顶点
并标记.
顶点
加入队列.
|
|
不管是
深度优先搜索
还是
广度优先搜索
,它们都是先将
起点
存入一个
数据结构
中,然后重复以下步骤直到
数据结构
被清空:
顶点
并标记它.
相邻
而又未被标记的
顶点
放入
数据结构
中.
这两种
算法
的
不同之处仅在于从
数据结构
中获取下一个
顶点
的规则(对于
广度优先搜索
来说是最早加入的
顶点
,对于
深度优先搜索
来说是最晚加入的
顶点
)
.
深度优先搜索
的方式是不断寻找离
起点
更远的
顶点
,直到碰见死胡同时才返回近处
顶点
.
广度优先搜索
的方式是先覆盖
起点
附近的
顶点
,只有当
邻接
的所有
顶点
都被访问过之后才继续前进.
深度优先搜素
的路径通常长且曲折,
广度优先搜索
的路径则短而直接.但不管是使用哪种
算法
,所有与
起点
连通的
顶点
和
边
都会被访问到.
深度优先搜索
的一个重要应用就是寻找出一幅
图
中的所有连通分量.
|
|
深度优先搜索
的应用远不于此,它还可以用来检测是否有环以及双色问题.
|
|
|
|
在很多应用中,是使用字符串而非整数来表示
顶点
的,为了适应这种需求,需要拥有以下性质的输入格式:
V
与边集
E
都是隐式定义的.
要实现
符号图
还需要借助以下数据结构:
符号表
,我这里使用的是
TreeMap
即
红黑树
,它的
Key
为
String
(顶点名),
Value
为
Integer
(顶点索引).
字符串数组
,它用来与
符号表
作
反向索引
,保存每个
顶点
索引所对应的
顶点名
.
Graph
对象,我们使用索引来生成这张
图
对象.
|
|
本文作者为 SylvanasSun([email protected]) ,转载请务必指明原文链接.