目录java
1、图的实现与应用-1node
2、图的实现与应用-2git
5、总结数据结构
6、参考资料函数
VertexNode
类和TripleEdge
类这两个类分别表示图的顶点和边。
VertexNode
类包含一个int类型变量key,表示顶点序号,T类型变量element,表示该顶点储存的元素。TripleEdge
类包含一个int类型变量isExist,表示这条边是否存在,为0时不存在,为1时不存在;一个int类型变量weight,表示边的权重,初始默认为1。为何不加上边的开始顶点和结束顶点的序号呢?这个问题在后面解释测试
MatrixGraph
类方法实现this
首先咱们用Arraylist来保存顶点,用一个二维数组matrix来保存边,这里解释下以前的为何不加上边的开始顶点和结束顶点的序号,由于matrix[i][j]就表示序号为i的点到序号为j的点之间的边,由于是无向图,因此在添加matrix[i][j]的同时也要添加matrix[j][i],这个在以后的实现insertEdge()方法时会再讲。而后构造函数先实例化ArrayList和二维数组,数组大小先暂定50spa
//count变量表示图中顶点的个数 private int count, max; //表示邻接矩阵的二维数组的初始大小 private final int SIZE = 50; //保存顶点结点 private ArrayList<VertexNode<T>> vertexList; //用二维数组表示邻接矩阵 private TripleEdge matrix[][]; //构造函数,初始化保存顶点的列表和表示邻接矩阵的二位数组 public MatrixGraph() { vertexList = new ArrayList<>(); matrix = new TripleEdge[SIZE][SIZE]; }
而后先实现最简单的size()和isEmpty()方法,这个就很少解释了
//返回图中顶点个数 public int size() { return count; } //返回图是否为空 public boolean isEmpty() { return (count == 0); }
//添加顶点,index为顺序下标,x为结点元素 public void addVertex(int index,T x) { vertexList.add(new VertexNode<>(index,x)); count++; if(index>=max) max=index+1; if(matrix.length == index) expandCapacity(); for(int i = 0; i< max; i++) { if(!(getVertex(i)==null)) { matrix[index][i] = new TripleEdge(); matrix[i][index] = new TripleEdge(); } } }
解释一下上面的代码。传入顶点下标和顶点元素,将其加入vertexlList中,其中有一个max的整型变量,有很重要的做用,和count不一样,count在删除顶点时会自减1,而max不会。设置这个max变量也是为了实现后面的方法可以更方便。同时也不是每次都自加1,而是当index>=max时把index加1的值赋给max(由于index从0开始,而max是从1开始)。我这里整体说一下个人构想。假设咱们初始化下面这样一个二维数组来表示邻接矩阵,初始长度为50,那么初始化的时候每一个元素都是null,代表此时图为空,对应的顶点也为空
接下来咱们添加元素,添加第一个元素时,把二维数组matrix[0][0]初始化为new TripleEdge(),就代表下标为0的顶点已经存在了,而此时matrix[0][0]的isExist是0,表明从0到0的边不存在。而后再添加一个顶点也是相似的,添加序号为i的顶点,那么相应的矩阵的第i行和第i列都要置为new TripleEdge()。若是要删除顶点,假设删除顶点的下标为i,那么矩阵的第i行和第i列元素都置为null,这样与之相关的边也删除了。总的来讲,每一个matrix中的元素都有三种状态(不考虑权重,下同),null表示边和相应的顶点都不存在,0表示顶点存在可是边不存在,1表示两个都存在。接下来咱们假设0~4的顶点都添加好了,也添加了一些边,此时的max就是5,若是删去下标为2的顶点,max仍是5,此时要再次添加下标为2的顶点,那么max仍是5,但若是添加的元素下标是7呢?那此时max就变成8了。我把max理解为咱们有效利用二维数组的最大长度。由于当max为5时,后面的行和列都是null,至关于不存在,没有用到。那当咱们添加的顶点下标大于50怎么办?,那就要扩容
//对二维数组扩容 private void expandCapacity() { TripleEdge larger[][] = new TripleEdge[matrix.length*2][matrix.length*2]; for(int i=0;i<matrix.length;i++) for (int j=0;j<matrix.length;j++) larger[i][j] = matrix[i][j]; matrix = larger; }
扩容以后有一个for循环,有什么用呢?它的做用是初始化边。好比你添加了下标为2的顶点,那么matrix[0][2]~matrix[2][2]和matrix[2][0]~matrix[2][2]的元素都要初始化,但也是有条件的,若是这些元素中有的顶点不存在呢?因此加一个
if(!(getVertex(i)==null))
,这样就能够在其中随意添加和删除顶点了。
//删除下标为index的顶点 public void removeVertex(int index) throws EmptyCollectionException { //若是为空,返回异常 if(isEmpty()) throw new EmptyCollectionException("graph"); //若是该顶点不存在,返回异常 else if(getVertex(index) == null) throw new ElementNotFoundException("graph"); else { for (int i = 0; i < vertexList.size(); i++) if (vertexList.get(i).getKey() == index) vertexList.remove(i); //删除顶点后,把与其相关的边置为null for (int i = 0; i < max; i++) { matrix[index][i] = null; matrix[i][index] = null; } count--; } }
这个比较简单,在vertexList中找到下标为index的元素,将其删除,而后把与之相关的边置为null,这里的循环就要用到max了。你们能够本身画个矩阵看看为何是max
//添加从下标为startIndex到下标为endIndex的顶点之间的边 public void insertEdge(int startIndex, int endIndex) { matrix[startIndex][endIndex] = new TripleEdge(1); matrix[endIndex][startIndex] = new TripleEdge(1); }
这里直接把相应的边初始化,把isExist变量设置为1,就表明边存在了。这里为何不扩容呢?由于添加边以前确定要先添加相应的顶点的,而在添加顶点的时候就已经扩容了(若是有必要的话),因此在这里就不须要了
//添加带有权重的边 public void insertEdge(int startIndex, int endIndex, int weight) { matrix[startIndex][endIndex] = new TripleEdge(1,weight); matrix[endIndex][startIndex] = new TripleEdge(1,weight); }
我还顺便实现了一个添加带有权重的边,不过这个实验用不到,就很少说了
//删除下标为startIndex到下标为endIndex的顶点之间的边 public void removeEdge(int startIndex, int endIndex) { if((matrix[startIndex][endIndex] == null)|| (matrix[startIndex][endIndex].getIsExist() == 0)) { System.out.println("This edge isn't exist!"); } else { matrix[startIndex][endIndex] = new TripleEdge(); matrix[endIndex][startIndex] = new TripleEdge(); } }
删除的话就把相应的边的isExist置为0,即从新初始化。
//返回图的邻接矩阵表示 public String toString() { String result ="顶点下标 "; for(int i=0;i<max;i++) if(!(matrix[i][i] == null)) result += i+" "; result += "\n"; for (int i=0;i<max;i++) { if(!(matrix[i][i]==null)) result += " "+i+" "; for (int j = 0; j < max; j++) { TripleEdge edge = matrix[i][j]; if (!(edge == null)) result += edge.getIsExist()*edge.getWeight()+ " "; } if (!(matrix[i][max-1] == null)) result += "\n"; } return result; }
把邻接矩阵打印出来,遇到为null的不打印,不为null的获取它的isExist*weight的值,咱们这里weight默认为1,没什么影响。
//返回无向图的广度优先迭代器 public Iterator<T> iteratorBFS(int startIndex) { int currentVertex; LinkedQueue<Integer> traversalQueue = new LinkedQueue<>(); ArrayIterator<T> iter = new ArrayIterator<>(); if(!indexIsValid(startIndex)) return iter; boolean visited[] = new boolean[max]; for(int i=0;i<max;i++) visited[i] = false; traversalQueue.enqueue(startIndex); visited[startIndex] = true; while(!traversalQueue.isEmpty()){ currentVertex = traversalQueue.dequeue(); iter.add(getVertex(currentVertex).getElement()); for (int i=0;i<max;i++) if(!(matrix[currentVertex][i] == null)) if(!visited[i]&& (matrix[currentVertex][i].getIsExist()==1)) { traversalQueue.enqueue(i); visited[i] = true; } } return iter; }
算法书上已经讲过了,就是按照书上的来,只不过一些if的条件和循环的条件改为了与我写的代码相适应的条件,原理是同样的。其中indexIsValid方法是本身实现的
//判断下标是否有效 private boolean indexIsValid(int index) { boolean valid = false; for(int i=0;i<vertexList.size();i++) if(vertexList.get(i).getKey()==index) valid = true; return valid; }
//返回无向图的深度优先迭代器 public Iterator<T> iteratorDFS(int startIndex) { int currentVertex; LinkedStack<Integer> traversalStack = new LinkedStack<>(); ArrayIterator<T> iter = new ArrayIterator<>(); boolean visited[] = new boolean[max]; boolean found; if(!indexIsValid(startIndex)) return iter; for(int i=0;i<max;i++) visited[i] = false; iter.add(getVertex(startIndex).getElement()); visited[startIndex] = true; while (!(iter.size() == count)) { traversalStack.push(startIndex); while (!traversalStack.isEmpty()) { currentVertex = traversalStack.peek(); found = false; for (int i = 0; i < max && !found; i++) { if (!(matrix[currentVertex][i]==null)&& (matrix[currentVertex][i].getIsExist() == 1) && !visited[i]) { traversalStack.push(i); iter.add(getVertex(i).getElement()); visited[i] = true; found = true; } if (!found && !traversalStack.isEmpty()) { traversalStack.pop(); } } } } return iter; }
深度优先同广度优先,也是稍微修改一下书上的代码便可。
在MatrixGraph中我还实现了getVertex和getEdge方法,用来得到顶点和边,比较简单,是用来辅助其余方法实现的
//返回下标为index的顶点,若是图为空,返回EmptyCollectionException public VertexNode<T> getVertex(int index) throws EmptyCollectionException { if(isEmpty()) throw new EmptyCollectionException("graph"); VertexNode<T> result = null; for (int i=0;i<vertexList.size();i++) { if(vertexList.get(i).getKey()==index) { result = vertexList.get(i); } } return result; }
//返回下标为startIndex到下标为endIndex的顶点之间的边 public TripleEdge getEdge(int startIndex, int endIndex) { return matrix[startIndex][endIndex]; }
改用十字链表实现无向图,那么顶点类和边类就要从新定义了。按照上图来从新定义,首先是LinkedVertex类。包含顶点下标,顶点元素,入弧和出弧
package ExpFour; /** * Created by Administrator on 2017/11/22. */ public class LinkedVertex<T> { private int key; private T element; private LinkedEdge<T> in, out; public LinkedVertex(int key,T element) { this.key = key; this.element = element; in = null; out = null; } public int getKey() { return key; } public void setKey(int key) { this.key = key; } public T getElement() { return element; } public void setElement(T element) { this.element = element; } public LinkedEdge<T> getIn() { return in; } public void setIn(LinkedEdge<T> in) { this.in = in; } public LinkedEdge<T> getOut() { return out; } public void setOut(LinkedEdge<T> out) { this.out = out; } public String toString() { return "("+key+","+element+")"; } }
而后是LinkedEdge类,包含弧尾下标,弧头下标,同弧头和同弧尾
package ExpFour; /** * Created by Administrator on 2017/11/22. */ public class LinkedEdge<T> { private int headIdx, tailIdx; private LinkedEdge<T> sameHead, sameTail; public LinkedEdge(int tailIdx,int headIdx) { this.tailIdx = tailIdx; this.headIdx = headIdx; sameHead = null; sameTail = null; } public LinkedEdge<T> getSameHead() { return sameHead; } public int getHeadIdx() { return headIdx; } public void setHeadIdx(int headIdx) { this.headIdx = headIdx; } public int getTailIdx() { return tailIdx; } public void setTailIdx(int tailIdx) { this.tailIdx = tailIdx; } public void setSameHead(LinkedEdge<T> sameHead) { this.sameHead = sameHead; } public LinkedEdge<T> getSameTail() { return sameTail; } public void setSameTail(LinkedEdge<T> sameTail) { this.sameTail = sameTail; } public String toString() { return "( 弧尾序号:"+ tailIdx +",弧头序号:"+ headIdx +")"; } }
方法实现
这几个比较简单,和以前差很少,就很少说了
public class LinkedGraph<T> { private int count; private ArrayList<LinkedVertex<T>> vertexList; private ArrayList<LinkedEdge<T>> edgesList; public LinkedGraph() { vertexList = new ArrayList<>(); edgesList = new ArrayList<>(); } public int size() { return count; } public boolean isEmpty() { return (count == 0); }
这个添加方法比邻接矩阵的简单,直接加到vertexList中便可
//添加下标为index的顶点 public void addVertex(int index,T e) { vertexList.add(new LinkedVertex<>(index,e)); count++; }
原理和以前同样,删除顶点的同时要删除相关的边
//删除下标为index的顶点 public LinkedVertex<T> removeVertex(int index) { if(isEmpty()) throw new EmptyCollectionException("graph"); LinkedVertex<T> result = null; for(int i=0;i<vertexList.size();i++) if (vertexList.get(i).getKey() == index) result = vertexList.remove(i); for(int i=0;i<edgesList.size();i++) { LinkedEdge<T> edge = edgesList.get(i); if((edge.getHeadIdx()==index)||(edge.getTailIdx()==index)) { edgesList.remove(i); i--; } } count--; return result; }
在edgesList 中找到弧头序号为index或弧尾序号为index的边,将其删除便可
添加边的方法有些不一样,添加边后,要找到对应的弧头和弧尾,使其指向这条边。同时,在edgesList中找与其同弧头或同弧尾的边,若是该边的同弧头或同弧尾为空,就指向这条新插入的边。同时,由于是表示无向图,因此用户在添加i到j的边时还要同时添加j到i的边
//插入边 public void insertEdge(int tailIdx,int headIdx) { LinkedEdge<T> edge = new LinkedEdge<>(tailIdx,headIdx); if(!edgesList.isEmpty()) { for(int i=0;i<edgesList.size();i++) { if((edgesList.get(i).getSameHead()==null)&& (edgesList.get(i).getHeadIdx()==edge.getHeadIdx())) edge.setSameHead(edgesList.get(i)); if((edgesList.get(i).getSameTail()==null)&& (edgesList.get(i).getTailIdx()==edge.getTailIdx())) edge.setSameTail(edgesList.get(i)); } } edgesList.add(edge); for (int i=0;i<vertexList.size();i++) { LinkedVertex<T> vertex = vertexList.get(i); if((vertex.getKey() == headIdx)&& (vertex.getIn()==null)) { vertex.setIn(edge); vertexList.set(i, vertex); } if((vertex.getKey() == tailIdx)&& (vertex.getOut()==null)) { vertex.setOut(edge); vertexList.set(i, vertex); } } }
删除边时要同时删除两条,i到j和j到i
//删除边 public void removeEdge(int tailIdx,int headIdx) { for (int i = 0; i < edgesList.size(); i++) { LinkedEdge<T> edge = edgesList.get(i); if (((edge.getTailIdx() == tailIdx) && (edge.getHeadIdx() == headIdx)) || ((edge.getTailIdx() == headIdx) && (edge.getHeadIdx() == tailIdx))) edgesList.remove(i); } }
先打印一个顶点,在该顶点后面打印以该顶点为弧尾的边
//返回图的字符串形式 public String toString() { String result = "顶点 以该顶点为弧尾的边\n"; for(int i=0;i<count;i++) { LinkedVertex<T> vertex = vertexList.get(i); result += vertex.toString()+" "; for(int j=0;j<edgesList.size();j++) { LinkedEdge<T> edge = edgesList.get(j); if(vertex.getKey() == edge.getTailIdx()) { result += edge.toString()+" "; } } result += "\n"; } return result; }
看似难,其实很简单。和邻接矩阵实现的原理同样,只不过它得到与顶点相连的边的方式不一样,也就是把if和for循环的一些条件改一改就好了,代码比较多,就不贴出来了,能够在后面的代码连接里看
其实这是一个最短路径的问题,图类能够不用写了,由于以前已经写好了,并且在邻接矩阵实现的图里我实现了一个添加带权重的边的方法,因此我只需在MatrixGraph类中实现一个得到最短路径的方法就好了。核心算法是Floyd算法,接下来的代码实现都是在MatrixGraph类的基础上作的
其实老师在课上讲的我并无彻底理解,因此课后我又到网上查了一下Floyd算法的相关内容和一些代码的实现,经过这些参考才完成这个实验
目录
要用Floyd算法,首先要获得这个图的带权矩阵,这个方法就是实现此功能。我以前在第一个实验里说过,MatrixGraph类里有一个二维数组matrix用来存边,可是若是咱们对图进行了删除点的操做,那么这个数组里的一些元素是null,不能直接拿来用,因此我须要稍微改一下。
public int[][] getWeightMatrix() { int weight[][] = new int[count][count]; int a=0; for(int i=0;i<max;i++) { if (!(getVertex(i)==null)) { int b=0; for (int j=0;j<max;j++) { if (!(getVertex(j)==null)) { weight[a][b] = matrix[i][j].getIsExist()*matrix[i][j].getWeight(); b++; } } a++; } } return weight; }
首先,获得的二维数组是整型,而不是TripleEdge类型;数组的长度是count而不是max或者其它。而后开始循环,对matrix中的元素进行判断,若是它不为null,则把它的isExist*weight获得的值赋给weight[a][b],同时b++;若是matrix中的元素为空,则直接跳过,不进行任何操做,a、b也不会自加1,最终获得的weight就是图的带权二维数组
这个是最主要的方法,能够获得最短路径和最小权值的矩阵(用数组储存)。用二位数组length存储从i点到j点的最小权重(在这个实验里是最小费用),path是一个三维数组,path[i][j][]中储存从i点到j点通过的顶点,由于可能不止一个,因此也用数组储存。
public void floyd() { int data[][]=getWeightMatrix(); int MAX = 100; // 储存中间点 int[][] spot = new int[count][count]; int [] onePath = new int[count]; length = new int[count][count]; path = new int[count][count][]; // 初始化图中两点间的路径 for (int i = 0; i < count; i++) for (int j = 0; j < count; j++) { // 没有路径的两个点之间的路径为默认最大 if (data[i][j] == 0) data[i][j] = MAX; if (i == j) data[i][j] = 0; } for (int i = 0; i < count; i++)// 初始化为任意两点之间没有路径 for (int j = 0; j < count; j++) spot[i][j] = -1; for (int i = 0; i < count; i++)// 假设任意两点之间的没有路径 onePath[i] = -1; for (int v = 0; v < count; v++) for (int w = 0; w < count; w++) length[v][w] = data[v][w]; for (int u = 0; u < count; u++) for (int v = 0; v < count; v++) for (int w = 0; w < count; w++) // 若是存在更短路径则取更短路径 if (length[v][w] > length[v][u] + length[u][w]) { length[v][w] = length[v][u] + length[u][w]; spot[v][w] = u;// 把通过的点加入 } for (int i = 0; i < count; i++) {// 求出全部的路径 int[] point = new int[1]; for (int j = 0; j < count; j++) { point[0] = 0; onePath[point[0]++] = i; outputPath(spot, i, j, onePath, point); path[i][j] = new int[point[0]]; for (int s = 0; s < point[0]; s++) path[i][j][s] = onePath[s]; } } }
这两个方法比较简单,直接获取length数组和path数组相应位置的元素就行了
public int getMinWeight(int startIdx,int endIdx) { return length[startIdx][endIdx]; } public String getMinPath(int startIdx,int endIdx) { String result = "From " + startIdx + " to " + endIdx + " path is: "; for (int k = 0; k < path[startIdx][endIdx].length; k++) result += path[startIdx][endIdx][k] + " "; return result; }
一个private的方法,用来获得路径
private void outputPath(int[][] spot, int i, int j, int[] onePath, int []point) { // 输出i// 到j// 的路径的实际代码,point[]记录一条路径的长度 if (i == j) return; if (spot[i][j] == -1) onePath[point[0]++] = j; else { outputPath(spot, i, spot[i][j], onePath, point); outputPath(spot, spot[i][j], j, onePath, point); } }
第一个图是以前作的,只打印出了最小费用的矩阵,因此云班课里之提交了这个。后来补充完整了,加上了最短路径,后面的图是测试结果
我感受本次实验比较特殊,由于前面的数据结构实验,书上都有代码,本身只需补充几个方法就好,但此次基本全是本身写,因此遇到的问题也比较多。由于有不少问题,有大也有小,并且不少是相互关联的,因此我以为单独写出来不是很好主要是当时遇到太多问题,只想着解决,没有记下来,其实大部分我遇到的问题和解决办法,在写实验过程的时候也或多或少嵌入进去了,因此这里就不单独写了。
此次实验主要是本身用代码实现图。前一周课上讲的理论知识,都理解了,并且课上的测试,本身画出几种图,也都没问题,但真正到了本身实现的时候才发现没有那么简单。有不少嘴上说的容易的实现起来却要考虑不少细节。因此,实践是真的很重要,本身动手实践才能更深入的理解理论。
除了Floyd算法找了资料之外,其它两个实验原本也想去网上找找资料,可是我看的他们的代码都是比较总体,顶点类、边类、图类是一体的,只能一块儿照搬,而我又本身定义了顶点类和边类,很难经过修改网上的代码来适应本身的代码,因此就没有再找资料了。除了老师给的PPT之外,没有再参考其它的资料