数据结构 -- 简单图的实现与遍历 (Java)
---恢复内容开始---
作者版权所有,转载请注明出处,多谢. http://www.cnblogs.com/Henvealf/p/5534071.html
前些天因为某些原因,就试着回想一下图(graph)的相关内容,没想脑子里一片空白,只怪当初没有好好听这门课.然后就学习了一下,这里做个小总结.
1.概念
简单图(simple graph):就是由一些顶点(V,vertice) 和 连接这些顶点的一些边(E,edge)所组成的结构,并且每对顶点之间只能存在一条边.所以通常会用G = (V,E)来表示一个简单图.简单图也被称为无向图(undirected graph).
说到无向图就一定有有向图(directed graph),有向图同样也用G = (V,E)来表示,都明白两种图的区别是什么,就不多说了.不过在这里说一些表示边时候的区别:
简单图的边使用{Vi,Vj}的方式表示,表示连接顶点i与顶点j的边.因为是简单图,所以有:
- {Vi,Vj} = {Vj,Vi,}
有向图的边使用(Vi,Vj)的方式表示.意思是当前边的方向是从顶点i到顶点j,所以很明显有:
- (Vi,Vj) ≠ (Vj,Vi)
2.图的表示
有很多方法可以表示一个图,不过思想上大致都一样.比如下面一个图(本文之后都会用这个图来作为例子):
图1
自己画的,或许丑了点:)
先看使用邻接表(adjacency list)来表示该图:
V0 | V1 V2 |
V1 | V0 V3 |
V2 | V0 V3 V4 |
V3 | V1 V2 V5 V6 |
V4 | V2 V7 |
V5 | V3 V6 |
V6 | V3 V5 |
V7 | V4 |
仔细观察就会发现,邻接表是在首列按顺序(正序倒序都可)列出所有顶点,然后第二行列出与顶点相邻的所有顶点,例如第一行,图中与V0相邻的顶点有V1与V2.所以第二列的内容便是 V1 与 V2.
另一种表示方法是使用邻接矩阵(adjacency matrix),思想与邻接表大致相同,不同的是邻接表是将与某一顶点相邻的顶点们列出,而邻接矩阵是将他们标注出。顾名思义就是使用矩阵表示,如在本例中,一共有8个顶点,所以此时邻接矩阵的大小就为8*8。如下:
V0 | V1 | V2 | V3 | V4 | V5 | V6 | V7 | |
V0 | oo | 1 | 1 | oo | oo | oo | oo | oo |
V1 | oo | oo | oo | 1 | oo | oo | oo | oo |
V2 | oo | oo | oo | 1 | 1 | oo | oo | oo |
V3 | oo | 1 | 1 | oo | oo | 1 | 1 | oo |
V4 | oo | oo | 1 | oo | oo | oo | oo | 1 |
V5 | oo | oo | oo | 1 | oo | oo | 1 | oo |
V6 | oo | oo | oo | 1 | oo | 1 | oo | oo |
V7 | oo | oo | oo | oo | 1 | oo | oo | oo |
观察可以发现,如果顶点i与顶点j之间存在边,就将矩阵的aij项设为1,其他情况就设为无穷大(oo)。公式就是:
{ 1 边(Vi ,Vj)存在
aij = {
{ 无穷大 其他情况
如果对于有向图,此公式就得改为:
{ 1 边{Vi ,Vj}存在
aij = {
{ 无穷大 其他情况
对于使用哪种表示法,这要取决与你所要处理的问题,如果你仅仅只是处理与某一顶点邻接的顶点,例如遍历,很明显使用表比使用矩阵所要步数要少很多。当你需要对图进行插入或者删除顶点的操作,就应该使用邻接矩阵,你只需要将矩阵上的数从0变为1,或者从1变为0即可。而使用邻接表,你还得要对表进行维护。
本例就选用邻接矩阵来表示图1,用二维数组就能直接实现,无穷大被换成了名为oo的变量.如下:
int oo = Integer.MAX_VALUE;
int[][] racs1 = new int[][]{
{oo, 1, 1,oo,oo,oo,oo,oo},
{ 1,oo,oo, 1,oo,oo,oo,oo},
{ 1,oo,oo, 1, 1,oo,oo,oo},
{oo, 1, 1,oo,oo, 1, 1,oo},
{oo,oo, 1,oo,oo,oo,oo, 1},
{oo,oo,oo, 1,oo,oo, 1,oo},
{oo,oo,oo, 1,oo, 1,oo,oo},
{oo,oo,oo,oo, 1,oo,oo,oo},
};
除此之外,这里还设定义了一个数组,用于为每个顶点添加一些信息.也就是各个顶点的名字.如果对与具体的问题,例如每个顶点代表地图上的一个城市,可使用像"北京","上海"城市名.当然可以很据特定情况将顶点的相关内容封装一下.
String[] verticeInfos1 = new String[] {
"V0","V1","V2","V3","V4","V5","V6","V7"
};
另:有一种图中有孤立顶点的情况,假设上面的图中有还有一个顶点V8,但它不与任何其他顶点相邻,则该怎么用表或者矩阵来表示那?就当留给大家的一个小问题。
3.图的遍历
3.1 先写一个名为Graph的类.用这个类来封装图的相关字段和遍历方法.先看类的字段与构造方法:
/**
* 使用邻接矩阵实现图<p>
* 深度优先遍历与广度优先遍历<p>
* 求最短路径:<p>
* 1. Dijkstra 算法 <p>
* 2. Ford 算法 <p>
* 3. 通用型的纠正标记算法<p>
* Created by Henvealf on 16-5-22.
*/
public class Graph<T> {
private int[][] racs; //邻接矩阵
private T[] verticeInfo; //各个点所携带的信息. private int verticeNum; //节点的数目,
private int[] visitedCount; //记录访问
private int[] currDist; //最短路径算法中用来记录每个节点的当前路径长度. public Graph(int[][] racs, T[] verticeInfo){
if(racs.length != racs[0].length){
throw new IllegalArgumentException("racs is not a adjacency matrix!");
}
if(racs.length != verticeInfo.length ){
throw new IllegalArgumentException ("Argument of 2 verticeInfo's length is error!");
}
this.racs = racs;
this.verticeInfo = verticeInfo;
verticeNum = racs.length;
visitedCount = new int[verticeNum];
}
//.....其他方法
}
这里使用了模板来模板化 verticeInfos.
还要说明的便是数组 int[] visitedCount; 其作用是为了标记图中的顶点是否被访问过. visitedCount[i] == 0 就说明顶点 i 还未被访问过,所以在进行遍历操作前需要初始化该数组全为0.方法如下:
/**
* 将记录访问的数组初始化为0
*/
private void initVisitedCount(){
for(int i = ; i < visitedCount.length; i ++){
visitedCount[i] = ;
}
}
3.2 图的遍历和树的遍历类似,分为深度优先遍历与广度优先遍历.
深度优先遍历,简单说就是先沿着一条路线最靠左或最靠右的路线往下走,并把路过的顶点标记为已访问.一直走到无路可走,或者下一站的顶点都被已经访问过了,就顺着刚才走过的路往回看(回溯),当发现回溯中的某一顶点还有其他未被访问过的分支的时候,就选择这个分支继续,重复上面的过程继续遍历.直到回溯到了遍历的出发点,就结束遍历.但如果检查发现图中还存在未被访问过的顶点,就说明图中还存在孤立与本图的分图.此时则任意选择一个未被访问过的顶点继续访问. 最后当图中所有的顶点都被访问过的时候,就说明遍历完成.
所以这里需要一个方法,用来判断图中顶点的访问情况:
/**
* 寻找没有被访问过的顶点.
* @return > 0 即为还未被访问过的顶点. -1 说明所有的节点都被访问过了.
*/
private int findNotVisited(){
for(int i = ; i < noteNum; i ++){
if(visitedCount[i] == ){
return i;
}
}
return -;
}
要寻找与顶点的相邻节点时,我们可以发现简单图的邻接矩阵以对角线对称,所以寻找相邻顶点的时候只需要遍历一半就可以,大大的提高了遍历的效率,不过对于有向图就需要遍历全图.这里只讨论简单图,有向图自行修改区分便可,这里是遍历全图:
/**
* 深度遍历的递归
* @param begin 从第几个节点开始遍历
*/
public void DFS(int begin, Queue<T> edges){
visitedCount[begin] = 1; //标记begin为已访问
edges.offer(verticeInfo[begin]); //加入记录队列
for(int a = 0; a < verticeNum; a++){ //遍历相邻的点
if((racs[begin][a] != Integer.MAX_VALUE)&& visitedCount[a] == 0){ //相邻的点未被访问过
DFS(a,edges);
}
}
} /**
* 开始深度优先遍历
* @return 返回保持有遍历之后的顺序的队列
*/
public Queue<T> depthFirstSearch(){
initVisitedCount(); //将记录访问次序的数组初始化为0
Queue<T> edges = new LinkedList<>(); //用于存储遍历过的点,用于输出
int begin = -1;
while((begin = findNotVisited()) != -1){ //不等于-1说明还有未访问过的点
DFS(begin,edges);
}
return edges;
}
广度优先遍历.与树的广度优先遍历相似,就是逐层遍历.代码如下:
/**
* 广度优先遍历
* @return 返回保持有遍历之后的顺序的队列
*/
public Queue<T> breadthFirstSearch(){
initVisitedCount(); //将记录访问次序的数组初始化为0
Queue<Integer> tallyQueue = new LinkedList<>(); //初始化队列
Queue<T> edges = new LinkedList<>(); //用于存储遍历过的点,用于输出
int nowVertice = -1; //当前所在的点
while((nowVertice = findNotVisited()) != -1){ //寻找还未被访过问的点
visitedCount[nowVertice] = 1; //设置访问标记
edges.offer(verticeInfo[nowVertice]);
tallyQueue.offer(nowVertice); //将当前孤立部分一个顶点加入记录队列中
while(!tallyQueue.isEmpty()){ //只要队列不为空
nowVertice = tallyQueue.poll(); //取出队首的节点
for(int a = 0; a < verticeNum; a++){ //遍历所有和nowVertice相邻的节点
if((racs[nowVertice][a] != Integer.MAX_VALUE) && visitedCount[a] == 0) { //没有访问过
visitedCount[a] = 1; //记为标记过
tallyQueue.offer(a); //加入队列,上面会继续取出.来遍历
edges.offer(verticeInfo[a]); //记录
}
}
}
}
return edges;
}
这里需要两个队列,一个队列用于存储遍历过程,另一个用于保存第n层被遍历的顺序,等到遍历第n+1层的时候,取出的顶点就是按照上层的顺序来排列,也就能按照上层的顺序来继续遍历.
下面是Graph完整代码:
package com.henvealf.datastructures.graph.arcs; import java.util.*; /**
* 使用邻接矩阵实现图<p>
* 深度优先遍历与广度优先遍历<p>
* 求最短路径:<p>
* 1. Dijkstra 算法 <p>
* 2. Ford 算法 <p>
* 3. 通用型的纠正标记算法<p>
* Created by Henvealf on 16-5-22.
*/
public class Graph<T> {
private int[][] racs; //邻接矩阵
private T[] verticeInfo; //各个点所携带的信息. private int verticeNum; //节点的数目,
private int[] visitedCount; //记录访问
private int[] currDist; //最短路径算法中用来记录每个节点的当前路径长度. public Graph(int[][] racs, T[] verticeInfo){
if(racs.length != racs[0].length){
throw new IllegalArgumentException("racs is not a adjacency matrix!");
}
if(racs.length != verticeInfo.length ){
throw new IllegalArgumentException ("Argument of 2 verticeInfo's length is error!");
}
this.racs = racs;
this.verticeInfo = verticeInfo;
verticeNum = racs.length;
visitedCount = new int[verticeNum];
} /**
* 深度遍历的递归
* @param begin 从第几个节点开始遍历
*/
public void DFS(int begin, Queue<T> edges){
visitedCount[begin] = 1; //标记begin为已访问
edges.offer(verticeInfo[begin]); //加入记录队列
for(int a = 0; a < verticeNum; a++){ //遍历相邻的点
if((racs[begin][a] != Integer.MAX_VALUE)&& visitedCount[a] == 0){ //相邻的点未被访问过
DFS(a,edges);
}
}
} /**
* 开始深度优先遍历
* @return 返回保持有遍历之后的顺序的队列
*/
public Queue<T> depthFirstSearch(){
initVisitedCount(); //将记录访问次序的数组初始化为0
Queue<T> edges = new LinkedList<>(); //用于存储遍历过的点,用于输出
int begin = -1;
while((begin = findNotVisited()) != -1){ //不等于-1说明还有未访问过的点
DFS(begin,edges);
}
return edges;
} /**
* 广度优先遍历
* @return 返回保持有遍历之后的顺序的队列
*/
public Queue<T> breadthFirstSearch(){
initVisitedCount(); //将记录访问次序的数组初始化为0
Queue<Integer> tallyQueue = new LinkedList<>(); //初始化队列
Queue<T> edges = new LinkedList<>(); //用于存储遍历过的点,用于输出
int nowVertice = -1; //当前所在的点
while((nowVertice = findNotVisited()) != -1){ //寻找还未被访过问的点
visitedCount[nowVertice] = 1; //设置访问标记
edges.offer(verticeInfo[nowVertice]);
tallyQueue.offer(nowVertice); //将当前孤立部分一个顶点加入记录队列中
while(!tallyQueue.isEmpty()){ //只要队列不为空
nowVertice = tallyQueue.poll(); //取出队首的节点
for(int a = 0; a < verticeNum; a++){ //遍历所有和nowVertice相邻的节点
if((racs[nowVertice][a] != Integer.MAX_VALUE) && visitedCount[a] == 0) { //没有访问过
visitedCount[a] = 1; //记为标记过
tallyQueue.offer(a); //加入队列,上面会继续取出.来遍历
edges.offer(verticeInfo[a]); //记录
}
}
}
}
return edges;
} /**
* 寻找没有被访问过的顶点.
* @return > 0 即为还未被访问过的顶点. -1 说明所有的节点都被访问过了.
*/
private int findNotVisited(){
for(int i = 0; i < verticeNum; i ++){
if(visitedCount[i] == 0){
return i;
}
}
return -1;
} /**
* 将记录访问的数组初始化为0
*/
private void initVisitedCount(){
for(int i = 0; i < visitedCount.length; i ++){
visitedCount[i] = 0;
}
}
}
下面测试类:
package com.henvealf.datastructures.graph.arcs; import java.util.Queue; /**
* 图的测试类
* Created by Henvealf on 16-5-22.
*/
public class Main { public static void main(String[] args) { int[][] racs = new int[][]{
{0,1,0,1,0,},
{1,0,1,0,1,},
{0,1,0,1,1,},
{1,0,1,0,0,},
{0,1,1,0,0,},
};
int oo = Integer.MAX_VALUE;
int[][] racs1 = new int[][]{
{oo, 1, 1,oo,oo,oo,oo,oo},
{ 1,oo,oo, 1,oo,oo,oo,oo},
{ 1,oo,oo, 1, 1,oo,oo,oo},
{oo, 1, 1,oo,oo, 1, 1,oo},
{oo,oo, 1,oo,oo,oo,oo, 1},
{oo,oo,oo, 1,oo,oo, 1,oo},
{oo,oo,oo, 1,oo, 1,oo,oo},
{oo,oo,oo,oo, 1,oo,oo,oo},
}; String[] verticeInfos1 = new String[] {
"V0","V1","V2","V3","V4","V5","V6","V7"
};
Graph<String> graph = new Graph<>(racs2,verticeInfos2);
Queue<String> dr = graph.depthFirstSearch();
Queue<String> br = graph.breadthFirstSearch(); System.out.println("--遍历");
System.out.println("----深度优先结果: " + dr);
System.out.println("----广度优先结果: " + br);
}
不足之处,请多多指出,感激不尽.
End... By Henvealf/自安
数据结构 -- 简单图的实现与遍历 (Java)的更多相关文章
- 数据结构------------------二叉查找树(BST)的java实现
数据结构------------------二叉查找树(BST)的java实现 二叉查找树(BST)是一种能够将链表插入的灵活性和有序数组查找的高效性相结合的一种数据结构.它的定义如下: 二叉查找树是 ...
- 循环遍历Java字符串字符的规范方法——类似python for ch in string
比如我将string作为CNN 文本处理输入: float [] input = new float[maxLength]; // 1 sentence by maxLenWords // int[] ...
- 【数据结构】之顺序表(Java语言描述)
之前总结过使用C语言描述的顺序表数据结构.在C语言类库中没有为我们提供顺序表的数据结构,因此我们需要自己手写,详细的有关顺序表的数据结构描述和C语言代码请见[我的这篇文章]. 在Java语言的JDK中 ...
- 【算法与数据结构】二叉搜索树的Java实现
为了更加深入了解二叉搜索树,博主自己用Java写了个二叉搜索树,有兴趣的同学可以一起探讨探讨. 首先,二叉搜索树是啥?它有什么用呢? 二叉搜索树, 也称二叉排序树,它的每个节点的数据结构为1个父节点指 ...
- 数据结构(一) 单链表的实现-JAVA
数据结构还是很重要的,就算不是那种很牛逼的,但起码得知道基础的东西,这一系列就算是复习一下以前学过的数据结构和填补自己在这一块的知识的空缺.加油.珍惜校园中自由学习的时光.按照链表.栈.队列.排序.数 ...
- 数据结构之单链表的实现-java
一.单链表基本概念 单链表是一种链式存取的数据结构,用一组地址任意的存储单元(一般是非连续存储单元)存放线性表中的数据元素.链表中的数据是以结点来表示的,每个结点的构成:元素data + 指针next ...
- 邻接表的广度优先遍历(java版)
到 0 的权是 91 到 2 的权是 31 到 3 的权是 61 到 4 的权是 7 2 到 0 的权是 22 到 3 的权是 5 3 到 0 的权是 33 到 4 的权是 1 4 到 2 的权是 2 ...
- 邻接矩阵的深度优先遍历(java版)
这是一个有向边带权的图 顶点数组:[v0, v1, v2, v3, v4] 边数组: v0 v1 v2 v3 v4 v0 6 v1 9 3 v2 2 5 v3 1 v4 package com.dat ...
- 数据结构之计算器的实现(JAVA)(四)
原理: 1.将中序表达式变化兴许表达式 2.当前字符为数字,将该数字放入栈中 3.当前字符为操作符,从栈中取出两个树,依据操作符来运算,将运算结果放入到栈中 4.反复,直到将字符操作完.此时栈中仅仅剩 ...
随机推荐
- 如何从List<T>中筛选符合条件的数据的集合或个数
方法一:Linq ChannelList就是一个List类型的数据,IsOpen 是其元素的属性 channelCount = (from channel in DevicesManager.Inst ...
- HeadFirst 13 (包装器, 过滤器) not Finish
过滤器准许你拦截请求 容器管理过滤器的生命周期 都在DD中声明
- Redis的过滤器(SCAN)功能
在写另一篇文章( link )的时候,涉及到过滤器(filter)功能.以前没有接触过,整理如下. 主要参考这两篇: http://blog.csdn.net/u011510825/article/d ...
- android 开发如何做内存优化
不少人认为JAVA程序,因为有垃圾回收机制,应该没有内存泄露.其实如果我 们一个程序中,已经不再使用某个对象,但是因为仍然有引用指向它,垃圾回收器就无法回收它,当然该对象占用的内存就无法被使用,这就造 ...
- UVa 10891 (博弈+DP) Game of Sum
最开始的时候思路就想错了,就不说错误的思路了. 因为这n个数的总和是一定的,所以在取数的时候不是让自己尽可能拿的最多,而是让对方尽量取得最少. 记忆化搜索(时间复杂度O(n3)): d(i, j)表示 ...
- 51nod1204 Parity
如果sm[j]和sm[i]奇偶性相同,那么(i+1,j)个数为偶数如果奇偶性相同看成是朋友,不同的看成是敌人,那么就跟bzoj1370的做法差不多了. 如果奇偶性相同,就将x和y合并,x+n,y+n合 ...
- PHP学习笔记02——简易计算器
<!DOCTYPE html> <html> <head> <title>PHP简易计算器</title> </head> &l ...
- Typed Message模式与Event Sourcing
引言 在<设计模式沉思录>(Pattern Hatching: Design Patterns Applied,[美]JohnVlissides著)一书的第4章中,围绕事件Message传 ...
- php 获取指定日期所在月份的最后一天
本文引用来自 http://hi.baidu.com/yflife/item/fd00ef142c5967fcdceeca84 php 获取指定月最后一天: <?phpfunction gett ...
- K2 Blackpearl开发技术要点(Part1)
转:http://www.cnblogs.com/dannyli/archive/2012/09/14/2685260.html K2 Blackpearl开发技术要点(Part1) 预知后事如何,请 ...