上一篇文章中记录了如何实现图的邻接表。本文借助上一篇文章实现的邻接表来表示一个有向无环图。

1,概述

图的实现与邻接表的实现最大的不同就是,图的实现需要定义一个数据结构来存储所有的顶点以及能够对图进行什么操作,而邻接表的实现重点关注的图中顶点的实现,即怎么定义JAVA类来表示顶点,以及能够对顶点进行什么操作。

为了存储图中所有的顶点,定义了一个Map<key, value>,实际实现为LinkedHashMap<T, VertexInterface<T>>,key 为 顶点的标识,key 是泛型,这样就可以用任意数据类型来标识顶点了,如String、Integer……

value 当然就是表示顶点的类了,因为我们需要存储的是顶点嘛。即value 为 VertexInterface<T> 。这里为什么不用List而用Map来存储顶点呢?用Map的好处就是方便查询顶点,即可以用顶点标识来查找顶点。这也是为了方便后面实现图的DFS、BFS 等算法而考虑的。

此外,还定义了一个整型变量 edgeCount 用来保存图中边的数目,这也是必要的。讨论一个图,当然要有图的顶点,由Map保存,顶点数目可以通过 Map.size() 方法获得;也要有边,而边已经隐含在Vertex.java中了(具体参考上一篇文章),因此这里只定义一个保存图中边的总数的变量即可。图的定义 部分代码如下:

 public class DirectedGraph<T> implements GraphInterface<T>,java.io.Serializable{

     private static final long serialVersionUID = 1L;

     private Map<T, VertexInterface<T>> vertices;//map 对象用来保存图中的所有顶点.T 是顶点标识,VertexInterface为顶点对象
private int edgeCount;//记录图中 边的总数 public DirectedGraph() {
vertices = new LinkedHashMap<>();//按顶点的插入顺序保存顶点
}

2,图的基本操作

这里的基本操作不是对图进行DFS、BFS、拓扑排序、求最短路径……而是一系列的如何构造图的方法,这些方法是实现图的遍历、求最短路径、拓扑排序的基础。

在 1 中说明了用Map保存图的顶点,那么如何把顶点对象添加到Map中呢?

 public void addVertex(T vertexLabel) {
//若顶点相同时,新插入的顶点将覆盖原顶点,这是由LinkedHashMap的put方法决定的
//每添加一个顶点,会创建一个LinkedList列表,它存储该顶点对应的邻接点,或者说是与该顶点相关联的边
vertices.put(vertexLabel, new Vertex(vertexLabel));//new Vertex 对象,会创建一个LinkedList,该LinkedList用来表示该顶点的邻接表
}

如何表示图中两个顶点之间的边呢?

 public boolean addEdge(T begin, T end, double edgeWeight) {
boolean result = false;
VertexInterface<T> beginVertex = vertices.get(begin);//获得表示边的起始顶点
VertexInterface<T> endVertex = vertices.get(end);//获得表示 边的终点 if(beginVertex != null && endVertex != null)
result = beginVertex.connect(endVertex, edgeWeight);//起始点与终点连接,即成一条边
if(result)
edgeCount++;
return result;//当添加重复边时会返回 false
}

3,图的相关算法的JAVA实现及分析

正如上一篇文章中的总结提到:算法的实现依赖于采用了何种数据结构,依赖于数据结构--图的具体实现。由于这里的数据结构--图的实现与《算法导论》中描述的图的数据结构有一点差别,如:没有定义表示图的访问状态的"白色顶点、灰色顶点、黑色顶点",因此算法的实现也与《算法导论》中算法的实现有轻微的差别。

对于不带权的图而言(边上没有权值) 广度优先遍历算法与最短路径算法很相似,对广度优先遍历算法稍加修改,就可以变成最短路径算法了。

可参考:无向图的最短路径算法JAVA实现带权图的最短路径算法(Dijkstra)实现

理解:

深度优先遍历算法与拓扑算法也很相似,拓扑排序算法的实现可以借助深度优先遍历算法。

理解:

具体参考《算法导论》

①广度优先遍历算法:若顶点A先于顶点B被访问,则顶点A的邻接点也先于顶点B的邻接点被访问。特点:先把起始顶点附近的顶点访问完,再访问远处的顶点。

在广度优先遍历算法的具体实现中,需要两个队列。一个辅助遍历,保存遍历过程中遇到的顶点,当访问完成了某个顶点A后,将A出队列,紧接着将A的所有邻接点都入队列,并访问

