图是很有用的数据结构,在解决最短路径、工程规划时有很重要的作用。
一、图的定义
1.1图的定义
图是由顶点的有穷非空集合和顶点指点的边的集合组成,通常表示尾:G(V,E),其中,G表示一个图,V是图G中的顶点的集合,E是图G中边的集合。
下面三个数据结构都可以称为图
1.2有向图、无向图和网
无向图:图中所有的边都没有方向。
完全无向图:图中任意两个顶点之间都存在边。含有n个顶点的完全无向图的边的总数是n(n-1)/2。
有向图:图中所有边都有方向。假设顶点A到顶点B之前存在有向边,从A指向B,则边可以用有序偶<A,B>来表示,A为弧尾,B为弧头。有向边也称为弧。
完全有向图:图中任意两点都有方向相反的两条有向边。含有n个顶点的完全有向图的边的总是是n(n-1)。
网:有些图的边具有与它相关的数字,这种与图的边相关的数字叫做权。这些权可以表示从一个顶点到另外一个顶点的距离或耗费。这种带权的图通常称为网。
度、入度、出度
度:对于无向图来说,一个顶点的度就是与该顶点相关联的边的数量。有n个顶点的无向图的度等于边数的两倍。
入度:对于有向图来说,一个顶点的入度是弧头是该顶点的弧的数量。
出度:对于有向图来说,一个顶点的出度是弧尾是该顶点的弧的数量。
1.3路径、环
路径:图G=(V,E)中从顶点A到顶点D的路径是一个顶点序列。路径的长度是路径上边的数量。
环:第一个顶点到最后一个顶点相同的路径称为回路或环。
1.4连通图
在无向图中,如果顶点A到顶点B有路径,则称A和B是联通的。如果图中任意两个顶点都是联通的,则该图称为连通图。
在有向图中,对于每一对顶点A,B,从A到B和从B到A都存在路径,则称该图是强联通图。
二、图的存储结构
我们可以在纸上把图画出来,但是图在程序中应该怎样表示?
2.1 邻接矩阵
邻接矩阵存储方法是用一个一维数组和一个二维数组来表示图。一维数组存储图中顶点的信息,二维数组存储图的边或弧的信息。
上图中,假如我们要找以弧尾为顶点V0的弧,就看二维数组的第一行(V0 - V4)。加入我们要以弧头为V0的弧,就看第一列(V1 - V0,V2 - V0)。
2.2 邻接表
与HashMap的存储结构有些类似。用一个一维数组存储结点,每个结点的邻接点形成一个链表。
三、图的遍历
有时候我们需要从图的某一顶点出发,以某种方式遍历图的每个顶点,并且每个顶点只会被访问一次。
3.1深度优先遍历
以深度优先的方法来遍历图。主要用到深度优先搜索和回溯。
以邻接矩阵表示的图的深度优先搜索的伪代码
/**
* 邻接矩阵表示的图的深度优先搜索
* @author TinyMonster
*
*/
public class DFSAdjMat {
boolean[] visited;
public void DFSTraverse(int[][] graph) {
visited = new boolean[graph.length]; //初始化访问数组,记录每个结点的访问情况
for(int i=0;i<graph.length;i++) { //遍历每个结点,使用深度优先搜索
if(!visited[i]) {
DFS(graph,i);
}
}
}
private void DFS(int[][] graph,int i) {
int j = 0;
visited[i] = true;
System.out.println(i); //打印结点
for(j = 1;j<graph[i].length;j++) { //遍历该结点的所有邻接结点,如果邻接结点没有被访问过,访问该邻接结点
if(!visited[j] && graph[i][j] == 1)
DFS(graph,j);
}
}
}
以邻接表表示的图的深度优先搜索的伪代码
/**
* 邻接表表示的图的深度优先搜索
* @author TinyMonster
*
*/
public class DFSAdjList {
boolean[] visited;
public void DFSAdjListTraverse(Node[] AdjList) {
visited = new boolean[AdjList.length]; //初始化访问数组,记录每个结点的访问情况
for(int i = 0;i<AdjList.length;i++) {
if(visited[i]) //遍历每个结边表中的结点,如果结点没有访问过,对结点进行深度优先搜索
DFS(AdjList,i);
}
}
private void DFS(Node[] AdjList,int i) {
visited[i] = true; //将结点标记为已访问
Node cur = AdjList[i];
System.out.println(cur.data);
Node next = cur.next;
while(next!=null) { //遍历该结点的邻接结点,如果邻接结点没有被访问过,访问该邻接结点
if(!visited[next.data]) {
DFS(AdjList,next.data);
}else {
next = next.next;
}
}
}
private class Node{
int data;
Node next;
}
}
3.2 图的广度优先搜索
图的广度优先搜索使用了队列这一数据结构
邻接矩阵表示的图的深度优先搜索
/**
* 邻接矩阵表示的图的广度优先搜索
* @author TinyMonster
*
*/
public class BFSAdjMat {
boolean[] visited;
public void BFSTraverse(int[][] graph) {
visited = new boolean[graph.length]; //初始化访问数组,记录每个结点的访问情况
for(int i=0;i<visited.length;i++) { //遍历每个结边表中的结点,如果结点没有访问过,对结点进行广度优先搜索
if(!visited[i])
BFS(graph,i);
}
}
private void BFS(int[][] graph,int i) {
Queue<Integer> queue = new LinkedList<Integer>(); //使用队列来进行广度优先搜索
queue.offer(i);
while(!queue.isEmpty()) {
int cur = queue.poll();
visited[cur] = true;
System.out.println(cur);
for(int j=1;j<graph[cur].length;j++) { //将当前结点的所有邻接结点放入队列中
if(!visited[j]&&graph[cur][j]==1) {
queue.offer(j);
}
}
}
}
}
邻接表表示的图的广度优先搜索
/**
* 邻接表表示的图的广度优先搜索
* @author TinyMonster
*
*/
public class BFSAdjList {
boolean[] visited;
public void BFSTraverse(Node[] AdjList) {
visited = new boolean[AdjList.length]; //初始化访问数组,记录每个结点的访问情况
for(int i=0;i<visited.length;i++) { //遍历每个结边表中的结点,如果结点没有访问过,对结点进行广度优先搜索
if(!visited[i])
BFS(AdjList,i);
}
}
private void BFS(Node[] AdjList,int i) {
Queue<Integer> queue = new LinkedList<Integer>(); //使用队列来进行广度优先搜索
queue.offer(i);
while(!queue.isEmpty()) {
int j = queue.poll();
Node cur = AdjList[j];
visited[j] = true; //打印数据
System.out.println(j);
Node next = cur.next;
while(next!=null) { //遍历该结点的所有邻接结点,压入队列中
queue.offer(next.data);
next = next.next;
}
}
}
private class Node{
int data;
Node next;
}
}
四、图的最小生成树
加入你是电信的实施工程师,需要为一个镇的九个村庄假设通信网络做设计,村庄位置大致如下图所示。其中每个结点表示一个村庄,边上的数据表示路径长短。我们要设计一个最短的路径。这就需要最小生成树的知识。
我们知道一个图的生成树是一个极小的连接子图,它含有图中的全部顶点,但是只有足以构成一颗树的n-1条边。
我们把构造连通网的最小代价生成树称为最小生成树。
求一个图的最小生成树可以用以下方法。
普里姆算法
该算法主要一个数组来保存每个结点在路径中的前一个结点,使用另外一个数组来保存到达每个结点的最短距离。通过广度优先搜索,不断扩大搜索范围,不断更新两个数组,最终完成最小生成树的搜索。
一个图和它的邻接矩阵如下图所示。邻接矩阵中,∞表示两个结点之间没有边。
我们定义两个数组adjvex和lowcost。其中adjvex表示每个结点在最小生成树中路径的上一个结点。lowcost表示每个结点在最小生成树中上一个结点到达该结点的最短路径。
伪代码
/**
* 图的最小生成树
* @author TinyMonster
*
*/
public class MiniSpanTreePrim {
public void MiniSpanTreePrim(int[][] graph) {
int min;
int[] adjvex = new int[graph.length];
int[] lowcost = new int[graph.length];
for(int i=1;i<graph.length;i++) { //初始化lowcost数组
lowcost[i] = graph[0][i];
}
for(int i=1;i<graph.length;i++) {
min = Integer.MAX_VALUE;
int nextNode = 0;
for(int j=1;j<graph.length;j++) { //找到当前最短的路径
if(lowcost[j]!=0&&lowcost[j]<min) {
min = lowcost[j];
nextNode = j;
}
}
lowcost[nextNode]=0;
System.out.println("路径:"+adjvex[nextNode]+"->"+nextNode); //打印路径
for(int j=1;j<graph.length;j++) { //更新最短路径和adjvex
if(lowcost[j]!=0&&graph[nextNode][j]<lowcost[j]) {
lowcost[j]=graph[nextNode][j];
adjvex[j]=nextNode;
}
}
}
}
}
五、图中两个结点的最短路径
生活中我们经常会需要找到从一个点到另外一个点的最短距离,这时候可以用图的最短路径来解决。
5.1迪杰斯特拉算法
与普里姆算法类似,通过不断搜索周围结点,找到最短路径。
public class ShortestPath {
public void ShortestPath(int[][] graph,int v0,int[] Patharc,int[] ShortPathValue) {
boolean[] finish = new boolean[graph.length];
int min = Integer.MIN_VALUE;
int next = v0;
for(int i=0;i<graph[v0].length;i++) {
ShortPathValue[i] = graph[v0][i];
Patharc[i] = v0;
}
ShortPathValue[v0] = 0;
finish[v0] = true;
for(int i=1;i<graph.length;i++) { //每次求一个点的最短路径
min = Integer.MAX_VALUE; //初始化最短路径
for(int j=0;j<graph.length;j++) {
if(!finish[j]&&ShortPathValue[j]<min) {
next = j;
min = ShortPathValue[j];
}
}
finish[min] = true;
for(int j=0;j<graph.length;j++) {
if(!finish[j]&&(min+graph[min][j])<ShortPathValue[j]) {
ShortPathValue[j] = min+graph[min][j];
Patharc[j] = min;
}
}
}
}
}
六、拓扑排序
在生活中,完成一个事情可以分为若干个步骤,但是这些步骤之前有依赖关系。比如要拍摄一部电影,可以分为完成据本,确定演员,进行拍摄。首先要在完成据本和确定演员之后才能进行拍摄。
在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称为AOV网。
拓扑排序的定义:设G=(V,E)是一个具有n个顶点的有序列向图,V中顶点序列v1,v2...vn,满足若从顶点vi到vj有一条路径,则在顶点序列中顶点vi必定在顶点vj之前。则我们称这样的顶点序列为一个拓扑排序。
一个AOV网的拓扑排序可能不止一条。所谓拓扑排序,其实就是对一个有向图构造拓扑排序的过程。构造时会有两个结果。如果拓扑序列包含了AOV网的所有结点,那么这个AOV网中不存在环,否则,这个AOV网存在环。
6.1拓扑排序算法
对AOV网进行拓扑排序的基本思路是:从AOV网中选择一个入度为0的顶点输出,然后删去此顶点,并删除以此顶点为尾的弧,继续重复此操作,知道输出全部顶点或者AOV网中不存在入度尾0的顶点为止。
在拓扑排序中需要删除顶点,我们用邻接表的存储结构更加方便。
/**
* AOV网的拓扑排序
* @author TinyMonster
*
*/
public class TopoSort {
public boolean TopoSort(EdgeNode[] head) {
Stack<EdgeNode> stack = new Stack<EdgeNode>(); //存储入度等于0的结点
EdgeNode cur = null; //当前顶点结点
Node next = null; //当前边结点
int count = 0; //拓扑序列中结点数量
for(int i=0;i<head.length;i++) { //将所有入度为0的结点压栈
if(head[i].in==0) {
stack.push(head[i]);
}
}
while(!stack.isEmpty()) {
cur = stack.pop();
System.out.println(cur.data); //打印序列
count++; //拓扑序列数量+1
next = cur.next; //获取顶点结点的下一个边结点
while(next!=null) { //遍历所有边结点,将入度-1,如果入度==0,将结点压入栈中
if(head[next.adjvex].in>0) {
head[next.adjvex].in --;
if(head[next.adjvex].in == 0) {
stack.push(head[next.adjvex]);
}
}
}
}
return count == head.length; //如果拓扑序列元素数量等于AOV网结点总数,该AOV网可以进行拓扑排序,AOV网中没有环
}
private class EdgeNode{ //邻接表的顶点
int in;//入度
int data;//数据
Node next;
}
private class Node{ //连接表的边结点
int adjvex; //该点对应的顶点的下标
Node next;
}
}