目录导读:
- 1.基本概念
- 2.图的存储概念(5中方式)
邻接矩阵
邻接表
- 3.图的遍历
深度优先遍历
广度优先遍历
- 4.最小生成树
普里姆算法(Prim)
克鲁斯卡尔算法(kruskal)
- 5.最短路径
迪杰斯特拉算法(Dijkstra)
佛罗伊德算法(Floyd)
- 6.拓扑排序
- 6.关键路径
1.图(Graph)
由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E)
其中,G 表示一个图,V 是图G中顶点的集合,E是图G中边的集合
注意:
- 图的顶点集 V 不可以为空,至少要为1,但是边集 E 可以为空。
- 线性表中数据元素叫元素,树中数据元素叫结点,图中数据元素叫顶点
2.若干个定义
无向边(Edge):(vi, vj) ——> 无向图:图中所有边均为无向边
有向边(Arc,也称为弧): ——> 有向图:图中所有边均有有向边
简单图:不存在顶点到其自身的边,且同一条边也不重复。
完全图:如果任意两个顶点之间都存在边
图分类
(1)按边分:
无向图 (顶点+无向边)
有向图(顶点+有向边(弧)),弧有弧头和弧尾之分
(2)按边的多少分:(这里是模糊概念)
稀疏图
稠密图
(3)按任意两顶点之间是否相连
无向完全图:任意两个顶点之间都存在边
有向完全图:任意两个顶点之间都存在边且边是有向的
无向完全图的边数:n(n-1)/2 (按等差数列来理解)
有向完全图的边数:n(n-1)
因此,对于n个顶点和e条边的图来说有:
无向图:0 ≤ e ≤ n(n-1)/2
有向图:0 ≤ e ≤ n(n-1)
网(NetWork):图的边或弧有相关的数(该树叫做权值weight)
子图:
假设图G=(V,{E})和图G'=(V,{E'})有:
V' ≤ V ,这里应该用包含符号
E' ≤ E ,这里应该用包含符号
则有,G' ≤ G 称G'是G的字图(subgraph)
2)图的顶点与边的关系
无向图 G = (V,{E}),边(v,v')依附于顶点v,v',而v与v'互为邻接
度:指针对具体的顶点而言的,指与该顶点相连的边数。
各顶点度之和 = 1/2*边数之和
有向图 G = (V,{E}),弧和顶点v,v'相关联,v邻接到v',v'邻接自v
度 = 出度+入度
各顶点出度之和 = 入度之和 = 弧数之和
3)路径
回环(回路)
简单路径
简单回环
4)连通图
在无向图中,任意两个顶点之间都是连通的(即存在路径),则称该图是"连通图"
连通分量:又称为无向图中的极大连通子图(换言之,就是尽可能多的的包含
顶点数的连通子图)
注意连通分量的概念,它强调:
是子图
子图要连通
连通子图包含"极大"顶点数、
包含依附这些顶点的所有边
在有向图中,任意两个顶点之间都是连通的(不一定是直接连通),则称该图是"强连通图"
强连通分量:又称为有向图中的极大强连通子图
5)生成树
针对连通图而言的。是连通图的一个极小的连通子图,它含有图中全部的 n 个顶点,n-1 条边
注意:最小生成树,生成树中构造连通网的代价最小的生成树称为最小生成树
6)有向树
在有向图中,只有一个顶点的入度为0,求余顶点的入度均为1
7)生成森林
3.图的存储结构
由于图的结构复杂,因此无法以数据元素在内存中的物理存储位置来反映元素之间
的逻辑关系
5种存储结构:
(1)邻接矩阵(Adjacency Martix)
该存储方式是用两个数组来表示图:一个一维数组存储顶点信息,一个二维数组
存储(称为邻接矩阵)图中边或弧的信息。
图G(V,{E}) 有 n 个顶点,则其邻接矩阵定义为:
┌ 1, 若(vi,vj)属于E,此时可以看成权值为1
arc[i][j] = │
└ 0, 反之
注意:对于无向图,很容易得知该邻接矩阵是对称矩阵
有了邻接矩阵求某一点的度就是该点所在的行或列元素之和
网G(V,{E}),有n个顶点,则其邻接矩阵定义为:
┌ Wij, 若(vi,vj)属于E,
│
arc[i][j] = │ 0, i = j;(自身到自身的权值是0)
│
└ ∞, 反之
V0 边数组: 顶点数组:
╱ │ ╲ ┌ ┐ ┌──┬──┬──┬──┐
V1 │ V3 │0 1 1 1│ │V0│V1│V2│V3│
╲ │ ╱ │1 0 1 0│ └──┴──┴──┴──┘
V2 │1 1 0 1│
│1 0 1 0│
└ ┘
'邻接矩阵存储'数据结构
import java.util.Scanner;
/**
* 图、网的邻接矩阵存储方式
* @author Administrator
*
* @param
*/
public class AdjMartix {
private T[] vertexArr; //用于存放图的顶点(一维数组)
private int[][] adjMar; //邻接矩阵(二维数组),存放权值
private int vertexNum; //图的顶点数
private int edgesNum; //图的边数
private final int MAX = 999; //表示网中权值是无穷大,数值可调
private final int MAXSIZE = 10; //邻接矩阵中存储顶点的最大默认数量
public AdjMartix() {
vertexArr = (T[])new Object[MAXSIZE];
adjMar = new int[MAXSIZE][MAXSIZE];
}
//创建无向图的邻接矩阵
public void createAdjMartix() {
Scanner sc = new Scanner(System.in);
System.out.print("请输入图的顶点数:");
vertexNum = sc.nextInt();
System.out.print("请输入图的边数:");
edgesNum = sc.nextInt();
System.out.print("请输入顶点信息:");
String ver = sc.next();
//初始化顶点存储数组
for(int i = 0; i < ver.length(); i++) {
vertexArr[i] = (T)(Object)ver.charAt(i);
}
System.out.println("输入边的信息:");
for(int j = 0; j < edgesNum; j++) {
System.out.print("输入第" + (j+1) + "条边的两个端点:");
String str = sc.next();
T v1 = (T)(Object)str.charAt(0);
T v2 = (T)(Object)str.charAt(1);
//获取端点所在的位置
int h = locatVer(v1); //横坐标
int v = locatVer(v2); //纵坐标
if(h >= 0 && v >= 0) {
//给邻接矩阵赋值,
adjMar[h][v] = 1;
adjMar[v][h] = 1; //注意:无向图的邻接矩阵是对称的
}
}
}
/**
* 根据端点获得所在顶点数组中的存储位置
* 如果存在就返回存储位置,否则就返回-1
* @param t
* @return
*/
private int locatVer(T t) {
for(int i = 0; i < vertexArr.length; i++) {
if(vertexArr[i] == t) {
return i;
}
}
return -1; //表示不存在
}
}
(2)邻接表(Adjacency List)
当图中边数远小于顶点个数时(即稀疏图),采用邻接矩阵的话,就是造成极大的空间浪费。
因此,我们可以考虑使用对边或弧使用链式存储的方式来避免空间浪费
数组和链表相结合的存储方法称为邻接表。
data,firstedge
┌──┬──┐ adjvex(邻接点域),next(边表下一结点的指针)
V0 0 │V0│ │→ 1| → 2| → 3|^
╱ │ ╲ ├──┼──┤
V1 │ V3 1 │V1│ │→ 1| → 2|^
╲ │ ╱ ├──┼──┤
V2 2 │V2│ │→ 0| → 1| → 3|^
├──┼──┤
3 │V3│ │→ 0| → 2|^
└──┴──┘
邻接表的建立:
1.建立顶点表:图中顶点用一个一维数组存储(用链表也可以,只是数组方便读取顶点信息)
每个数据元素还需要存储指向第一个邻接点的指针,便于查找该顶点的边的信息
2.建立边表:图中每个顶点Vi的所有邻接点构成一个线性表,由于每个顶点的邻接点的个数
不定,所以,采用单链表来存储这些邻接点。该线性表对于无向图称为顶点Vi的边表,
对于有向图则称为以顶点Vi为弧尾的出边表。
注意:对于有向图而言,以每个顶点为弧尾建立的邻接表称为邻接表(便于得到顶点的出度)
对于有向图而言,以每个顶点为弧头建立的邻接表称为逆邻接表(便于得到顶点的入度)
对于网图,可以在边表结点定义中再增加一个weight的数据域,用于存储权值
实现(看下面)该邻接表的算法复杂度:O(n+e), n为顶点个数,e为边的条数
'代码描述'
package chapter05.graph.storage;
import java.util.Scanner;
/**
* 图的邻接表的存储方式
* VerNode: 表示顶点表的结构
* ArcNode:表示边表的结构
* @author Administrator
*
*/
public class AdjListGraph {
protected final int MAXSIZE = 10;
protected VerNode[] adjList; //存储顶点结点的数组
int n; //表示顶点数
int e; //表示弧数
public AdjListGraph() {
adjList = new VerNode[MAXSIZE];
}
//建立无向图的邻接表
public void createAdjGraph() {
String str; //用于接受字符串而已
Scanner sc = new Scanner(System.in);
System.out.println("请输入图的顶点数:");
n = sc.nextInt();
System.out.println("请输入图的边数:");
e = sc.nextInt();
System.out.println("请输入图的顶点信息");
str = sc.next();
//初始化顶点信息
for (int i = 0; i < str.length(); i++) {
adjList[i] = new VerNode();
adjList[i].data = str.charAt(i); //将顶点信息存入新建的顶点中
adjList[i].firstArc = null;
}
System.out.println("请输入图的边的信息:");
ArcNode s; //重复利用的边表结点
for(int j = 0; j < e; j++) {
System.out.println("请输入第" + (j + 1) + "条边的两个端点:");
str = sc.next();
//得到边的连个端点
T v1 = (T)(Object)str.charAt(0);
T v2 = (T)(Object)str.charAt(1);
//确定这连个顶点在图中的位置
int loc1 = locateVex(v1);
int loc2 = locateVex(v2);
//在相应位置插入边表结点
if (loc1 >= 0 && loc2 >= 0) {
//下面这段代码可以创建有向图的出边邻接表
s = new ArcNode();
s.adjVer = loc2;
s.next = adjList[loc1].firstArc;
adjList[loc1].firstArc = s;
/**
* 由于是无向图,因此是对称的.
* 如果是创建有向图的出边邻接表,则只需注释掉下面的这段
* 代码即可。
*/
s = new ArcNode();
s.adjVer = loc1;
s.next = adjList[loc2].firstArc;
adjList[loc2].firstArc = s;
}
}
}
/**
* 在图G中查找顶点v在顶点数组的位置。若不存在则返回-1
* @param v
* @return
*/
public int locateVex(T v) {
for (int i =0 ; i < n; i++) {
if(adjList[i].data == v) {
return i;
}
}
return -1;
}
//打印邻接表
public void disPlayAdjList() {
ArcNode p;
System.out.print("图的邻接表的表示:");
for(int i = 0; i < n; i++) {
System.out.print("\n " + adjList[i].data);
p = adjList[i].firstArc;
while(p != null) {
System.out.print("-->" + p.adjVer);
p = p.next;
}
}
}
}
4.图的遍历(Traversing Graph)
含义:从图中某一顶点出发访问遍历图中其余顶点,且使每一个顶点仅被访问一次,这
一过程就是图的遍历
两种遍历方式:
(1)深度优先(Deep First Search, DFS)
(2)广度优先(Breadth First Search, BFS)
1.深度优先遍历(DFS)
类似于树的先序遍历,是树的先序遍历的推广。
思想过程:
1)首先访问出发点V
2)然后依次从V出发搜索V的每个'邻接点'W, 若W未曾访问过,则以W为新的出发
点为深度优先搜索遍历,直至图中所有和源点V有路径相同的顶点均已被访问为止
3)若此图中仍有未访问的顶点,则选另一个尚未被访问的顶点作为新的源点重复
上述过程,直至图中所有顶点均已被访问为止。
上述遍历过程就是递归的过程。
下面分别用邻接矩阵和邻接表来实现图的深度优先遍历
注意:对于点多边少的图而言,邻接表结构效率更高 O(n+e), n为顶点个数,e为边数
(1)用邻接矩阵来实现:算法时间复杂度O(n^2)
'代码描述'
//深度优先遍历图
public void DFSTraverse() {
//其实这段是可以不用的,因为boolean的默认值就是false
for(int i = 0; i < vertexNum; i++) {
visited[i] = false; //默认所有的顶点均为被访问到
}
System.out.println("图的深度优先遍历序列:");
for(int j = 0; j < vertexNum; j++) {
//判断该点是否被访问到过
if(!visited[j]) {
//表示该点没有被访问到过
DFS(j); //则从i好点开始进行深度优先遍历
}
}
}
/**
* 利用邻接矩阵完成连通图的遍历
* @param i
*/
private void DFS(int n) {
System.out.print(vertexArr[n] + " "); //访问到第i个点,并打印出来
visited[n] = true;
for(int i = 0; i < vertexNum; i++) {
if(!visited[i] && (adjMar[n][i] == 1)) {
DFS(i);
}
}
}
(2)利用邻接表实现深度优先遍历:算法时间复杂度 O(n+e)
//深度优先遍历
public void DFSTraverse() {
//其实这段是可以不用的,因为boolean的默认值就是false
for(int i = 0; i < n; i++) {
visited[i] = false; //默认所有的顶点均为被访问到
}
System.out.println("图的深度优先遍历序列:");
for(int j = 0; j < n; j++) {
//判断该点是否被访问到过
if(!visited[j]) {
//表示该点没有被访问到过
DFS(j); //则从i好点开始进行深度优先遍历
}
}
}
/**
* 采用邻接表完成图的深度优先遍历
* @param n
*/
private void DFS(int n) {
System.out.print(adjList[n].data + " "); //访问到第i个点,并打印出来
visited[n] = true;
ArcNode p;
p = adjList[n].firstArc;
while(p != null) {
if(!visited[p.adjVer]) {
DFS(p.adjVer);
}
p = p.next;
}
}
2.广度优先遍历(BFS)
类似于树的层序遍历,是树的层序遍历的推广。
过程:
1)首先访问出发点V
2)接着访问顶点V的所有邻接点V1,V2,...Vt
3)然后依次访问V1,V2,...Vt的所有邻接点
4)依次执行,直至图中所有的顶点都被访问到
由该遍历过程可知,该过程用迭代即可完成
(1)利用邻接矩阵实现的图的广度优先搜索:O(n^2)
'代码描述'
/**
* 广度优先搜索
*/
public void BFSTraverse() {
System.out.println("图的广度优先遍历");
for(int i = 0; i < vertexNum; i++) {
visited[i] = false;
}
for(int j = 0; j < vertexNum; j++) {
if(!visited[j]) {
BFS(j);
}
}
}
/**
* 邻接矩阵实现的广度优先遍历:类似于树的层序遍历
* 该部分只能实现图的一个连通子图
* @param n
*/
private void BFS(int n) {
System.out.print(verArr[n] + " ");
Queue queue = new LinkedList();
visited[n] = true;
queue.offer(n); //当前顶点进队
while(!queue.isEmpty()) {
int i = queue.poll();
for(int j = 0; j < vertexNum; j++) {
if(!visited[j] && (adjMar[i][j] == 1)) {
System.out.print(verArr[j] + " ");
visited[j] = true;
queue.offer(j);
}
}
}
}
(2)利用邻接表实现的图的广度优先搜索:O(n+e)
'代码描述'
/**
* 用邻接表实现的图的广度优先搜索
*/
public void BFSTraverse() {
System.out.println("广度优先遍历:");
for(int i = 0; i < n; i++) {
visited[i] = false;
}
for(int j = 0; j < n; j++) {
if(!visited[j]) {
BFS(j);
}
}
}
/**
* 辅助方法
* 该部分只能实现图的一个连通子图
* @param n
*/
private void BFS(int n) {
System.out.print(adjList[n].data + " ");
Queue queue = new LinkedList();
visited[n] = true;
queue.offer(n); //当前顶点进队
int i;
ArcNode p;
while(!queue.isEmpty()) {
i = queue.poll();
p = adjList[i].firstArc;
while(p != null) {
if(!visited[p.adjVer]) {
System.out.print(adjList[p.adjVer].data + " ");
queue.offer(p.adjVer);
visited[p.adjVer] = true;
}
p = p.next;
}
}
}
5.最小生成树
含义:生成树中构造连通网的代价最小(即权值最小的)的生成树称为最小生成树
注意:一个连通网的生成树是不唯一的
而对于非连通的网,其各个连通分量的生成树组成的集合为其生成森林。
如何寻找连通网的最小生成树:
(1)普里姆算法(Prim)
(2)克鲁斯卡算法(Kruskal)
1.普里姆算法:
基本思想:设G=(V,E)是具有n个顶点的网,T=(U, TE)为G的最小生成树,U是T的顶点集合,
TE是T的边集。
则其构建过程:
首先从集合V中任选取一点(如V0)放入集合U中,这是U={V0}, TE{Ф},然后选择这样的
一条边:一个顶点在集合U中,另一个顶点在集合V-U中,且权值最小的边(u,v)(u∈U, v∈V-U)
将该边放入TE中,并将顶点v加入到U中。重复上述过程,知道U=V位置,此时TE中有n-1条边,那
么T=(U,TE)就是G的一颗最小生成树。
算法分析:普里姆算法对顶点数量敏感,因此,它适合稠密图
时间复杂度:O(n^2)
"代码描述"
/**
* 建立网的最小生成树
* @param adjMar 表示网图的邻接矩阵
* @param n 表示从第n个顶点开始搜索最小生成树
*/
public void miniSpanTree_Prim(int[][] adjMar, int n) {
//对输入的参数进行校验
if(n > vertexNum || n < 1) {
System.out.println("输入的起始顶点位置有误!");
return;
}
//保存相关顶点下标,
int adjvex[] = new int[vertexNum];
/**
* 用于存放已进入最小生成树中的顶点到其余未进入生成树中的顶点之间
* 权值最小的边
*/
int lowcost[] = new int[vertexNum];
/**
* lowcost[i]对应的值为0就表示将该顶点Vi加入生成树
*/
lowcost[n-1] = 0; //表示将第一个顶点V0加入生成树中
adjvex[n-1] = n-1; //初始化第一个顶点下标为0
for(int i = 0; i < vertexNum; i++) {
//将于V0有关的边的权值存储起来,没有直接相连的也要存储
if(i == n-1) {
continue;
}
lowcost[i] = adjMar[n-1][i];
adjvex[i] = n-1;
}
//构造最小生成树
int min;
int j;
int k;
for(int i = 0; i < vertexNum; i++) {
j = 0;
k = 0;//用于记录当前权值最小的顶点下标
if(i == n-1 || j == n-1) {
//由于n-1时,在一开始已经进行过初始化了,因此这里就不能再进行筛选了
continue;
}
min = MAX; //初始化min的值为无穷大
while(j < vertexNum) {
if(lowcost[j] != 0 && lowcost[j] < min) {
min = lowcost[j];
k = j; //将权值最小的顶点的下标存入K中
}
j++;
}
System.out.println(vertexArr[adjvex[k]] + "" + vertexArr[k]);
lowcost[k] = 0; //将当前顶点的加入生成树种
// 重新更新lowcost[]数组 ,使其始终保持着最小权值
for(j = 0; j < vertexNum; j++) {
if((j != n-1) && lowcost[j] != 0 && adjMar[k][j] < lowcost[j]) {
lowcost[j] = adjMar[k][j];
adjvex[j] = k; //将下标为k的顶点存入adjvex[]中,
}
}
}
}
2.克鲁斯卡尔算法:
基本思想:是按权值递增的次序来构造的,使生成树中每一条边的权值尽可能地小。
构建过程:
先构造一个只含n个顶点的子图G',然后从网中权值最小的边开始,若它的添加不
使G'中产生回路,则在G'上加上这条边,如此反复,直到G'中加上 n-1 条边为止。
算法分析:克鲁斯卡尔算法是针对边展开的,因此,它适合稀疏图
时间复杂度:O(eloge)
"代码描述"
对网的边封装成一个类
public class Edge {
int begin; //表示该边的起始端点
int end; //表示该边的结束端点
int weight; //表示该边上的权值
}
/**
* 克鲁斯卡尔求最小生成树的算法
*/
public void miniSpanTree_Kruskal(int[][] adjMar) {
Edge[] edges; //定义边集数组
//定义一个数组用来判断边与边是否形成环路
int[] parent = new int[vertexNum];
egdes = adjMarToEdge(adjMar); //这里只是一个虚拟方法,并没有真正去实现它。实际上实现也很容易
//对parent[]数组进行初始化,所有的顶点均没有进入最小生成树中
for(int i = 0; i < vertexNum; i++) {
parent[i] = 0;
}
int n;
int m;
//对每一条边结合edges[], parent[]进行扫面检查,
for(int j = 0; j < edgesNum; j++) {
n = find(parent, edges[j].begin);
m = find(parent, edges[j].end);
if(n != m) {
//n != m , 表明边(n, m)还没有进入最小生成树中。
parent[n] = m; //将此边的结尾顶点放入下标为起点的parent中,表示此边(n,m)已经进入最小生成树
//打印出已加入最小生成树中的边
System.out.println(vertexArr[edges[j].begin] + "" + vertexArr[edges[j].end]);
}
}
}
//查找连线顶点的尾部下标
private int find(int[] parent, int f) {
//如果parent[f] = 0,则表明该顶点还没有进入最小生成树中
//此部分时间复杂度:O(loge), e表示边数
while(parent[f] > 0) {
/**
* 表明该顶点(准确来说是 边(f,parent[f]) 的结束点parent[f])已经进入最
* 小生成树中,因此,要继续追踪下去,看看以此结束点parent[f]为起始点的
* 下一条边的结束点是什么样的?有可能和已进入最小生成树的顶点构成环
*/
f = parent[f];
}
return f;
}
6.最短路径
对于网图(有权值)和非网图(权值可以看成都是1)来说,他们的最短路径含义是不同的。
非网图:是指两顶点之间经过的边数最少的路径
网图:是指两顶点之间经过的权值之和最小的路径
我们主要研究网图的最短路径,且第一个顶点是源点,最后一个顶点是终点。
求最短路径的算法:
迪杰斯特拉算法(Dijkstra)
弗洛伊德算法(Floyd)
1.迪杰斯特拉算法
是求某一点到其余各点的最短路径问题的,其核心思想是穷举法,因此计算效率不高
整个过程是基于已经求出的最短路径的基础,在来求得更远顶点的最短路径。最终,可以
得到我们想要到的那个点的最短路径。
算法分析:时间复杂度O(n^2)
"代码描述"
/**
* 计算从顶点n到其余各点的最短距离
* 注意:此算法目前只能计算无向图,对于有向图需要修改代码
* @param adjMar 网图的邻接矩阵
* @param n 起始顶点
*/
public void dijisktraShortPath(int[][] adjMar, int n) {
//对输入的参数进行校验
if(n < 1 || n > vertexNum) {
System.out.println("输入的起始顶点不合理");
return;
}
int s = n - 1;
//用于记录是否已经计算出s到vi点的最短路径,
int[] isFinal = new int[vertexNum]; //isFinal[w]=1时,表示已确定到点w的最短路径
int[] distance = new int[vertexNum]; //表示s到vi的最短路径长度
int[] path = new int[vertexNum]; //表示在最短路径上到当前点的前驱顶点下标, -1表示到此点没有路径
for(int i = 0; i < vertexNum; i++) {
isFinal[i] = 0; //全部置为0,表示还没有找到到任何点的最短路径
distance[i] = adjMar[s][i]; //初始化s到其余各点最短路径长度
/**
* 默认没有路径,初始化为s,是为了后面好处理
* 这里最好的初始化为-1来表示没有路径,但对于无向图来说无所谓,因为网中只要没有
* 孤立顶点,那么两个顶点之间一定是连通的额,但是对于有向图而言,情况就不一样了
* 这里就应该初始化为-1
*/
path[i] = s;
}
distance[s] = 0; //s到s的路径长度为0
isFinal[s] = 1; //s到s的最短路径是不需要确定的,也就相当于已经确定了
path[s] = s; //表示s到s的前驱点下标为自身
int min; //记录s到vi的最短距离
int k = 0; //辅助变量,用于记录每次确定到s最短距离的那个顶点的下标
//计算s到所有顶点的最短路径
for(int j = 0; j < vertexNum; j++) {
if(j == s) {
continue;
}
min = MAX;
//寻找离s最近的顶点
for(int w = 0; w < vertexNum; w++) {
if(isFinal[w] == 0 && distance[w] < min) {
k = w;
min = distance[w];
}
}
isFinal[k] = 1; //表明s到k顶点的最短路径已确定
// 更新当前最短路径及距离
for(int t = 0; t < vertexNum; t++) {
if(isFinal[t] == 0 && (min + adjMar[k][t] < distance[t])) {
//表明找到了更短的距离
distance[t] = min + adjMar[k][t];
path[t] = k;
}
}
}
//打印s到各点的最短路径
displayPath(s, path);
}
/**
* 根据前驱节点数组打印最短路径
* @param s
* @param path
*/
private void displayPath(int s, int[] path) {
for(int i = 0; i < path.length; i++) {
System.out.print(path[i] + " ");
}
System.out.println();
int temp;
for(int i = 0; i < path.length; i++) {
System.out.print(vertexArr[s] + "->" + vertexArr[i] + ": " + i);
temp = i;
while(path[temp] != s) {
System.out.print(" " + path[temp]);
temp = path[temp];
if(temp == -1) {
break;
}
}
System.out.println(" " + s);
}
}
2.弗洛伊德算法
是求所有顶点到所有顶点的最短路径。
算法分析:O(n^3)
"代码描述"
/**
* 计算从顶点n到其余各点的最短距离
* 注意:此算法目前只能计算无向图,对于有向图需要修改代码
* @param graph 网图的邻接矩阵
* @param n 起始顶点
*/
public void floydShortPath() {
//定义初始化最短路径矩阵,初始状态为网图的邻接矩阵,可以用来计算最小路径长度
int[][] shortPath = new int[MAXSIZE][MAXSIZE];
//定义前驱结点矩阵,可以用来求出最短路径
int[][] path = new int[MAXSIZE][MAXSIZE];
//初始化前驱节点矩阵和最短路径权值矩阵
for(int i = 0; i < vertexNum; i++) {
for(int j = 0; j < vertexNum; j++) {
shortPath[i][j] = adjMar[i][j];
path[i][j] = j;
}
}
/**
* 是决定该算法时间复杂度的主要地方:
* 三层循环,很容易得知时间复杂度为:O(n^3)
* 计算所有点其他各点的最短路径
* w表示中转顶点的下标,m是起点,n是结束顶点
* 挨着以w控制的顶点作为中转点,来求所有点到其余所有点的最短路径距离
*/
for(int w = 0; w < vertexNum; w++) {
for(int m = 0; m < vertexNum; m++) {
/**
* 注意:这个n当然也可以是从0开始,但是下面的
* shortPath[n][m] = shortPath[m][n];
* 显得多余了, 但是对于有向图就不能这么写了。要注意改写
* 改写成下面这种形式,可以减少循环的次数,从而提高程序部分性能
*/
for(int n = m; n < vertexNum; n++) {
if(shortPath[m][n] > (shortPath[m][w] + shortPath[w][n])) {
//如果经过下标为w的顶点比原来两点间的路径更短,就要修正这两点间的距离
shortPath[m][n] = shortPath[m][w] + shortPath[w][n];
shortPath[n][m] = shortPath[m][n]; //因为该矩阵是对称的,所以可以这么写
/**
* 修改路径经过下标为w的顶点,表示到相应的点时,要经过该点,
* 准确说,这个点是到达该点的前驱结点。
*/
path[m][n] = path[m][w];
path[n][m] = path[m][n]; //因为该矩阵是对称的,所以可以这么写
}
}
}
}
//显示最短路径
displayPath(path, shortPath);
}
/**
* 打印所有点到其余所有点的最短路径
* @param path 最短路径的前驱结点下标矩阵(在该矩阵上纵向打印路径)
* @param shortPath 最短路径矩阵
*/
private void displayPath(int[][] path, int[][] shortPath) {
int k;
for(int i = 0; i < vertexNum; i++) {
for(int j = i; j < vertexNum; j++) {
System.out.print(i + " " + j + " (" + shortPath[i][j] + ") ");
k = path[i][j]; //获得第一个路径顶点下标
System.out.print("path: " + i);
while(k != j) {
System.out.print("->" + k);
k = path[k][j]; //获得下一个路径顶点的下标
}
System.out.println("->" + j);
}
System.out.println();
}
}
思考:分析迪杰斯特啦和弗洛伊德求最短路径的算法,二者均可求出所有点到其余所有点
之间的最短路径。只不过对于迪杰斯特拉算法而言,需要循环调用迪杰斯特拉算法,时间
复杂度为O(n^3), 而弗洛伊德算法只需一次调用即可全部计算出来,尽管其时间复杂度也
为O(n^3),弗洛伊德算法更加简洁。
7.拓扑排序(Topological Sort)
这里的排序并不是平时理解的安大小排序的,而是安工作步骤先后顺序排序的,主要解决一个工程能否
顺利进行。
用于工程施工、生产流程、教学安排等应用中来合理安排工作流程步骤
这些流程图都是无环的有向图(图中的活动都是有先后顺序的)
(1)AOV网(Activity On Vertex Network): 在一个表示工程的有向图中,用顶点表示活动,用弧表示
活动之间的优先关系,这样的有向图称为AOV网
注意:AOV网中没有回路
(2)拓扑序列:设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列V1,V2,...Vn,满足若从Vi到Vj有
一条路径,则在顶点序列中顶点Vi必在顶点Vj之前,则我们成这样的顶点序列为拓扑序列。
(3)拓扑排序:就是对一个有向图构造拓扑序列的过程
(4)拓扑排序算法:
基本思想:从AOV网中选择一个入读为0的顶点输出,然后删除此顶点,并删除以此顶点为尾的弧,
继续重复此步骤,知道输出全部顶点或者AOV网中不存在入读为0的顶点为止。
因为在排序的过程中要频繁删除顶点,因此选择使用邻接表来做存储结构(而不用邻接矩阵做存储结构)
由于要查找入度为0的顶点,需要在邻接表的顶点结构中增加入度域in,
邻接表顶点结构:
┌─────┬──────┬───────────┐
│ in │ data │ firstedge │
└─────┴──────┴───────────┘
算法分析:O(n+e)
"代码描述"
1.邻接表顶点结构
public class VerNode {
public T data; //存储顶点的名称或相关信息
public ArcNode firstArc; //指向顶点Vi的第一个邻接点的边结点
public int in;//增加一个入度域,方便拓扑排序时
public VerNode() {
this.data = null;
this.firstArc = null;
}
}
2.拓扑排序
/**
* 进行拓扑排序
* 要用到栈这个数据结构,我们就选用前面栈一章写的栈数据结构
*/
public void topologicalSort() {
//定义一个栈(用的之前在栈一章的数据结构),用于存储入度为0的顶点
LinkStack> stack = new LinkStack>();
//将入度为0的顶点存入栈中:O(n)
for(int i = 0; i < n; i++) {
if(adjList[i].in == 0) {
stack.push(adjList[i]);
}
}
int count = 0; //用于记录输出顶点的个数
ArcNode arcEdge; //用于循环使用, 邻接表结构中的边表结点结构
VerNode p; //用于循环使用, 邻接表结构中的顶点结构
int k; //用于循环接受边表中结点在顶点表中的位置
//当栈不为空,循环始终进行
while(!stack.isEmpty()) {
//打印此入度为0的顶点
p = stack.pop();
if(count == (n - 1)) {
System.out.print(p.data);
}else{
System.out.print(p.data + "->");
}
//统计输出的顶点数
count ++;
arcEdge = p.firstArc;
//处理与p邻接的结点:O(e)
while(arcEdge != null) {
k = arcEdge.adjVer;
/**
* --adjList[k].in ,这个动作就表明将到与上面p结点相连的
* 结点的之间的边删除了
*/
if((--adjList[k].in) == 0) {
stack.push(adjList[k]); //将入度为0的顶点入栈
}
arcEdge = arcEdge.next;
}
}
//可用于检查网中有没有环
if (count < n) {
//System.out.println("网中存有环");
}else {
//System.out.println("正常");
}
}
8.关键路径
拓扑排序主要是为解决一个工程能否顺序进行的问题,而关键路径主要是解决工程完成需要的最短时间的问题
(1)AOE网(Activity On Edge NetWork)
在一个表示工程的的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的
持续时间,这种有向图的边表示活动网,我们称之为AOE网
源点:没有入边,只有出边
汇点:没有出边,只有入边
注意:AOV网的顶点表示活动, 边表示活动之间的优先关系
AOE网的边表示活动,边也表示优先关系,边上的权值表示活动持续的时间
AOE网是建立在活动之间制约关系(先后关系)没有矛盾的基础上,再来分析完成整个工程至少需要多少时间
(2)关键路径
把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点的最大长度的路径叫关键路径,
在关键路径上的活动叫关键活动。
那么,如果我们想缩短整个工程活动的完成时间,就只需在关键路径上算短相应的活动时间就能
做到整体工期缩短
(3)关键路径的算法实现
基本思想:在整个AOE网上寻找所有活动的最早开始时间和最晚开始时间,并比较它们,如果而二者
相等,则表明该活动是关键活动(当然也就是关键路径上的一段了),如果不想等那就不是关键活动
需要知道的几个参数:
1)事件的最早发生时间etv(earliest tiem of vertex):即顶点vk的最早发生时间
注意:etv[i]的求解必须在所有前驱的最早发生时间求得之后才能确定,因此是在拓扑有序的基础上进行的
2)事件的最晚发生时间ltv:即在不推迟整个工程的前提下,顶点vk的最晚发生时间
注意:ltv[i]必须在其所有后继的最晚发生时间求得之后才能确定,因此是在逆拓扑有序的基础上进行的
3)活动所需的时间:t(i,j),即AOE网上的边的权值
4)活动最早开工时间ete(earliest time of edge):即弧ak的最早发生时间
ete(i,j) = etv[i];
5)活动最晚开工时间lte:即在不推迟整个工程的前提下,弧ak的最晚发生时间
lte(i,j) = ltv[i] - t(i,j)
由 etv 和 ltv 求得ete, lte
当 ete[k] == lte[k] 时,该活动为关键活动
算法描述:
时间复杂度:O(n+e)
"代码描述"
1.顶点表结点结构
//表示顶点表结点的结构
public class VerNode {
public T data; //存储顶点的名称或相关信息
public ArcNode firstArc; //指向顶点Vi的第一个邻接点的边结点
public int in;//增加一个入度域,方便拓扑排序时
public VerNode() {
this.data = null;
this.firstArc = null;
}
}
2.边表结点的结构
public class ArcNode {
public int adjVer; //邻接点域,用于存储该该点结点域数组中的下标
public int weight; //用于网图存储权值
public ArcNode next; //指向下一个邻接点
public ArcNode() {
adjVer = 0;
weight = 0;
next = null;
}
}
3.关键路径的算法实现
/**
* 寻找关键路径:是建立在拓扑序列的基础上进行相关关键路径的寻找
*/
public void getCriticalPath() {
Object[] obj = topologicalSort(); //拓扑排序:O(n+e)
//通过拓扑排序返回各个事件最早完成时间
int[] eventEarly = (int[])obj[0];
//返回拓扑序列
LinkStack topPath = (LinkStack)obj[1];
int[] eventLast = new int[n];//定义事件发生最晚的数组,好好理解这个
int actEarly; //活动最早发生时间变量
int actLast; //活动最晚发生时间变量
//初始化最晚事件数组:O(n)
for(int i = 0; i < n; i++) {
/**
* 因为事件的最晚发生时间必须在其所有后继的最迟发生的时间确定后才能确定的
* 而最后一个事件(汇点)的最晚发生时间就要保正整个工程部耽误,因此,它的最晚
* 发生时间就是整个工程的最早发生时间。
* 因此,才会这样初始化
*/
eventLast[i] = eventEarly[n-1];
}
int temp; //循环中,用来临时存储topPath出栈的顶点下标号
int k; //用于循环接受边表中结点在顶点表中的位置
ArcNode p;//用于循环使用, 邻接表结构中的边表结点结构
//计算各个顶点的最晚发生时间,整个过程是在逆拓扑序列的基础上进行的:O(n+e)
while(!topPath.isEmpty()) {
//将拓扑序列顺序出栈
temp = topPath.pop();
p = adjList[temp].firstArc;
while(p != null) {
k = p.adjVer;
if((eventLast[k] - p.weight) < eventLast[temp]) {
eventLast[temp] = eventLast[k] - p.weight;
}
p = p.next;
}
}
//求关键活动,当活动的最早时间和最晚时间是一样的时候,就表明该
//活动间的路径为关键活动:O(n+e)
for(int j = 0; j < n; j++) {
p = adjList[j].firstArc;
while(p != null) {
k = p.adjVer;
actEarly = eventEarly[j]; //活动最早发生时间
actLast = eventLast[k] - p.weight; //活动的最晚发生时间
if(actEarly == actLast) {
//两者相等则表明该活动在关键路径上
System.out.print("<" +adjList[j].data + "," + adjList[k].data +">(" +p.weight+") ");
}
p = p.next;
}
}
}
/**
* 进行拓扑排序:O(n+e)
* 要用到栈这个数据结构,我们就选用前面栈一章写的栈数据结构
*/
private Object[] topologicalSort() {
/**
* 定义一个栈,用于存储入度为0的顶点
* 存储拓扑序列的顶点下标号(即该顶点在邻接表的顶点数组中的存储位置,从0开始的)
* 为啥不直接存储该顶点呢?
* 一开始也是采用存储的顶点,但是该顶点结构在存储的时候并没有存储其位置
* 信息,导致下面在计算各顶点的最早事件发生时间时,无法获取存储相应的最早事件发生的
* 数组中的元素内容。相反直接存储入度为0的顶点的下标号,那么想找该顶点时可以直接在adjList[]数组中获取。
*/
LinkStack stack = new LinkStack();
//定义事件发生最早的数组(最早发生即到该点的最长路径)
int[] eventEarly = new int[n];
//将入度为0的顶点存入栈中,同时初始化earlyTiem
for(int i = 0; i < n; i++) {
eventEarly[i] = 0; //全部初始化为0
if(adjList[i].in == 0) {
stack.push(i);
}
}
/**
* 拓扑序列不用直接在控制台打印出来,而是用topPath存储这些序列顶点
* 同上面的stack一样,也是直接存储相应的顶点下标号
*/
LinkStack topPath = new LinkStack();
int count = 0; //用于记录输出顶点的个数
ArcNode arcEdge; //用于循环使用, 邻接表结构中的边表结点结构
VerNode p; //用于循环使用, 邻接表结构中的顶点结构
int k; //用于循环接受边表中结点在顶点表中的位置
int temp; //用于临时存储从stack中取出的顶点下标号
//当栈不为空,循环继续进行
while(!stack.isEmpty()) {
//打印此入度为0的顶点
temp = stack.pop();
p = adjList[temp]; //由下标号结合adjList[]可直接获取对应的顶点
topPath.push(temp); //将拓扑顶点标号顺序存入,不用在控制台打印出来
//统计输出的顶点数
count ++;
arcEdge = p.firstArc; //获取与p邻接的顶点
//处理与p邻接的结点
while(arcEdge != null) {
k = arcEdge.adjVer; //该顶点在顶点数组中的存储位置
/**
* --adjList[k].in ,这个动作就表明将到与上面p顶点相连的
* 顶点之间的边删除了,同时该点的入度也就跟着减1
*/
if((--adjList[k].in) == 0) {
//stack.push(adjList[k]); //将入度为0的顶点入栈
stack.push(k); //将入度为0的顶点入栈
}
//计算该顶点(事件)的最早发生时间
if((eventEarly[temp] + arcEdge.weight) > eventEarly[k]) {
eventEarly[k] = eventEarly[temp] + arcEdge.weight;
}
arcEdge = arcEdge.next;
}
}
//可用于检查网中有没有环
if (count < n) {
//System.out.println("网中存有环");
}else {
//System.out.println("正常");
}
//返回两个重要的结果
return new Object[]{eventEarly, topPath};
}