另一个队列用来保存访问的顺序,另一个队列的顶点入队顺序就是图的广度遍历顺序,因此,该队列保持 与 前一个队列的顶点入队操作 一致。由于前一个队列是辅助遍历的,它有出队的操作,它就不能记录整个顶点的访问序列了,因此才需要一个保存访问顺序的队列。当整个过程遍历完成后,将 保存访问顺序的队列 进行出队操作,即可得到整个图的广度优先遍历的顺序了。具体算法如下:

 public Queue<T> getBreadthFirstTraversal(T origin) {//origin 标识遍历的初始顶点
resetVertices();//将顶点的必要数据域初始化,复杂度为O(V)
Queue<VertexInterface<T>> vertexQueue = new LinkedList<>();//保存遍历过程中遇到的顶点,它是辅助遍历的,有出队列操作
Queue<T> traversalOrder = new LinkedList<>();//保存遍历过程中遇到的 顶点标识--整个图的遍历顺序就保存在其中,无出队操作
VertexInterface<T> originVertex = vertices.get(origin);//根据顶点标识获得初始遍历顶点
originVertex.visit();//访问该顶点
traversalOrder.offer(originVertex.getLabel());
vertexQueue.offer(originVertex); while(!vertexQueue.isEmpty()){
VertexInterface<T> frontVertex = vertexQueue.poll();//出队列,poll()在队列为空时返回null
Iterator<VertexInterface<T>> neighbors = frontVertex.getNeighborInterator();
while(neighbors.hasNext())//对于 每个顶点都遍历了它的邻接表,即遍历了所有的边,复杂度为O(E)
{
VertexInterface<T> nextNeighbor = neighbors.next();
if(!nextNeighbor.isVisited()){
nextNeighbor.visit();//广度优先遍历未访问的顶点
traversalOrder.offer(nextNeighbor.getLabel());
vertexQueue.offer(nextNeighbor);//将该顶点的邻接点入队列
}
}//end inner while
}//end outer while
return traversalOrder;
}

从中可以看出,该算法的时间复杂度为--遍历之前,给每个顶点进行初始化时需要遍历所有顶点V,在遍历过程中需要判断顶点的邻接点是否被遍历,也即遍历该顶点的邻接表,邻接表代表的实质是边,边总数为E,故总的时间复杂度为O(V+E),空间复杂度为O(V)--辅助队列的长度为顶点的长度

②最短路径算法:在边不带权值的图中求顶点A到顶点B的最短路径--其实就是顶点A到顶点B之间的最少边的条数

调用最短路径算法之前,首先要确定一个初始顶点,图中其他顶点的路径长度都是相对于初始顶点而言的。求两个顶点间最短路径,其实并不是找出两个顶点间所有的路径长度,然后取最小值。而是借助于广度优先遍历算法,将每个顶点相对于初始顶点的最短路径长度保存在 cost 属性中,广度优先算法的性质保证了顶点间的路径是最短的。在最短路径的计算中,设初始点为 i,顶点A相对于初始点的最短路径长度为 length,则 顶点A的邻接点 相对于初始顶点 i 的最短长度为 length+1.

因此,执行最短路径算法后,实际上求得了图中所有顶点相对于初始顶点的最短路径。

初始顶点的路径长度为0(每个顶点有一个 cost 属性---见上一文章分析,由 cost 来记录每个顶点相对于初始顶点的路径长度)。因此,获得某顶点的最短路径只需要调用它的getCost方法即可。

最短路径算法的代码如下,可以看出它和广度优先算法的代码非常的相似,其实就是广度优先算法的应用而已。

     public int getShortestPath(T begin, T end, Stack<T> path) {
resetVertices();//图中顶点的初始化
boolean done = false;//标记整个遍历过程是否完成
Queue<VertexInterface<T>> vertexQueue = new LinkedList<>();//辅助队列,保存遍历过程中遇到的顶点
VertexInterface<T> beginVertex = vertices.get(begin);//获得起始顶点
VertexInterface<T> endVertex = vertices.get(end);//获得终点,求起始顶点到终点的最短路径 beginVertex.visit();
vertexQueue.offer(beginVertex);//起始顶点入队列
//Assertion: resetVertices() 已经对 beginVertex 执行了 setCost(0) while(!done && !vertexQueue.isEmpty()){//while循环完成后,实际上求得了图中所有顶点相对于初始点的 cost 属性值
VertexInterface<T> frontVertex = vertexQueue.poll();
Iterator<VertexInterface<T>> neighbors = frontVertex.getNeighborInterator();
while(!done && neighbors.hasNext()){//计算 frontVertex的所有邻接顶点的 路径长度
VertexInterface<T> nextNeighbor = neighbors.next();
if(!nextNeighbor.isVisited()){
nextNeighbor.visit();
nextNeighbor.setPredecessor(frontVertex);//设置frontVertex 的前驱顶点
nextNeighbor.setCost(frontVertex.getCost() + 1);//该顶点的路径长度是 它的前驱顶点的路径长度+1
vertexQueue.offer(nextNeighbor);
}//end if if(nextNeighbor.equals(endVertex))
done = true;
}//end inner while
}//end outer while. and traverse over int pathLength = (int)endVertex.getCost();//初始顶点的 cost为 0,每个顶点的 cost 属性记录了它相对于初始顶点的最短长度
path.push(endVertex.getLabel()); VertexInterface<T> vertex = endVertex;
while(vertex.hasPredecessor()){
vertex = vertex.getPredecessor();
path.push(vertex.getLabel());
}
return pathLength;
}

