一篇文章轻松掌握java图实现
创始人
2024-05-03 01:43:15
0

图的基本概念:这里就不予介绍了,这里主要是讲图的代码实现

荔枝目录:

  • 1.图的存储结构
    • 1.1邻接矩阵
    • 1.2邻接表
  • 2.图的遍历
    • 2.1广度优先
    • 2.2深度优先
  • 3.最小生成树
    • 3.1Kruskal算法(全局)
    • 3.2Prim算法(局部)
  • 4.最短路径
    • 4.1单源最短路径算法1--Dijkstra算法
    • 4.2单源最短路径算法2--Bellman-Ford算法

1.图的存储结构

1.1邻接矩阵

邻接矩阵是图常见的存储结构
❤️用邻接矩阵存储图的优点是能够快速知道两个顶点是否连通
💕缺陷是如果顶点比较多,边比较少时,矩阵中存储了大量的0成为系数矩阵,比较浪费空间,并且要求两个节点之间的路径不是很好求。

具体做法是通过一个二维数组来记录每一条边或边的权值

无向图:
在这里插入图片描述
可以发现是对称的。
有向图:
在这里插入图片描述

具体代码实现呢,因为我们是默认没有权值的,如果有需要把这个二维数组的所有元素初始化为最大值(当∞使用)

//顶点集private char[] arrayV;//边集private int[][] arrayE;//是否是有向图private boolean isDirect;//存储顶点下标private static Map indexV=new HashMap<>();public MyGraph(int size, boolean isDirect) {arrayV=new char[size];arrayE=new int[size][size];this.isDirect=isDirect;/*for(int i=0;ifor (int i = 0; i < arrV.length; i++) {this.arrayV[i]=arrV[i];indexV.put(arrV[i],i);}}/*** 添加边和对应权重* @param v1* @param v2* @param weight*/public void addEdge(char v1,char v2,int weight){//先找出两个顶点在顶点集的位置--搞个哈希表方便int v1Index=indexV.get(v1);int v2Index=indexV.get(v2);this.arrayE[v1Index][v2Index]=weight;//如果是无向图if(!isDirect){this.arrayE[v2Index][v1Index]=weight;}}/*** 把边集的二维数组打印出来*/private void printGraph() {for(int i=0;i< arrayE.length;i++){for (int j = 0; j < arrayE[i].length; j++) {System.out.print(arrayE[i][j]+" ");}System.out.println();}}

测试用例:

public static void main(String[] args) {MyGraph graph = new MyGraph(4,false);char[] array = {'A','B','C','D'};graph.initArrayV(array);graph.addEdge('A','B',1);graph.addEdge('A','D',1);graph.addEdge('B','A',1);graph.addEdge('B','C',1);graph.addEdge('C','B',1);graph.addEdge('C','D',1);graph.addEdge('D','A',1);graph.addEdge('D','C',1);graph.printGraph();}

结果:
在这里插入图片描述

1.2邻接表

这里通过结点的形式将该边的起点和终点记录,接到对应的链表上
结点结构

static class Node{public int src;//起始点下标public int dest;//终止点下标public int weight;//权值public Node next;public Node(int src, int dest, int weight) {this.src = src;this.dest = dest;this.weight = weight;}}

图示:
在这里插入图片描述

当然也有使用入边和出边分开保存的,这里借用图片:
在这里插入图片描述

具体代码:

