数据结构之图
在前面我们所介绍的树的数据结构中,我们可以明显的感觉到,树的表示是分层的,例如父子关系,而其他关系只能间接的表示,例如同级关系。而图却不受这种限制。图是由顶点(或结点)及顶点之间的关系组成的集合。通常,图中的顶点数量或者一个顶点与其他顶点之间的连线的个数不受限制。(C++数据结构与算法)
定义(百度百科)
主要有以下两种定义。
二元组的定义:
图G是一个有序二元组(V,E),其中V称为顶集(Vertices Set),E称为边集(Edges set),E与V不相交。它们亦可写成V(G)和E(G)。
E的元素都是二元组,用(x,y)表示,其中x,y∈V。
三元组的定义:
图G是指一个三元组(V,E,I),其中V称为顶集,E称为边集,E与V不相交;I称为关联函数,I将E中的每一个元素映射到 。如果e被映射到(u,v),那么称边e连接顶点u,v,而u,v则称作e的端点,u,v此时关于e相邻。同时,若两条边i,j有一个公共顶点u,则称i,j关于u相邻。
在介绍图之前,首先让我们来了解一下图中的一个重要的分类。图的术语特别的多,不过我们可以慢慢的了解,因为定义都比较简单(我将在下面慢慢的介绍一些术语)。
-
无向图:图是有一组顶点和一组能够将两个顶点相连的边组成的。可能概念有点模糊,但是可以根下面的有向图相比较就特别简单了。
-
有向图:由一组顶点和一组有方向的边组成,每条有方向的边都连接着有序的一对顶点
(这张来自百度百科的图片都快糊了)
图的分类其实很多,但是我们主要介绍的就是这两种分类,还有一些分类可能会在接下来的博客中提到(我也不确定会不会提到,还没写)
图的术语表
-
相邻:如果两个顶点通过一条边相连, 则称这两个顶点是相邻的,并称这条边依附于这两个顶点
-
度数:某个顶点的度数即为依附于它的边的总数。
-
子图:一幅图的所有边的一个子集以及他们所依附的所有顶点组成的图。
-
路径:由边顺序链接的一系列顶点。
-
简单路径:一条没有重复顶点的路径。
-
环:一条至少包含一条边且起点和终点相同的路径。
-
简单环:除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的环。
-
连通图:任意两个顶点之间互通。一副非连通的图由诺干个连通的部分组成。
-
图的密度:已连接的顶点对占所有可能被连接的顶点对的比例。
-
平行边:连接同一对顶点的两条边称为平行边。
-
二分图:图中的每一条边所连接的两个顶点都分别属于不同的部分,如下图所示:
在这一章博客中,我所讲的内容会偏向于算法,并不会在数据结构上面说很多内容。
无向图
OK,在前面说完这么多,首先让我们来说下最简单的图:无向图
不过在说在无向图的操作之前,我们至少得解决一个问题:我们使用如何的结构去储存图。在前面我们知道,图不是像树一样(绝大部分的树),只需要关心父子关系,而不需要关心兄弟关系。简单点来说,就是树的关系是纵向的(从上到下),而图却并不是这样,图之间的关系是并列的。相信看过图这种数据结构的人,应该对于图的储存结构的方式可以说的信口拈来吧。下面是一些图的储存的方法:
-
邻接矩阵表示法
下图一眼就可以看懂,如果结点a与结点b之间相连接,则A(a,b) = A(b,a) = 1,否则为0。
-
邻接表表示法
在邻接表表示法中,第一列代表的为结点,如0,1,2……,而后面的则代表为结点与其他结点相连接的结点。(例如0结点后面为1,4结点,则代表0结点与1结点和4结点相连接【在这里我们可以发现,第5行的4结点的后面同样有1结点】)
-
关联矩阵表示法
那么我们该选择哪一种的表示方式呢?两种各有优缺点:
-
如果我们需要处理顶点V的邻接顶点,我们使用邻接表只需要deg(v)步操作(deg:图论中点连出的边数)。而在邻接矩阵中,我们需要进行|V|步操作。但是在当我们需要插入或者删除一个邻接与v的节点的时候,我们需要对邻接表进行调整,而对于邻接矩阵,只需要进行0和1的变换即可。
-
邻接矩阵的空间复杂度是O(V*V),而邻接表的复杂度为O(V+E),V为顶点数,E为边数。
我们将会遇到的应用使用几乎都是稀疏图——《算法第四版》
在这里我们可以再想一下,在最稠密的情况下(每一个结点都与其他结点相连接),邻接矩阵的空间复杂度会远远的 小于邻接表(n!和n^2不在一个数量级)。
- 如果出现平行边了,我们只能够使用邻接表去表示。
说了这么多,在下面的数据结构中,除非特殊说明,我们选择使用邻接表来进行数据储存。我们可以上代码了。
首先是抽象类的代码:
package graph;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
/**
* 图的抽象数据结构
* @author xiaohui
*/
public abstract class Graph {
// 顶点数量
int V;
// 边的数量
int E;
// 邻接表
List[] adj;
// 构造一个含有V个顶点的图,但是不含边
Graph(int V) {
adj = new ArrayList[V];
for (int i = 0; i < V; i++) {
adj[i] = new ArrayList<Integer>();
}
this.V = V;
}
/**
* @return 返回顶点的数量
*/
int V(){
return V;
}
/**
* @return 返回边的数量
*/
int E(){
return E;
}
/**
* 在图中添加一条边v-w
* @param v
* @param w
*/
abstract void addEdge(int v, int w);
/**
* 获得与v相邻的所有顶点
* @param v
* @return
*/
abstract Iterable<Integer> adj(int v);
/**
* 与结点s相连通的所有结点
* @param s
* @return
*/
abstract Iterable<Integer>search(int s);
/**
* 是否存在S结点到V结点的路径
* @param s
* @param v
* @return
*/
abstract boolean hasPathTo(int s,int v);
/**
* 找出s到v结点的路径
* @param s
* @param v
* @return
*/
abstract Iterable<Integer> pathTo(int s,int v);
/**
* 便于进行打印
* @return
*/
@Override
public String toString() {
String s = "Graph{" +
"V=" + V +
", E=" + E +
'}';
for (int v=0;v<V;v++){
s += (v+":");
for (int w :this.adj(v)) {
s += w+" ";
}
s+= "\n";
}
return s;
}
}
大家可能发现,上面的数据结构设计的不是很严谨,比如说结点都是使用了Int数据类型,而没有使用泛型。同样,这些方法不一定全部在一个类中实现,可能会进行分离。
首先让我们来实现较为简单的几个函数。
@Override
void addEdge(int v, int w) {
adj[v].add(w);
adj[w].add(v);
this.E ++;
}
@Override
Iterable<Integer> adj(int v) {
return adj[v];
}
接下来我们需要实现的就是众所周知的搜索函数了(因为深度优先搜索和广度有限搜索应该算大名鼎鼎的算法了吧)。我们想知道途中有哪一些的点,使用不同的算法会产生不同的作用效果。
深度优先搜索
深度优先搜索类似i走迷宫,一条路走到黑,如果发现这条路走不通,就在前一个路口继续向前走。就像下面这样(图片节选自《算法第四版》)
那么算法中,我们需要解决什么问题呢?我们可以通过adj函数得到结点的相邻结点,但是如果我们如何保证结点已经被我们访问过了,我们就需要一个标志mark,这个标志代表着这个结点是否已经被访问过。(HashSet这种数据结构也可以做到这种事情)。步骤如下:
- 将被访问的结点标记为已访问
- 递归地访问它的所有没有被标记过的邻居结点
/**
* 无向图的深度优先搜索
* @author xiaohui
*/
public class DepthFirstSearch {
private boolean[] marked;
private int count;
public DepthFirstSearch(UndirGraph graph,int s){
marked = new boolean[graph.V()];
dfs(graph,s);
}
private void dfs(UndirGraph graph, int s) {
marked[s] = true;
count++;
for (int v:graph.adj(s)){
if (!marked[v]){
dfs(graph,v);
}
}
}
public boolean getMarked(int w) {
return marked[w];
}
public int getCount() {
return count;
}
}
大家可以有上面的代码可以i很简单的知道,获得与s相同的结点,只需要对dfs进行递归即可,并将结点的marked标志设置为true即可。现在我们就可以完善search函数了。
Iterable<Integer> search(int s) {
DepthFirstSearch dfs = new DepthFirstSearch(this,s);
List list = new ArrayList(dfs.getCount());
for (int i=0;i<this.V();i++) {
if (dfs.getMarked(i)){
list.ad