1,图的遍历
和树的遍历类似,图的遍历也是从某个顶点出发,沿着某条搜索路径对图中所有顶点各作一次访问。若给定的图是连通图,则从图中任一顶点出发顺着边可以访问到该图中所有的顶点,但是,在图中有回路,从图中某一顶点出发访问图中其它顶点时,可能又会回到出发点,而图中可能还剩余有顶点没有访问到,因此,图的遍历较树的遍历更复杂。我们可以设置一个全局型标志数组visited来标志某个顶点是否被访问过,未访问的值为0,访问过的值为1。根据搜索路径的方向不同,图的遍历有两种方法:深度优先搜索遍历(DFS)和广度优先搜索遍历(BFS)。它们对无向图和有向图都 适用。
1.1,深度优先遍历
深度优先搜索遍历类似于树的先序遍历。假定给定图G的初态是所有顶点均未被访问过,在G中任选一个顶点i作为遍历的初始点,则深度优先搜索遍历可定义如下:
- 首先访问顶点i,并将其访问标记置为访问过,即visited[i]=1。
- 然后搜索与顶点i有边相连的下一个顶点j,若j未被访问过,则访问它,并将j的访问标记置为访问过,visited[j]=1,然后从j开始重复此过程,若j已访问,再看与i有边相连的其它顶点。
- 若与i有边相连的顶点都被访问过,则退回到前一个访问顶点并重复刚才过程,直到图中所有顶点都被访问完为止。
DFS策略:访问某个顶点,寻找的一个邻接顶点访问,反复执行,走过一条较长路径到达最远顶点;若顶点没有未被访问的其他邻接顶点,则退回到前一个被访问顶点,再寻找其他访问路径。
【策略】通过一个结点开始遍历,直到遍历到该结点没有下一个结点为止,然后开始递归下一个结点,如果被访问过,则跳过遍历,依次类推。类似于一口气到底,如果没到底,则换个结点继续到底。如果被访问过的结点则不需要遍历。
【过程】A开始进入递归,A先打印。然后发现A的下一个结点为B,C,D。此时按照顺序开始,B不为空,B进入递归,打印B,然后发现B的下一个结点为A,C,E。由于A已被访问,此时按顺序得到结点C,然后C进入递归,打印D,E。然后D进入递归,打印D,然后E,F依次进入递归。。。。。顺序为:ABCDEF。
public void DFSTraverse(int i) { boolean[] visited = new boolean[this.vertexCount()]; int j = i; do { // 使每个结点进入递归 if (!visited[j]) { // 用于标记是否被访问 System.out.println("{"); // 打印外边 this.depthfs(j, visited); // 进入递归 System.out.println("}"); } j = (j + 1) % this.vertexCount(); } while (j != i); System.out.println(); } private void depthfs(int i, boolean[] visited) { // 递归开始 System.out.println(this.getVertex(i) + " "); // 打印结点值 visited[i] = true; // 设置访问过 int j = this.next(i, -1); // 找到当前结点的下一个结点,顺序优先 while (j != -1) { // 如果存在,不存在为-1 if (!visited[j]) { // 并且没有被访问过 depthfs(j, visited); // 再次进入递归 } j = this.next(i, j); // 出现了递归终止,发现该 结点没有下一结点,则在最开始的递归结点后,找下一节点 } }
分析上述过程,在遍历图时,对图中每个顶点至多调用一次depthfs过程,因为一旦某个 顶点被标志成已被访问,就不再从它出发进行搜索。因此,遍历图的过程实质上是对每个 顶点查找其邻接点的过程。其耗费的时间则取决于所采用的存储结构。
1.2,广度优先遍历
广度优先搜索遍历类似于树的按层次遍历。设图G的初态是所有顶点均未访问,在G 中任选一顶点i作为初始点,则广度优先搜索的基本思想是:
- 首先访问顶点i,并将其访问标志置为已被访问,即visited[i]=1。
- 接着依次访问与顶点i有边相连的所有顶点W1,W2,…,Wt。
- 然后再按顺序访问与W1,W2,…,Wt有边相连又未曾访问过的顶点。
依此类推,直到图中所有顶点都被访问完为止 。
图的广度优先遍历:与上面不同,利用队列储存每个结点的下一结点(所有)。然后入队,打印完当前结点后再出队,进行下次递归。类似于,一下子遍历当前结点的所有下一节点。然后一层一层进行。
【过程】从A开始,然后while循环找A的所有子结点,发现了BCD。然后入队此时队列中为BCD,打印出来A结点。然后进入A进入的递归,出队B代替A,在递归中执行,发现C,E此时由于C已在度列中,则不需要入队,此时只需要E入队,此时队列中为CDE。类似执行D,队列中为EF。。。。然后直到所有被访问完。同理遇到结点无子结点,则递归结束,返回到之前进入递归之前的结点,改变为下一节点。。。。再次递归。
public void BFSTraverse(int i) { boolean[] visited = new boolean[this.vertexCount()]; // 用于标志是否被访问 int j = i;// 获得结点 do { if (!visited[j]) { System.out.println("{"); // 打印外边 breadthfs(j, visited); // 进入打印递归 System.out.println("}"); } j = (j + 1) % this.vertexCount(); } while (j != i);// 直到循环一圈 System.out.println(); } private void breadthfs(int i, boolean[] visited) { // 循环 System.out.println(this.getVertex(i) + " "); // 打印结点 visited[i] = true; // 设置访问过 LinkedQueue<Integer> que = new LinkedQueue<Integer>(); // 建立链队 que.add(i); // 将当前结点入队 while (!que.isEmpty()) { // 如果队列不空,无限循环 i = que.poll(); // 出队 for (int j = next(i, -1); j != i; j = next(i, j)) {// 循环,获得i的所有子结点 if (!visited[j]) { // 如果没被访问 System.out.println(this.getVertex(i) + " "); // 输出结点 visited[j] = true; // 设置为访问过 que.add(j); // 结点入队 } } } }
2,最小生成树
2.1,生成树
连通的无回路的无向图称为无向树,简称树(tree)。n个顶点的一棵树,有n-1条边。
树中的悬挂点又称为树叶(leaf),其他顶点称为分支点(branched node)。
各连通分量均为树的图称为森林(forest)。
由于树中没有回路,因此树中必定无自身环也无重边(否则有回路)。若去掉树中的任意一条边,则变为森林,成为非连通图;若给树加上一条边,形成图中的一条回路,则不是树。
- 若图是连通的或强连通的,则从图中某一个顶点出发可以访问到图中所有顶点。
- 若图是非连通的或非强连通图,则需从图中多个顶点出发搜索访问而每一次从一个新的起始点出发进行搜索过程中得到的顶点访问序列恰为每个连通分量中的顶点集。
深度优先搜索遍历算法及广度优先搜索遍历算法中遍历图过程中历经边的集合和顶点集合一起构成连通图的极小连通子图。它是连通图的一颗生成树。
生成树:是一个极小连通子图,它含有图中全部顶点,但只有n-1条边。由深度优先搜索遍历得到的生成树,称为深度优先生成树,由广度优先搜索遍历得到的生成树,称为广度优先生成树。
生成树具有以下特点:
- 如果在生成树中去掉任何一条边,此子图就会变成非连通图。
- 任意两个顶点之间有且仅有一条路径,如再增加一条边,就会出现一条回路。
- 由遍历连通图G时所经过的边和顶点构成的子图是G的生成树。
生成森林:若一个图是非连通图或非强连通图,但有若干个连通分量或若干个强连通分量,则通过深度优先搜索遍历或广度优先搜索遍历,不可以得到生成树,但可以得到生成森林,且若非连通图有 n 个顶点,m 个连通分量或强连通分量,则可以遍历得到m棵生成树,合起来为生成森林,森林中包含n-m条树边。
生成森林可以利用非连通图的深度优先搜索遍历或非连通图的广度优先搜索遍历算法得到。
2.2,最小生成树
最小生成树:设G是一个带权连通无向图,w(e)是边e上的权,T是G的生成树,T中各边的权之和称为生成树T的权或代价(Cost)
在一般情况下,图中的每条边若给定了权(cost),这时,我们所关心的不是生成树,而是生成树中边上权值之和。若生成树中每条边上权值之和达到最小,称为最小生成树。
前提:
- 使用不同的遍历图的方法,可以得到不同的生成树;从不同的顶点出发,也可能得到不同的生成树。
- 按照生成树的定义,n 个顶点的连通网络的生成树有 n 个顶点、n-1 条边。
目标:在网络的多个生成树中,寻找一个各边权值之和最小的生成树。
构造最小生成树的准则:
- 必须只使用该网络中的边来构造最小生成树。
- 必须使用且仅使用n-1条边来联结网络中的n个顶点。
- 不能使用产生回路的边。
典型用途:欲在n个城市间建立通信网,则n个城市应铺n-1条线路;但因为每条线路都会有对应的经济成本,而n个城市可能有n(n-1)/2 条线路,那么,如何选择n–1条线路,使总费用最少?
数学模型:
- 顶点———表示城市,有n个。
- 边————表示线路,有n–1条。
- 边的权值—表示线路的经济代。
- 连通网——表示n个城市间通信网。
问题抽象: n个顶点的生成树很多,需要从中选一棵代价最小的生成树,即该树各边的代价之和最小。此树便称为最小生成树MST。
2.3,最小生成树的构造算法—Prim算法
普里姆方法的思想是:在图中任取一个顶点K作为开始点,令U={k},W=V-U,其中V为图中所有顶点集,然后找一个顶点在U中,另一个顶点在W中的边中最短的一条,找到后,将该边作为最小生成树的树边保存起来,并将该边顶点全部加入U集合中,并从W中删去这些顶点,然后重新调整U中顶点到W中顶点的距离,使之保持最小,再重复此过程,直到W为空集止。
【思想】通过选择一个根结点,然后遍历其所有的边,选择权重最小的一个边。然后到达相应的结点,然后再从该边出发,依旧选择权重最小的边到达下一个结点。(目的是为了使A到各个结点的权值和最小)
【过程】A开始遍历AB,AC,AD。发现AD权值最小,然后选择AD,从D开始发现DF最小。。。。。。。ADC比ADFC小就用ADC,经过循环后,得到如下图所示的最小生成树。
public void minSpanTree() { Triple[] mst = new Triple[vertexCount() - 1]; // 最小生成树的边集合,边数为顶点数n-1; for (int i = 0; i < mst.length; i++) { // 保存首结点到各个结点的权值 mst[i] = new Triple(0, i + 1, this.weight(0, i + 1)); } for (int i = 0; i < mst.length; i++) { // 找出A到各个结点的权值最小的那条边 int minweight = MAX_WEIGHT, min = 1; for (int j = i + 1; j < mst.length; j++) {//找权值最小的那个 if (mst[j].value < minweight) { minweight = mst[j].value; min = j; } } Triple edge = mst[min]; // 替换原来权值较大的那条边 mst[min] = mst[i]; mst[i] = edge; int tv = edge.column; // 更新其他结点的权值,由于路径选择问题,其他权值也会发生该边 for (int j = i + 1; j < mst.length; j++) { int v = mst[i].column; int weight = this.weight(tv, v); if (weight < mst[j].value) { //如果新权值小,则替换原权值 mst[j] = new Triple(tv, v, weight); } } } System.out.println("最小生成树的边集合为:"); int mincost = 0; for (int i = 0; i < mst.length; i++) { System.out.println(mst[i] + " "); mincost += mst[i].value; } }
2.4,最小生成树的构造算法—Krushal算法
克鲁斯卡尔算法的基本思想是:将图中所有边按权值递增顺序排列,依次选定取权值较小的边,但要求后面选取的边不能与前面选取的边构成回路,若构成回路,则放弃该条边,再去选后面权值较大的边,n个顶点的图中,选够n-1条边即可。
3,最短路径
3.1,单源最短路径-Dijkstra算法
单源点最短路径:求图中某一顶点到其余各顶点的最短路径。
单源点最短路径:给定一个出发点(单源点)和一个有向网G=(V,E),求出源点到其它各顶点之间的最短路径。
迪杰斯特拉算法:按路径长度递增序产生各顶点的最短路径算法。
迪杰斯特拉算法思想:
- 把图中顶点集合分成两组,第一组为集合S,存放已求出其最短路径的顶点,第二组为尚未确定最短路径的顶点集合是V-S(用U表示),其中V为网中所有顶点集合。
- 按最短路径长度递增的顺序逐个把U中的顶点加到S中,直到S中包含全部顶点,而U为空。在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。
- 此外,每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。
迪杰斯特拉算法复杂度:O(n*n)
Dijkstra算法实现
public void shortesPath(int i) { int n = this.vertexCount(); // 图的结点数 boolean[] vset = new boolean[n]; // 已求出最短路径的顶点集合,初始全为false; vset[i] = true; // 当前的开始结点为true int[] dist = new int[n]; // 最短路径的长度 int[] path = new int[n]; // 最短路径终点的前一结点 for (int j = 0; j < n; j++) { // 初始化dist和path dist[j] = this.weight(i, j); path[j] = (j != i && dist[j] < MAX_WEIGHT) ? i : -1; } for (int j = (i + 1) % n; j != i; j = (j + 1) % n) { // 开始寻找从i到各个结点的最短路径,取余为了使防止出现j比结点数i大的情况 int mindist = MAX_WEIGHT, min = 0; // 定义路径最小值和其下标 for (int k = 0; k < n; k++) { // 开始寻找最小路径 if (!vset[k] && dist[k] < mindist) {// 如果没有访问过并且有路径小于当前路径的最小路径 mindist = dist[k]; // 跟新最小路径和最小路径的下标 min = k; } } if (mindist == MAX_WEIGHT) {// 如果没有其他最短路径则此算法结束。 break; } vset[min] = true; // 设置最小结点访问过 for (int k = 0; k < n; k++) {// 更新i到其他结点的路径 if (!vset[k] && this.weight(min, k) < MAX_WEIGHT && dist[min] + this.weight(min, k) < dist[k]) {// 如果没有被访问,并且小于初始路径权值,更新路径。 dist[k] = dist[min] + this.weight(min, k); //最小路径+最小路径结点到目的结点的权《=》i直接到目的结点的权。决定是否替换。 path[k] = min; } } } }
3.2,每对顶点间的最短路径-Floyd算法
迪杰斯特拉的时间复杂度是 O(n²),如果对每个顶点都执行一次,那么时间复杂度将变为 O(n³)。
弗洛伊德算法的基本思想:
- 算法维护一个n*n的距离矩阵D,其中D[i][j]表示从i到j的最短路径长度。算法的核心在于如何更新这个距离矩阵。初始时,D的值为图G的邻接矩阵,即如果存在边(i,j),则D[i][j]的值为边权值,否则D[i][j]的值为无穷大。
- 接下来,我们依次考虑每个顶点k是否作为中转点,如果作为中转点可以使得从i到j的路径长度缩小,则将D[i][j]的值更新为D[i][k]+D[k][j]。也就是说,对于任意的i和j,更新后的D[i][j]表示从i到j经过任意数量中转点的最短路径长度。
- 接着,再依次考虑每个顶点k+1,以此类推,直到中转点为n。最终得到的矩阵D,即为图G中所有顶点间的最短路径长度。
弗洛伊德算法的核心思想总结下来就是:不断增加中转顶点,然后更新每对顶点之间的最短距离。