 //邻接结点static class Node{public int src;//起始点下标public int dest;//终止点下标public int weight;//权值public Node next;public Node(int src, int dest, int weight) {this.src = src;this.dest = dest;this.weight = weight;}}//顶点集private char[] arrayV;//是否是有向图private boolean isDirect;//存储顶点下标private static Map indexV=new HashMap<>();//存储每个顶点的边结点链表private List list;public MyGraph2(int size,boolean isDirect){this.arrayV=new char[size];this.isDirect=isDirect;list=new ArrayList<>(size);//给个初始大小并且置为null,方便后面判断for (int i = 0; i < size; i++) {list.add(null);}}/*** 初始化顶点集* @param arrV*/public void initArrayV(char[] arrV){for (int i = 0; i < arrV.length; i++) {this.arrayV[i]=arrV[i];indexV.put(arrV[i],i);}}/*** 插入边结点到链表中* @param v1* @param v2* @param weight*/public void addEdge(char v1,char v2,int weight){int src=indexV.get(v1);int dest=indexV.get(v2);addEdgeChild(src,dest,weight);//如果是无向图,需要反着再添加一次if(!isDirect){addEdgeChild(dest,src,weight);}}private void addEdgeChild(int src,int dest,int weight) {Node node=new Node(src,dest,weight);//头插入法Node cur= list.get(src);//获取src的链表头位置if(cur==null){list.set(src,node);}else{Node tmp=list.get(src);node.next=tmp;list.set(src,node);}}public void printGraph() {for (int i = 0; i < arrayV.length; i++) {System.out.print(arrayV[i]+"-> ");Node cur = list.get(i);while (cur != null) {System.out.print(cur.dest+"-> ");cur = cur.next;}System.out.println("null");}}

测试用例:

public static void main(String[] args) {MyGraph2 graph = new MyGraph2(3,true);char[] array = {'A','B','C'};graph.initArrayV(array);graph.addEdge('A','B',1);graph.addEdge('B','A',1);graph.addEdge('B','C',1);graph.printGraph();}

结果:
在这里插入图片描述

2.图的遍历

2.1广度优先

核心是确定首位置点,遍历时先遍历与该点最近的点,再遍历第二近的点,直到遍历完成。
在这里插入图片描述
这个看起来和二叉树的层序遍历很像,没错,我们在层序遍历二叉树的时候利用了一个队列,那这里也需要一个队列。不过只一个队列还不行,还需要一个数组来记录某个点是否已经遍历过。因为点与点直接是互连的,会出现一个点重复进入队列的情况。

具体代码实现:

public void bfs(char start){boolean isAppear[]=new boolean[arrayV.length];//默认为falseDeque deque=new ArrayDeque();//Integer为记录点的下标deque.offer(indexV.get(start));//添加第一个点并记录该点已经出现isAppear[indexV.get(start)]=true;while(!deque.isEmpty()){int top=deque.poll();//弹出第一个点System.out.print(arrayV[top]+":"+top+"->");//加入下一个点for (int i = 0; i < arrayE.length; i++) {if(arrayE[top][i]!=0&&isAppear[i]==false){//添加点并先记录该点已经出现isAppear[i]=true;deque.offer(i);}}}}

这里需要再加入的时候就设置已经出现,如果在弹出的时候才设置就会导致多打印最后一个点。

2.2深度优先

和二叉树的前序遍历差不多,都是一条路先走到头再回溯换路
但是这里也需要判断当前点是否已经遍历过了,防止重复遍历
在这里插入图片描述
具体代码如下:

 public void dfs(char start){boolean isAppear[]=new boolean[arrayV.length];//默认为falseint index=indexV.get(start);dfsChild(index,isAppear);}private void dfsChild(int cur, boolean[] isAppear) {isAppear[cur]=true;System.out.print(arrayV[cur]+"->");for (int i = 0; i < arrayE.length; i++) {if(arrayE[cur][i]!=0&&isAppear[i]==false){dfsChild(i,isAppear);}}}

3.最小生成树

基本概念

一个连通图的最小连通子图称作该图的生成树。 连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从其中删去任何一条边,生成树 就不在连通;反之,在其中引入任何一条新边,都会形成一条回路。
若连通图由n个顶点组成,则其生成树必含n个顶点和n-1条边。因此构造最小生成树的准则有三 条:

  1. 只能使用图中的边来构造最小生成树
  2. 只能使用恰好n-1条边来连接图中的n个顶点
  3. 选用的n-1条边不能构成回路 构造最小生成树的方法:Kruskal算法和Prim算法。这两个算法都采用了逐步求解的贪心策略。

贪心算法:是指在问题求解时,总是做出当前看起来最好的选择。也就是说贪心算法做出的不是整体
最优的的选择,而是某种意义上的局部最优解。贪心算法不是对所有的问题都能得到整体最优解

3.1Kruskal算法(全局)

克鲁斯卡尔算法的核心是,每次在边集取一条权重最小的边加入生成树。这个算法可能会产生回路问题,所以每次取边都需要判断取该条边会不会使生成树有回路。
图示:
在这里插入图片描述
从这个图我们可以提取到我们代码的核心:
1.怎么每次取一条权值最小的边?
2.如何判断回路?
第一个问题可以采用优先级队列(小根堆)的方式来处理
第二个问题可以采用并查集的方式来处理,把已经连上的点看成一个集合,比如在图f中如果想选择左下的7,我们就到并查集里查找i和h点在不在同一个集合中在就不能选。
这里图的存储结构采用邻接矩阵

代码分析:

/**** @return 返回最小生成树的权值*/public int kruskal(MyGraph minTree){int totalWeight=0;//优先级队列存储边--小根堆PriorityQueue minHead=new PriorityQueue<>(new Comparator() {@Overridepublic int compare(Edge o1, Edge o2) {return o1.weight-o2.weight;//自定义类型要传比较器重写compare方法确定比较条件}});//将所有边加入优先集队列for (int i = 0; i < arrayE.length; i++) {for (int j = 0; j < arrayE[i].length; j++) {if(arrayE[i][j]!=Integer.MAX_VALUE){Edge edge=new Edge(i,j,arrayE[i][j]);minHead.add(edge);}}}//创建一个并查集来方便检查有没有回路UnionFindSet loops=new UnionFindSet(arrayV.length);//n个结点需要n-1条边int size=0;int n=arrayV.length;while (size//取出边Edge top=minHead.poll();//判断这条边的加入会不会产生回路if(loops.isSameSet(top.srcIndex,top.destIndex)==false){//加入到生成树中minTree.addEdgeUserIndex(top.srcIndex,top.destIndex,top.weight);//将两点加入同一个集合loops.union(top.srcIndex,top.destIndex);//累加权值totalWeight+=top.weight;size++;}}//判断边不够的情况---做不了生成树--也就是不是连通图if(minHead.isEmpty()){return -1;}return totalWeight;}

测试代码:

public static void testGraphMinTree() {String str = "abcdefghi";char[] array = str.toCharArray();MyGraph g = new MyGraph(str.length(), false);g.initArrayV(array);g.addEdge('a', 'b', 4);g.addEdge('a', 'h', 8);g.addEdge('b', 'c', 8);g.addEdge('b', 'h', 11);g.addEdge('c', 'i', 2);g.addEdge('c', 'f', 4);g.addEdge('c', 'd', 7);g.addEdge('d', 'f', 14);g.addEdge('d', 'e', 9);g.addEdge('e', 'f', 10);g.addEdge('f', 'g', 2);g.addEdge('g', 'h', 1);g.addEdge('g', 'i', 6);g.addEdge('h', 'i', 7);MyGraph kminTree = new MyGraph(str.length(), false);System.out.println(g.kruskal(kminTree));kminTree.printGraph();}

结果:
在这里插入图片描述

3.2Prim算法(局部)

和克鲁斯卡尔算法不同的是,普利姆算法采用局部贪心的思想,从任意点出发,将已经加入生成树的点和未加入的点分成两个集合X,Y,在连接这两个集合中任意两点的边里选择一条权值最小的。选完之后,将Y集合的那个点加入X集合中。这个方法天生就是没有回路的,因为取边的时候Y的点肯定不和X的点在同一个集合。
图示:
在这里插入图片描述
这里在代码中主要问题有:
1.X和Y的集合怎么表示?
2.如何在两个集合中任意两点的边里选择一条权值最小的边?
第一个问题可以通过哈希表来表示集合
第二个问题依然采用优先级队列,每往X集合加入一个点,我们就把这个点占有的所有边加入优先级队列,每次取边时以Y集合的点为起点,判断取的这条边的另一个点即终点在不在X集合里。因为我们加入边的时候不是所有边都能取到
就像图8一样,那个7虽然小,但取不得。

代码分析:

/**** @param minTree* @return 最小生成树的权值*/public int prim(MyGraph minTree){int totalWeight=0;//优先级队列存储边--小根堆PriorityQueue minHead=new PriorityQueue<>(new Comparator() {@Overridepublic int compare(Edge o1, Edge o2) {return o1.weight-o2.weight;//自定义类型要传比较器重写compare方法确定比较条件}});//创建X,Y集合  存下标Set X=new HashSet<>();Set Y=new HashSet<>();int start=0;//任意点均可X.add(start);for (int i = 0; i < arrayV.length; i++) {if(i!=start)Y.add(i);}//初始化队列--根据X的内容来初始化for (int j = 0; j < arrayE[start].length; j++) {if(arrayE[start][j]!=Integer.MAX_VALUE){Edge edge=new Edge(start,j,arrayE[start][j]);minHead.add(edge);}}int size=0;int n=arrayV.length;while(sizeEdge top=minHead.poll();//取的这条边的另一个点即终点在不在X集合里if(!X.contains(top.destIndex)){//加入生成树minTree.addEdgeUserIndex(top.srcIndex,top.destIndex,top.weight);//将来自Y的这个点destIndex加入X中,Y要删除它,并更新边的队列X.add(top.destIndex);Y.remove(top.destIndex);//每选一条边 就打印一条语句 测试一下System.out.println(arrayV[top.srcIndex] + "-> " + arrayV[top.destIndex] + " : " + top.weight);for (int j = 0; j < arrayE[top.destIndex].length; j++) {if(arrayE[top.destIndex][j]!=Integer.MAX_VALUE&&!X.contains(j)){//加入边的时候不能加入了a->b还加入b->aEdge edge=new Edge(top.destIndex,j,arrayE[top.destIndex][j]);minHead.add(edge);}}totalWeight+=top.weight;size++;}}//判断边不够的情况---做不了生成树--也就是不是连通图if(minHead.isEmpty()){return -1;}return totalWeight;}

测试用例:

public static void test2GraphMinTree() {String str = "abcdefghi";char[] array = str.toCharArray();MyGraph g = new MyGraph(str.length(), false);g.initArrayV(array);g.addEdge('a', 'b', 4);g.addEdge('a', 'h', 8);g.addEdge('b', 'c', 8);g.addEdge('b', 'h', 11);g.addEdge('c', 'i', 2);g.addEdge('c', 'f', 4);g.addEdge('c', 'd', 7);g.addEdge('d', 'f', 14);g.addEdge('d', 'e', 9);g.addEdge('e', 'f', 10);g.addEdge('f', 'g', 2);g.addEdge('g', 'h', 1);g.addEdge('g', 'i', 6);g.addEdge('h', 'i', 7);MyGraph primTree = new MyGraph(str.length(), false);int total=g.prim(primTree);System.out.println(total);primTree.printGraph();}

结果:
在这里插入图片描述

4.最短路径

最短路径问题:从在带权图的某一顶点出发,找出一条通往另一顶点的最短路径,最短也就是沿路径各边的权值总和达到最小

4.1单源最短路径算法1–Dijkstra算法

注:单源就是给你一个起点让你求和其他点的最短路径,多源就是任意两点的最短路径。

迪杰斯特拉算法思想:
针对一个带权有向图G,将所有结点分为两组S和Q,S是已经确定最短路径的结点集合,在初始时为空(初始时就可以将源节点s放入,毕竟源节点到自己的代价是0),Q 为其余未确定最短路径的结点集合每次从Q中找出一个起点到该结点代价最小的结点u ,将u 从Q 中移出,并放入S 中,对u 的每一个相邻结点v 进行松弛操作松弛即对每一个相邻结点v ,判断源节点s到结点u 的代价与u 到v 的代价之和是否比原来s 到v 的代价更小,若代价比原来小则要将s 到v 的代价更新为s 到u 与u 到v 的代价之和,否则维持原样。如此一直循环直至集合Q 为空,即所有节点都已经查找过一遍并确定了最短路径,至于一些起点到达不了的结点在算法循环后其代价仍为初始设定的值,不发生变化。Dijkstra算法每次都是选择V-S中最小的路径节点来进行更新,并加入S中,所以该算法使用的是贪心策略。
在这里插入图片描述在这里插入图片描述
缺点:不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路径的最短路
径。例如:就找不到a->c->b这条最短路径
在这里插入图片描述

代码分析
主要问题:1.如何表示该点已经固定
2.如何获取我们的最短路径(怎么走的)
解答:
1.可通过boolean数组表示
2.通过pPath数组下标对应值来表示,例如:
在这里插入图片描述
代码:

public void dijkstra(char vSrc,int[] dist,int[] pPath){//获取起点下标int srcIndex=indexV.get(vSrc);//初始化dist数组--初始全无穷,起点下标设成0Arrays.fill(dist,Integer.MAX_VALUE);dist[srcIndex]=0;//初始化pPath数组--初始全-1,起点的下标设为自己的下标Arrays.fill(pPath,-1);pPath[srcIndex]=srcIndex;//表示某点已经固定--初始全falseboolean[] isSure=new boolean[dist.length];int n=dist.length;for (int k = 0; k < n; k++) {//要松弛的结点int u=srcIndex;//每次循环确定一个最小值固定,并确定下一次要松弛的结点int min=Integer.MAX_VALUE;for (int i = 0; i < n; i++) {if(dist[i]!=Integer.MAX_VALUE&&isSure[i]==false&&dist[i]min=dist[i];u=i;}}//固定找到的最小值isSure[u]=true;//对u点进行松弛操作for (int v = 0; v < n; v++) {/*判断源节点s到结点u 的代价与u 到v 的代价之和是否比原来s 到v 的代价更小,若代价比原来小则要将s 到v 的代价更新为s 到u 与u 到v 的代价之和,否则维持原样*///v顶点未固定 并且 u->v 是有权值的 并且 加起来的权值 < dist[v]if(isSure[v]==false&&arrayE[u][v]!=Integer.MAX_VALUE&&arrayE[u][v]+dist[u]dist[v]=arrayE[u][v]+dist[u];//修改最小值pPath[v]=u;//修改路径}}}}

4.2单源最短路径算法2–Bellman-Ford算法

迪杰斯特拉算法虽好,但是解决不了带负权的图,那么要处理有负权图的最短路径,就要用到贝尔曼-福特算法。
它的优点是可以解决有负权边的单源最短路径问题,而且可以用来判断是否有负权回路。它也有明显的缺点,它的时间复杂度 O(N*E)(N是点数,E是边数)普遍是要高于Dijkstra算法O(N²)的。像这里如果我们使用邻接矩阵实现,那么遍历所有边的数量的时间复杂度就是O(N^3),这里也可以看出来贝尔曼-福特算法就是一种暴力求解更新。
注意负权不能有负权回路!不然永远都求不到最短。例如:
在这里插入图片描述
接下来我们来研究一下这个算法是如果处理的吧!
这个算法就是对N个点每个点都进行N次松弛操作,因为后一次的负权路径可能会影响到前面的最短路径,例如:我们按a,b,c,d依次进行一次松弛操作可以发现
在这里插入图片描述
所以,单单一次松弛是不能够搞定的,每个结点所以要进行n次松弛操作才能保证我们的结果是正确的。

代码:
代码和迪杰斯特拉算法的代码很相似,但不再需要判断该点是否固定。

/**** @param vSrc* @param dist* @param pPath* @return 是否存在负权回路*/public boolean bellmanFord(char vSrc,int[] dist,int[] pPath) {//获取起点下标int srcIndex=indexV.get(vSrc);//初始化dist数组--初始全无穷,起点下标设成0Arrays.fill(dist,Integer.MAX_VALUE);dist[srcIndex]=0;//初始化pPath数组--初始全-1,起点的下标设为自己的下标Arrays.fill(pPath,-1);int n=arrayV.length;//循环n次松弛结点for (int k = 0; k < n; k++) {for (int i = 0; i < n; i++) {for (int j = 0; j < n; j++) {if(arrayE[i][j]!=Integer.MAX_VALUE&&dist[i]+arrayE[i][j]dist[j]=dist[i]+arrayE[i][j];pPath[j]=i;}}}}//判断是否存在负权回路,再松弛一遍还能修改就代表存在for (int i = 0; i < n; i++) {for (int j = 0; j < n; j++) {if(arrayE[i][j]!=Integer.MAX_VALUE&&dist[i]+arrayE[i][j]return false;}}}return true;}

相关内容

热门资讯

开源电脑安卓系统排行,探索自由... 亲爱的电脑爱好者们,你是否曾想过,在电脑的世界里,也能体验到安卓系统的便捷与乐趣?没错,这就是今天我...
如何清空相册安卓系统,轻松恢复... 手机里的相册是不是越来越满,看着那些堆积如山的照片,是不是有点头疼呢?别急,今天就来教你怎么在安卓系...
安卓系统要停止更新,拥抱新变革 你知道吗?最近有个大消息在安卓圈里炸开了锅!安卓系统,这个陪伴我们多年的老朋友,竟然要停止更新了!这...
安卓系统怎样强行关机,安卓系统... 手机突然卡壳了,是不是又想强行关机了?别急,今天就来教你安卓系统怎样强行关机,让你轻松应对各种突发状...
安卓系统如何删除桌面,轻松删除... 手机桌面乱糟糟的,是不是感觉像你的房间一样,东西堆得有点多?别急,今天就来教你怎么给安卓系统的桌面来...
安卓系统怎么发英语,Andro... 你有没有想过,在安卓系统上发送英语信息竟然也能变得如此简单有趣?没错,就是那种轻松自如,仿佛英语是你...
最早期的安卓系统,揭秘最早期安... 亲爱的读者,你是否曾好奇过,那个陪伴我们手机成长的安卓系统,它的起源究竟是怎样的呢?今天,就让我们一...
安卓双系统添加应用,轻松实现多... 你有没有想过,你的安卓手机里可以同时运行两个系统呢?听起来是不是很酷?想象一边是熟悉的安卓系统,一边...
pipo安卓进系统慢,探究pi... 最近是不是发现你的Pipo安卓系统更新或者运行起来特别慢?别急,今天就来给你好好分析分析这个问题,让...
怎样使用安卓手机系统,安卓手机... 你有没有发现,安卓手机已经成为我们生活中不可或缺的一部分呢?从早晨闹钟响起,到晚上睡前刷剧,安卓手机...
双系统安卓安装caj,轻松实现... 你有没有想过,你的安卓手机里装上双系统,是不是就能同时享受安卓和Windows系统的乐趣呢?没错,这...
安卓使用ios系统教程,安卓用... 你是不是也和我一样,对安卓手机上的iOS系统充满了好奇?想要体验一下苹果的优雅和流畅?别急,今天我就...
安卓系统gps快速定位,畅享便... 你有没有遇到过这样的情况:手机里装了各种地图导航软件,但每次出门前都要等上好几分钟才能定位成功,急得...
安卓手机系统更新原理,原理与流... 你有没有发现,你的安卓手机最近是不是总在提醒你更新系统呢?别急,别急,让我来给你揭秘一下安卓手机系统...
安卓系统通知管理,全面解析与优... 你有没有发现,手机里的通知就像是一群调皮的小精灵,时不时地跳出来和你互动?没错,说的就是安卓系统的通...
安卓系统手机哪买,揭秘哪里购买... 你有没有想过,拥有一部安卓系统手机是多么酷的事情呢?想象你可以自由安装各种应用,不受限制地探索各种功...
安卓系统 ipv4,基于安卓系... 你知道吗?在智能手机的世界里,有一个系统可是无人不知、无人不晓,那就是安卓系统。而在这个庞大的安卓家...
目前安卓是什么系统,探索安卓系... 亲爱的读者,你是否曾好奇过,如今安卓系统究竟是什么模样?在这个科技飞速发展的时代,操作系统如同人体的...
安卓6.0系统比5.0,从5.... 你有没有发现,自从手机更新了安卓6.0系统,感觉整个人都清爽了不少呢?没错,今天咱们就来聊聊这个话题...
安卓2.36系统升级,功能革新... 你知道吗?最近安卓系统又来了一次大变身,那就是安卓2.36系统升级!这可不是一个小打小闹的更新,而是...