③深度优先遍历算法:

在深度优先遍历中,需要两个栈,这里可以看出深度优先遍历带有递归的性质。一个栈用来辅助遍历,即用来保存遍历过程中里面的顶点,另一个栈用来保存遍历的顺序。之所以另外需要一个栈来保存遍历的顺序的原因 与 广度优先遍历 中需要用另一个队列来保存 遍历顺序 的原因相同。当深度优先遍历到某个顶点时,若该顶点的所有邻接点均已经被访问,则发生回溯,即返回去遍历 该顶点 的 前驱顶点 的 未被访问的某个邻接点。

深度优先遍历的代码与广度优先遍历的代码很大的一个不同就是,在while 循环里面,当取出栈顶/队头 顶点时,深度优先是用一个 if 语句 来执行逻辑,而广度优先 则是用一个 while 循环来执行逻辑。

这是因为:对于深度优先而言,访问了 顶点A 时,紧接着只需要找到 顶点A 的一个未被访问的邻接点,再访问该邻接点即可。而对于广度优先,访问了 顶点A 时,就是要寻找 顶点A的 所有未被访问的邻接点,再访问 所有的这些邻接点。

代码对比如下:

 while(!vertexStack.isEmpty()){
VertexInterface<T> topVertex = vertexStack.peek();
//找到该顶点的一个未被访问的邻接点,从该邻接点出发又去遍历邻接点的邻接点
VertexInterface<T> nextNeighbor = topVertex.getUnvisitedNeighbor();
if(nextNeighbor != null){
nextNeighbor.visit();
//由于用的是if,在这里push邻接点后,下一次while循环pop的是该邻接点,然后又获得它的邻接点,---DFS
vertexStack.push(nextNeighbor);
traversalOrder.offer(nextNeighbor.getLabel());
}
else
vertexStack.pop();//当某顶点的所有邻接点都被访问了时,直接将该顶点pop,这样下一次while pop 时就回溯到前一个顶点
 while(!vertexQueue.isEmpty()){
VertexInterface<T> frontVertex = vertexQueue.poll();//出队列,poll()在队列为空时返回null
Iterator<VertexInterface<T>> neighbors = frontVertex.getNeighborInterator();
while(neighbors.hasNext())//对于 每个顶点都遍历了它的邻接表,即遍历了所有的边,复杂度为O(E)
{
VertexInterface<T> nextNeighbor = neighbors.next();
if(!nextNeighbor.isVisited()){
nextNeighbor.visit();//广度优先遍历未访问的顶点
traversalOrder.offer(nextNeighbor.getLabel());
vertexQueue.offer(nextNeighbor);//将该顶点的邻接点入队列
}
}//end inner while
}//end outer while

整个深度优先遍历算法代码如下:

 public Queue<T> getDepthFirstTraversal(T origin) {
resetVertices();//先将所有的顶点初始化--时间复杂度为O(V)
LinkedList<VertexInterface<T>> vertexStack = new LinkedList<>();//辅助DFS递归遍历
Queue<T> traversalOrder = new LinkedList<>();//保存DFS遍历顺序 VertexInterface<T> originVertex = vertices.get(origin);//根据起始顶点的标识获得起始顶点
originVertex.visit();//访问起始顶点,起始顶点的出度不能为0(只考虑多于一个顶点的连通图),若为0,它就没有邻接点了
vertexStack.push(originVertex);//各个顶点的入栈顺序就是DFS的遍历顺序
traversalOrder.offer(originVertex.getLabel());//每当一个顶点入栈时,就将它入队列,从而队列保存了整个遍历顺序 while(!vertexStack.isEmpty()){
VertexInterface<T> topVertex = vertexStack.peek();
//找到该顶点的一个未被访问的邻接点,从该邻接点出发又去遍历邻接点的邻接点
VertexInterface<T> nextNeighbor = topVertex.getUnvisitedNeighbor();//判断所有未被访问的邻接点,也即遍历了所有的边--复杂度O(E)
if(nextNeighbor != null){
nextNeighbor.visit();
//由于用的是if,在这里push邻接点后,下一次while循环pop的是该邻接点,然后又获得它的邻接点,---DFS
vertexStack.push(nextNeighbor);
traversalOrder.offer(nextNeighbor.getLabel());
}
else
vertexStack.pop();//当某顶点的所有邻接点都被访问了时,直接将该顶点pop,这样下一次while pop 时就回溯到前一个顶点
}//end while
return traversalOrder;
}

