一,图的定义
在图中的数据元素通常称为顶点,图(Graph)是由顶点集合(Vertex)及顶点之间的关系集合(Edge)组成的一种数据结构。记为G=(V,E)。
我们把左图称为无向图G1。
G1=(V1,E1)
其中V1={A, B, C, D, E}
E1={(A, D),(B, C),(B, D),(B, E),(D, E)}
右图称为有向图G2。
G2=(V2, E2)
其中V2={A, B, C, D}
E2={<A, B>, <A, C>, <D, A>, <D, B>}
关于图的基本术语:
在图中,根据顶点之间的关系是否有方向性可将图分为有向图和无向图。
无向图:对于无向图,顶点的关系为无向边,用圆括号表示,如(x,y)。
有向图:对于有向图来说,顶点间的关系称为有向边,用尖括号表示,如<x,y>。
由于无向图中边的取值范围:0≤e≤n(n-1)/2。
有向图中弧的取值范围:0≤e≤n(n-1)。
其中n表示图中的顶点数目,e表示边数。
完全图:有 n(n-1)/2 条边的无向图(即: 每两个顶点之间都存在着一条边)称为完全图。
有 n (n - 1) 条弧的有向图 (即:每两个顶点之间都存在着方向相反的两条弧)称为有向完全图。
稀疏图:含有很少条边或弧的图。
稠密图:含有很多条边或弧的接近完全图的图。
权:与图的边或弧相关的数,权值可以是距离,时间,价格等。
网: 带权的图。
子图:若有两个图 G1和G2,其中G1=(V1,E1) ,G2=(V2,E2) ,且满足如下条件:V2 属于 V1,E2 属于 E1
即V2 为V1 的子集,E2 为E1 的子集,则称图 G2为图G1 的子图。
如图所示,后面的两个图就是前面图的子图。
邻接点和度:
对于无向图,假若顶点v 和顶点w 之间存在一条边,则称顶点v 和w 互为邻接点。和顶点v 关联的边的数目定义为v的度。记为ID(V)。
对于有向图,由于弧有方向性,则有入度和出度之分。顶点的出度是以顶点v 为弧尾的弧的数目,记为OD(V)。顶点的入度是以顶点v为弧头的弧的数目,记为ID(V)。
路径长度:图中两个顶点之间的路径为两个顶点之间的顶点序列,路径上所含边的数目。
简单路径:若序列中第一个顶点和最后一个顶点相同的路径称为回路或环,序列中顶点不重复出现的路径。
简单回路:若序列中除第一个顶点和最后一个顶点相同外,其余顶点不重复的回路。
连通:从顶点 v 到 v´ 有路径,则说 v 和 v´ 是连通的。
连通图:图中任意两个顶点都是连通的。
非连通图:图中并非任意两个顶点都是连通的。
连通分量:无向图的极大连通子图。
强连通图:有向图的任意两个顶点之间都存在一条有向路径。
强连通分量:有向图中极大的强连通子图
二,图的存储结构
1.邻接矩阵
用两个数组来表示图,一个一维数组,存储图中顶点的信息,一个数二维数组,即矩阵,存储顶点之间相邻的信息,也就是边(或弧)的信息。
设图G=(V,E),有n 个顶点,则其所对应的邻接矩阵A 是按如下定义的一个 二维数组:
对于无向图:
无向图的邻接矩阵的特点?
一定是对称矩阵。
如何求顶点i的度?
邻接矩阵的第i行(或第i列)非零元素的个数。
如何求顶点 i 的所有邻接点?
将数组中第 i 行元素扫描一遍,若a[i][j]为1,则顶点 j 为顶点 i 的邻接点。
对于有向图:
如何求顶点 i 的出度、入度?
邻接矩阵的第 i 行元素之和,第i列元素之和。
邻接矩阵的优点:很容易确定图中任意两个顶点之间是否有边相连(邻接)
不足:但要确定图中有多少条边,则必须按行、按列对每个元素进行检测,所花费的时间代价很大。
邻接矩阵存储空间为 O(n^2),所以适用于稠密图。
public class Graph<T>
{
protected final int MAXSIZE=10;//邻接矩阵可以表示的最大顶点数
protected final int MAX=999; //在网中,表示没有联系,(权值无限大)
protected T[] V;//顶点信息
protected int[][] arcs;//邻接矩阵
protected int e;//边数
protected int n;//顶点数
public Graph( ) {
V = (T[]) new Object [MAXSIZE];
arcs = new int [MAXSIZE] [MAXSIZE];
}
public void CreateAdj( ){
//创建无向图的邻接矩阵 算法6-3
}
//查找顶点,存在返回索引,不存在则返回-1.--算法6-1
public int LocateVex(T v){ return -1; }
//显示邻接矩阵--算法6-2
public void DisplayAdjMatrix(){ }
}
//算法6-1
public int LocateVex(T v){
int i;
for(i=0;i<n;i++)if(V[i]==v)return i;
return -1;
}
//算法6-2
public void DisplayAdjMatrix( ){
int i,j;
System.out.println("图的邻接矩阵表示:"); for(i=0;i<n;i++){
for(j=0;j<n;j++){
System.out.print(" "+arcs[i][j]);
}
System.out.println();
}
}
//算法6-3
public void CreateAdj( ) {
int i,j,k;
T v1;T v2;
Scanner in = new Scanner (System.in) ;
System.out.println("请输入图的顶点数及边数");
System.out.print("顶点数 n="); n=in.nextInt();
System.out.print("边数 e="); e=in.nextInt();
System.out.print("请输入图的顶点信息:");
String str = in.next();
for(i=0;i<n;i++) V[i]=(T)(Object)str.charAt(i);
for(i=0;i<n;i++)
for(j=0;j<n;j++)
arcs[i][j]=0;
System.out.print("请输入图的边的信息:");
for(k=0;k<e;k++) {
System.out.print("请输入第"+(k+1)+"条边的两个端点:");
str=in.next();
v1=(T)(Object)str.charAt(0);
v2=(T)(Object)str.charAt(1);
i=LocateVex(v1);j=LocateVex(v2);
if(i>=0&&j>=0) {
arcs[i][j]=1;
arcs[j][i]=1;
}
}
}
//如果建立有向图的邻接矩阵,把该算法最后一个if语句改为if(i>=0&&j>=0) arcs[i][j]=1;
//主函数
public static void main(String [] args){
Graph<Character> G =new Graph<Character>();
G.CreateAdj();
G.DisplayAdjMatrix();
}
2.邻接表
邻接表是图的一种顺序存储与链式存储结合的存储方式。
无向图的邻接表具有如下性质:
(1)第i 个链表中结点的数目为第i个顶点的度。
(2)所有链表中结点的数目的一半为图中边的数目。
(3)占用的存储单元数目为n+2e。(n为顶点数,e为边数)
有向图的邻接表具有如下性质:
1.顶点 vi 的出度为第 i 个单链表中的结点个数。
2.顶点 vi 的入度为整个单链表中邻接点域值是 i 的结点个数。
3.顶点 vi 的入度为第 i 个单链表中的结点个数。
4.顶点 vi 的出度为整个单链表中邻接点域值是 i 的结点个数。
中间的图是出边图,右边的是入边图。
三,图的遍历
从图的任意指定顶点出发,依照某种规则去访问图中所有顶点,且每个顶点仅被访问一次,这一过程叫做图的遍历。
1.深度优先遍历(DFS------Depth_First Search)
方法:
1、首先访问出发点V;
2、然后依次从V 出发搜索V的每个邻接点W,若W未曾访问过,则以W为新的出发点继续进行深度优先搜索遍历,直至图中所有和源点V有路径相通的顶点(也称为从源点可达的顶点)均已被访问为止;
3、若此时图中仍有未访问的顶点,则另选一个尚未访问的顶点作为新的源点重复上述过程,直至图中所有顶点均已被访问为止。
从顶点V1出发深度优先遍历的序列为:
V1、V2、V4、V8、V5、V3、V6、V7
还可以是:
V1、V2、V5、V8、V4、V3、V7、V6
V1、V3、V7、V6、V2、V5、V8、V4
import java.util.Scanner;
public class Graph1<T> {
protected boolean [] visited;
protected final int MAXSIZE=10;
protected int e;//边数
protected int n;
protected T[] V;//顶点信息
protected int[][] arcs;//邻接矩阵
public void CreateAdj( ){
int i,j,k;
T v1;
T v2;
Scanner in = new Scanner (System.in) ;
System.out.println("请输入图的顶点数及边数");
System.out.print("顶点数 n="); n=in.nextInt();
System.out.print("边数 e="); e=in.nextInt();
System.out.print("请输入图的顶点信息:");
String str = in.next();
for(i=0;i<n;i++) V[i]=(T)(Object)str.charAt(i);
for(i=0;i<n;i++)
for(j=0;j<n;j++)
arcs[i][j]=0;
System.out.print("请输入图的边的信息:");
for(k=0;k<e;k++) {
System.out.print("请输入第"+(k+1)+"条边的两个端点:");
str=in.next();
v1=(T)(Object)str.charAt(0);
v2=(T)(Object)str.charAt(1);
i=LocateVex(v1);j=LocateVex(v2);
if(i>=0&&j>=0) {
arcs[i][j]=1;
arcs[j][i]=1;
}
}
//创建无向图的邻接矩阵 算法6-3
}
public int LocateVex(T v2){
int i;
for(i=0;i<n;i++)if( (V[i]==v2))return i;
return -1; }
public Graph1() {
V = (T[]) new Object [MAXSIZE];
arcs = new int [MAXSIZE] [MAXSIZE];
visited = new boolean [MAXSIZE];
}
protected void DFS(int i) {//算法6-7 利用邻接矩阵实现连通图的遍历
int j;
System.out.print(V[i]+" ");
visited[i]=true;
for(j=0;j<n;j++) {
if((arcs[i][j]==1)&&(visited[j]==false))
DFS(j);
}
}
public void DFSTraverse() {//对图G进行深度优先遍历
int v;
for(v=0;v<n;v++)visited[v]=false;
for(v=0;v<n;v++)
if(!visited[v])DFS(v);
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Graph1 <Character> G =new Graph1 <Character>();
G.CreateAdj();
System.out.println("图的深度优先遍历序列");
G.DFSTraverse();
}
}
//完整代码如上,可以看看,重点是算法思想理解。
2.广度优先遍历(BFS------Breadth_First Search)
方法:
(1)首先访问出发点V。
(2)接着依次访问顶点V的所有邻接点V1, V2,…,Vt;
(3)然后再依次访问顶点V1, V2,……,Vt的所有邻接点。
(4)如此类推,直至图中所有的顶点都被访问到。
从顶点V出发广度优先遍历的序列为:
V,W1,W 2,W 8,W7,W 3,W 5,W6,W 4
V,W2,W 8,W 1,W3,W 5,W 7,W4,W 6
V,W1,W 8,W 2,W7,W 3,W 5,W6,W 4
class Graph{
private :...
void BFS(int k);//从第k个顶点出发广度优先遍历
public :...
void BFSTraverse();
}
public class Graph<T>{
//....
//算法6-10 从第i个顶点出发递归地广度优先遍历图
protected void BFS(int i){
}
//算法6-11 对图G进行广度优先遍历
}
//算法6-10
protected void BFS(int k){
int i,j;
Queue <Integer> Q=new LinkedList<Integer>();
System.out.print(V[k]+" ");//访问第k个顶点
visited[k]=true;
Q.offer(k);
while(!Q.isEmpty()){
i=Q.poll();//出队
for(j=0;j<n;j++){
//访问第i个顶点的未曾访问的顶点
if((arcs[i][j]==1)&&(visited[j]==false)){
System.out.print(V[j]+" ");
visited[j]=true;
Q.offer(j);//第j个顶点进队
}
}
}
}
//算法6-11
public void BFSTraverse(){
int v;
for(v=0;v<n;v++) visited[v]=false;//初始化数组标志
for(v=0;v<n;v++) //保证非连通图的遍历
if(!visited[v]) BFS(v);//递归的进行广度优先遍历
}
BFS是浪费空间节省时间,DFS是浪费时间节省空间。
4.生成树与最小生成树
生成树:在一个有n 个顶点的连通图G 中,存在一个极小的连通子图G′,G′包含图G 的所有顶点,但只有n-1 条边,并且G′是连通的,则称G′为图G 的生成树。
如图所示,左图为G,中间为G的深度优先生成树,右边为G的广度优先生成树。
最小生成树:某个图所有生成树中必有一棵边的权值总和最小的生成树,我们称这棵生成树为最小生成树,简称为最小生成树,记为MST。
把生成树T中各边的权值总和称为该树的权,记为:
1.普里姆算法(prim)
算法思想:
1.设 N=(V, {E}) 是连通网 T E 是N 上最小生成树中边的集合。
2.初始令 U={u0}, (u0属于V ), TE={ }。
3.在所有 u属于U, v属于V-U 的边 (u, v)属于E 中,找一条代价最小的边 (u0, v0)。
4.将 (u0, v0) 并入集合 TE,同时 v0 并入 U。
5. 重复上述操作直至 U=V 为止,则 T=(V, TE) 为 N 的最小生成树。
2.克鲁斯卡尔算法
算法思想:
1.设连通网 N = (V, {E} ),令最小生成树初始状态为只有 n 个顶点而无边的非连通图 T=(V, { }),每个顶点自成一个连通分量。
2.在 E 中选取代价最小的边,若该边依附的顶点落在 T 中不同的连通分量上(即不能形成环),则将此边加入到 T 中;否则,舍去此边,选取下一条代价最小的边。
3.依此类推,直至 T 中所有顶点都在同一连通分量上为止。
普里姆算法 | 克鲁斯卡尔算法 |
---|---|
O(n^2) | O(eloge) |
适合稠密图 | 适合稀疏图 |
5.图的应用
1.迪杰斯特拉算法
算法思想 :
按路径长度递增的顺序逐步产生最短路径。
狄克斯特拉算法的思想是:设置两个顶点的集合S和T,集合S中存放已找到最短路径的顶点,集合T中存放当前还未找到最短路径的顶点。初始状态时,集合S中只包含源点,设为v0,然后从集合T中选择到源点v0路径长度最短的顶点u加入到集合S中,集合S中每加入一个新的顶点u都要修改源点v0到集合T中剩余顶点的当前最短路径长度值,集合T中各顶点的新的当前最短路径长度值,为原来的当前最短路径长度值与从源点过顶点u到达该顶点的路径长度中的较小者。此过程不断重复,直到集合T中的顶点全部加入到集合S中为止。
2.拓扑排序
有向图中,若以图中的顶点表示活动,以弧表示活动之间的优先关系,这样的有向图称为AOV网(Active On Vertex Network)。
1、若<vi,vj>是AOV网中的弧,则称vi是vj的直接前驱,vj是vi的直接后继。
2、在AOV网中,不应该出现有向环路。
对AOV网进行拓扑排序的方法的步骤如下:
(1)从有向图中选一个没有前驱的顶点,并输出之;
(2)从有向图中删去此顶点以及所有以它为尾的弧;
重复上述两步,直至图空,或者图不空但找不到无前驱的顶点为止。
3.关键路径
把工程计划表示为有向图,用顶点表示事件,弧表示 活动,弧的权表示活动持续时间。每个事件表示在它之前的活动已经完成,在它之后的活动可以开始。称这种有向图为边表示活动的图,简称为 AOE (Activity On Edge) 网。
(持续更新中…)