深度优先遍历的算法的时间复杂度:O(V+E)--遍历之前,给每个顶点进行初始化时需要遍历所有顶点V,在遍历过程中需要判断顶点的邻接点是否被遍历,也即遍历该顶点的邻接表,邻接表代表的实质是边,边总数为 E,故总的时间复杂度为O(V+E);空间复杂度:O(V)--用了两个辅助栈

④拓扑排序算法

求图的拓扑序列的思路就是:先找到图中一个出度为0的顶点,访问该顶点并将之入栈。访问了该顶点之后,相当于指向该顶点的所有的边都已经被删除了。然后,继续在图中寻找下一个出度为0且未被访问的顶点,直至图中所有的顶点都已被访问。寻找这样的顶点的方法实现如下:

 private VertexInterface<T> getNextTopologyOrder(){//最坏情况下复杂度为O(V+E)
VertexInterface<T> nextVertex = null;
Iterator<VertexInterface<T>> iterator = vertices.values().iterator();//获得图的顶点的迭代器
boolean found = false;
while(!found && iterator.hasNext()){
nextVertex = iterator.next();
//寻找出度为0且未被访问的顶点
if(nextVertex.isVisited() == false && nextVertex.getUnvisitedNeighbor() == null)
found = true;
}
return nextVertex;
}

图的拓扑排序实现代码如下:

 public Stack<T> getTopologicalSort() {
/**
*相比于《算法导论》中的拓扑排序借助了DFS复杂度为O(V+E),该算法的时间复杂度较大
*因为算法导论中介绍的图的数据结构与此处实现的图的数据结构不同
*此算法的最坏时间复杂度为O(V*(V+E))==V * max{V,E}
*/
resetVertices();//先将所有的顶点初始化 Stack<T> vertexStack = new Stack<>();//存放已访问的顶点的栈,该栈就是一个拓扑序列
int numberOfVertices = vertices.size();//获得图中顶点的个数 for(int counter = 1; counter <= numberOfVertices; counter++){
VertexInterface<T> nextVertex = getNextTopologyOrder();//获得一个未被访问的且出度为0的顶点
if(nextVertex != null){
nextVertex.visit();
vertexStack.push(nextVertex.getLabel());//遍历完成后,出栈就可以获得图的一个拓扑序列
}
}
return vertexStack;
}

此拓扑排序算法实现的最坏情况下时间复杂度为:O(V*max(V,E));空间复杂度为:O(V)--定义一个辅助栈来保存遍历顺序

4,总结

本文实现了有向无环图及四个常用的图的遍历算法,在客户程序中只需要 new 一个图对象,然后就可以调用这些算法了。哈哈,以后可以用这个类来测试一些复杂的算法了。。。

在实现过程中让我明白了,数据结构与算法是紧密相关的,算法实现的难易程序及好坏依赖于你所设计的数据结构。

整个数据结构的学习至此为止告一段落了。在整个学习过程中,用JAVA语言把常用的数据结构数组、链表、栈、队列、树、词典、图都实现了一遍。感觉学到最多的是加深了对JAVA集合类库的理解和基本算法的理解(树的遍历算法和图的遍历算法)。

整个图的实现的JAVA完整代码下载(仅供学习)

数据结构--图 的JAVA实现(下)的更多相关文章

  1. 数据结构--图 的JAVA实现(上)

    1,摘要: 本系列文章主要学习如何使用JAVA语言以邻接表的方式实现了数据结构---图(Graph),这是第一篇文章,学习如何用JAVA来表示图的顶点.从数据的表示方法来说,有二种表示图的方式:一种是 ...

  2. 数据结构 -- 图的最短路径 Java版

    作者版权所有,转载请注明出处,多谢.http://www.cnblogs.com/Henvealf/p/5574455.html 上一篇介绍了有关图的表示和遍历实现.数据结构 -- 简单图的实现与遍历 ...

  3. 深入理解Java虚拟机--下

    深入理解Java虚拟机--下 参考:https://www.zybuluo.com/jewes/note/57352 第10章 早期(编译期)优化 10.1 概述 Java语言的"编译期&q ...

  4. Java环境下shiro的测试-认证与授权

    Java环境下shiro的测试 1.导入依赖的核心jar包 <dependency> <groupId>org.apache.shiro</groupId> < ...

  5. 图学java基础篇之集合工具

    两个工具类 java.utils下又两个集合相关_(准确来说其中一个是数组的)_的工具类:Arrays和Collections,其中提供了很多针对集合的操作,其中涵盖了一下几个方面: 拷贝.填充.反转 ...

  6. 小白学 Python(15):基础数据结构(集合)(下)

    人生苦短,我选Python 前文传送门 小白学 Python(1):开篇 小白学 Python(2):基础数据类型(上) 小白学 Python(3):基础数据类型(下) 小白学 Python(4):变 ...

  7. 数据结构与抽象 Java语言描述 第4版 pdf (内含标签)

    数据结构与抽象 Java语言描述 第4版 目录 前言引言组织数据序言设计类P.1封装P.2说明方法P.2.1注释P.2.2前置条件和后置条件P.2.3断言P.3Java接口P.3.1写一个接口P.3. ...

  8. 数据结构与算法 java描述 第一章 算法及其复杂度

    目录 数据结构与算法 java描述 笔记 第一章 算法及其复杂度 算法的定义 算法性能的分析与评价 问题规模.运行时间及时间复杂度 渐进复杂度 大 O 记号 大Ω记号 Θ记号 空间复杂度 算法复杂度及 ...

  9. <数据结构>图的最小生成树

    目录 最小生成树问题 Prim算法:点贪心 基本思想:类Dijstra 伪代码 代码实现 复杂度分析:O(VlogV + E) kruskal算法:边贪心 基本思想: 充分利用MST性质 伪代码 代码 ...

随机推荐

  1. HTTP数据组织方式

    HTTP网络传输中的数据组织方式有三种方式: 1.HTML方式 2.XML方式 3.JSON方式     XML介绍:XML称为可扩展标记语言,它与HTML一样,都是SGML(标准通用标记语言) XM ...

  2. WPFの实现word的缩放效果

    ms-word做出的效果令人十分欣喜,那么如何用wpf达到这个效果,下面我们来进行讨论. 界面上我用一个WrapPanel作为父级控件,动态添加InkCanvas作为子控件 <Grid> ...

  3. Spring配置文件中的那些标签意味着什么(持续更新)

    前言 在看这边博客时,如果遇到有什么不清楚的地方,可以参考我另外一边博文.Spring标签的探索,根据这边文章自己来深入源码一探究竟.这里自己只是简单记录一下各标签作用,每个人困惑不同,自然需求也不一 ...

  4. ASP 基础三 SQL指令

    一 增删改查 <% set conn=server.CreateObject("adodb.connection") DSNtemp="DRIVER={SQL Se ...

  5. Android学习之基础知识八—Android广播机制实践(实现强制下线功能)

    强制下线功能算是比较常见的了,很多的应用程序都具备这个功能,比如你的QQ号在别处登录了,就会将你强制挤下线.实现强制下线功能的思路比较简单,只需要在界面上弹出一个对话框,让用户无法进行任何操作,必须要 ...

  6. 源码分享篇:使用Python进行QQ批量登录

    直接上源码 1 #coding=utf-8 2 __author__ = 'Eagle' 3 import os 4 import time 5 import win32gui 6 import wi ...

  7. 每个大主播都是满屏弹幕,怎么做到的?Python实战无限刷弹幕!

    anmu 是一个开源的直播平台弹幕接口,使用他没什么基础的你也可以轻松的操作各平台弹幕.使用不到三十行代码,你就可以使用Python基于弹幕进一步开发.支持斗鱼.熊猫.战旗.全民.Bilibili多平 ...

  8. sprintf()函数用法

    sprintf()用法见操作手册:http://www.php.net/sprintf 简单写下format的用法: 1. + - 符号,数字 2. 填充字符 默认是空格,可以是0.如果其他字符填充, ...

  9. Removing Timezone from XMLGregorianCalendar

    1.去掉時間之後的“Z”或者修改時區 package Package0809; import javax.xml.datatype.DatatypeConfigurationException; im ...

  10. java使用POI读取excel报表

    留此作为记录 package com.demo; import java.io.FileInputStream; import java.util.Iterator; import org.apach ...