图:
表示“多对多”的关系
包含:
一组顶点:通常用V (Vertex) 表示顶点集合
一组边:通常用E (Edge) 表示边的集合
边是定点对(v,w)
有向边<v,w> 表示从v指向w的边
不考虑重边和自回路
抽象数据类型:
类型名称:图(Graph)
数据对象集:G(V,E)由一个非空的有限顶点集合V和一个有限边集合E组成。
Graph Create():建立并返回空图;
Graph InsertVertex(GraphG, Vertex v):将v插入G;
Graph InsertEdge(GraphG, Edge e):将e插入G;
void DFS(GraphG, Vertex v):从顶点v出发深度优先遍历图G;
void BFS(GraphG, Vertex v):从顶点v出发宽度优先遍历图G;
void ShortestPath(GraphG, Vertex v, intDist[]):计算图G中顶点v到任意其他顶点的最短距离;
void MST(GraphG):计算图G的最小生成树;
怎么在程序中表示一个图:
1.邻接矩阵:G[N][N]——N个顶点从0到N-1编号:
说明:矩阵中每个点取值1或0:1表示有边,0表示没有边相连
特点:
[1]直观、简单、好理解
[2]方便检查任意一对顶点间是否存在边
[3]方便找任一顶点的所有“邻接点”(有边直接相连的顶点)
[4]方便计算任一顶点的“度”(从该点发出的边数为“出 度”,指向该点的边数为“入度”)
无向图:对应行(或列)非0元素的个数
有向图:对应行非0元素的个数是“出度”;对应列非0元素的 个数是“入度”
[5]浪费空间——存稀疏图(点很多而边很少)有大量无效元素
对稠密图(特别是完全图)还是很合算的
[6]浪费时间——统计稀疏图中一共有多少条边
2.邻接表:G[N]为指针数组,对应矩阵每行一个链表, 只存非0元素
说明:一个数组,数组每个元素为一个链表。数组大小为途中点的个数,每个链表表示一个点与其他点的连接
特点:
[1]方便找任一顶点的所有“邻接点”
[2]节约稀疏图的空间
需要N个头指针+ 2E个结点(每个结点至少2个域)
[3]方便计算任一顶点的“度”?
对无向图:是的
[4]对有向图:只能计算“出度”;需要构造“逆邻接表”(存指向自己 的边)来方便计算“入度”
[5]方便检查任意一对顶点间是否存在边?NO
图的遍历:
1.深度优先搜索(DFS) //类似于树的先序遍历
void DFS (Vertex v){
visit[v] = true;
for (v的每个邻接点w){
if (!visit[w])
DFS(W);
}
}
若有N个顶点、E条边,时间复杂度是:
用邻接表存储图,O(N+E)
用邻接矩阵存储图,O(N^2)
2.广度优先搜索(BFS) //类似于层序遍历
void BFS ( Vertex V ) {
visited[V] = true;
Enqueue(V, Q); //队列
while(!IsEmpty(Q)){
V = Dequeue(Q);
for( V 的每个邻接点W )
if( !visited[W] ) {
visited[W] = true;
Enqueue(W, Q);
}
}
}
时间复杂度同1
图的联通性问题:
1.联通:如果从V到W存在一条(无向)路径,则称 V和W是连通的
2.路径::V到W的路径是一系列顶点{V, v1, v2, …, vn, W}的集合,其中任一对相邻的顶点间都有图 中的边。路径的长度是路径中的边数
(如果带 权,则是所有边的权重和)。如果V到W之间的所 有顶点都不同,则称简单路径
3.回路:起点等于终点的路径
4.连通图:图中任意两顶点均连通
对于不连通的图:
1.联通分量:无向图的极大联通子图,性质:
极大顶点数:再加1个顶点就不连通了
极大边数:包含子图中所有顶点相连的所有边
2.强连通:有向图中顶点V和W之间存在双向路 径,则称V和W是强连通的
3.强连通图:有向图中任意两顶点均强连通
4.强连通分量:有向图的极大强连通子图
不连通图的遍历:
void ListComponents( Graph G ) {
for( each V in G )
if( !visited[V] ) {
DFS( V ); /*or BFS( V )*/ //每调用一次DFS(V),就 把V所在的连通分量遍历 了一遍。BFS也是一样。
}
}
最短路径问题:
在网络中,求两个不同顶点之间的所有路径 中,边的权值之和最小的那一条路径
单源最短路径问题:从某固定源点出发,求其 到所有其他顶点的最短路径,可分为有权图的情况和无权图情况
1.无权图单源最短路径算法:
实际上就是通过广度优先搜索(BFS)遍历实现
dist[W] 保存每个点到源点s的最小距离
path[w]保存w的上一个点,用于保存最短路径
void Unweighted( Vertex S ) {
Enqueue(S, Q);
while(!IsEmpty(Q)){
V = Dequeue(Q);
for( V 的每个邻接点W )
if( dist[W]==-1 ) {
dist[W] = dist[V]+1;
path[W] = V;
Enqueue(W, Q);
}
}
}
简单代码实现:
public static void main(String[] args) throws InterruptedException {
System.out.println("begin");
Node nodea = new Node("a"); Node nodeb = new Node("b");
Node nodec = new Node("c"); Node noded = new Node("d");
Node nodee = new Node("e"); Node nodef = new Node("f");
Node nodeg = new Node("g");
Map<Node, List<Node>> map = new HashMap<>();
List<Node> nodes1 = new ArrayList<>();
nodes1.add(nodeb);
nodes1.add(nodee);
map.put(nodea, nodes1);
List<Node> nodes2 = new ArrayList<>();
nodes2.add(nodea);
nodes2.add(nodef);
nodes2.add(nodec);
map.put(nodeb, nodes2);
List<Node> nodes3 = new ArrayList<>();
nodes3.add(nodeb);
nodes3.add(nodef);
nodes3.add(nodeg);
nodes3.add(noded);
map.put(nodec, nodes3);
List<Node> nodes4 = new ArrayList<>();
nodes4.add(nodec);
nodes4.add(nodeg);
map.put(noded, nodes4);
List<Node> nodes5 = new ArrayList<>();
nodes5.add(nodea);
nodes5.add(nodef);
nodes5.add(nodeg);
map.put(nodee, nodes5);
List<Node> nodes6 = new ArrayList<>();
nodes6.add(nodeb);
nodes6.add(nodec);
nodes6.add(nodee);
map.put(nodef, nodes6);
List<Node> nodes7 = new ArrayList<>();
nodes7.add(nodee);
nodes7.add(nodec);
nodes7.add(noded);
map.put(nodeg, nodes7);
Map<Node, Integer> dist = new HashMap<>();
Map<Node, Node> path = new HashMap<>();
dist.put(nodea, 0);
dist.put(nodeb, -1);
dist.put(nodec, -1);
dist.put(noded, -1);
dist.put(nodee, -1);
dist.put(nodef, -1);
dist.put(nodeg, -1);
Queue<Node> queue = new LinkedList<>();
queue.add(nodea);
nodea.isVisit = true;
while (!queue.isEmpty()) {
Node poll = queue.poll();
List<Node> list = map.get(poll);
if(list != null && list.size()>0){
for (Node node : list) {
if(!node.isVisit){
dist.put(node, dist.get(poll) +1);
path.put(node, poll);
node.isVisit = true;
queue.add(node);
}
}
}
}
System.out.println(dist);
//显示a到d的最短路径
Stack<Node> nodeSta= new Stack<>();
nodeSta.add(noded);
Node tempNode = noded;
while(!tempNode.equals(nodea)){
Node node = path.get(tempNode);
nodeSta.add(node);
tempNode = node;
}
System.out.println("a到d最短路径:");
while (!nodeSta.isEmpty()) {
System.out.println(nodeSta.pop());
}
System.out.println("end");
}
public static class Node {
String name;
Boolean isVisit = false;
public Node(String name) {
super();
this.name = name;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Node other = (Node) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
@Override
public String toString() {
return "Node [name=" + name + "]";
}
}
2.有权图单源最短路径算法:
Dijkstra算法:
a.令S={源点s + 已经确定了最短路径的顶点vi}
b.对任一未收录的顶点v,定义dist[v]为s到v的最短路径长度,但该路径仅经过S中的顶点。即路径 {s(viS)v}的最小长度
c.若路径是按照递增(非递减)的顺序生成的,则
真正的最短路必须只经过S中的顶点(为什么?)
每次从未收录的顶点中选一个dist最小的收录(贪心)
增加一个v进入S,可能影响另外一个w的dist值!
dist[w] = min{dist[w], dist[v] + <v,w>的权重}
dist[W] 保存每个点到源点s的最小距离 //dist[W]的初始化:dist[s]设置为零,与s直接相连的节点设置为对应边的权值,其他初始化为无穷大
path[w]保存w的上一个点,用于保存最短路径
思路:每次从dist[W]中得到距离值最小的节点,将这个节点设置为已收录,遍历这个节点的邻接且未被收录节点,更新dist值
,循环这个过程,直到所有点被收录
实现:
void Dijkstra( Vertex s ) {
while(1) {
V = 未收录顶点中dist最小者;
if( 这样的V不存在)
break;
collected[V] = true;
for ( V 的每个邻接点W )
if( collected[W] == false )
if( dist[V]+E<V,W>< dist[W] ) {
dist[W] = dist[V] + E<V,W>;
path[W] = V;
}
}
}/* 不能解决有负边的情况*/
public static void main(String[] args) throws InterruptedException {
System.out.println("begin");
Node nodea = new Node("a"); Node nodeb = new Node("b");
Node nodec = new Node("c"); Node noded = new Node("d");
Node nodee = new Node("e"); Node nodef = new Node("f");
Node nodeg = new Node("g");
//类似邻接表,保存连接关系
Map<Node, List<Node>> map = new HashMap<>();
List<Node> nodes1 = new ArrayList<>();
nodes1.add(nodeb);
nodes1.add(nodee);
map.put(nodea, nodes1);
List<Node> nodes2 = new ArrayList<>();
nodes2.add(nodea);
nodes2.add(nodef);
nodes2.add(nodec);
map.put(nodeb, nodes2);
List<Node> nodes3 = new ArrayList<>();
nodes3.add(nodeb);
nodes3.add(nodef);
nodes3.add(nodeg);
nodes3.add(noded);
map.put(nodec, nodes3);
List<Node> nodes4 = new ArrayList<>();
nodes4.add(nodec);
nodes4.add(nodeg);
map.put(noded, nodes4);
List<Node> nodes5 = new ArrayList<>();
nodes5.add(nodea);
nodes5.add(nodef);
nodes5.add(nodeg);
map.put(nodee, nodes5);
List<Node> nodes6 = new ArrayList<>();
nodes6.add(nodeb);
nodes6.add(nodec);
nodes6.add(nodee);
map.put(nodef, nodes6);
List<Node> nodes7 = new ArrayList<>();
nodes7.add(nodee);
nodes7.add(nodec);
nodes7.add(noded);
map.put(nodeg, nodes7);
//记录权值,即每条边的长度
Map<String, Integer> distMap = new HashMap<>();
distMap.put("a-b", 3);
distMap.put("a-e", 2);
distMap.put("b-f", 2);
distMap.put("b-c", 3);
distMap.put("c-f", 4);
distMap.put("c-d", 2);
distMap.put("e-f", 1);
distMap.put("e-g", 1);
distMap.put("c-g", 2);
distMap.put("d-g", 6);
//记录最短路径
Map<Node, Node> path = new HashMap<>();
//保存收录的点
Set<Node> s = new HashSet<>();
//最小堆保存节点,//记录最短路径
PriorityQueue<Node> queue = new PriorityQueue<>(1000, new Comparator<Node>() {
public int compare(Node o1, Node o2) {
return o1.distance - o2.distance;
};
});
Node atoa = new Node("a", 0);
Node btoa = new Node("b", Integer.MAX_VALUE);
Node ctoa = new Node("c", Integer.MAX_VALUE);
Node dtoa = new Node("d", Integer.MAX_VALUE);
Node etoa = new Node("e", Integer.MAX_VALUE);
Node ftoa = new Node("f", Integer.MAX_VALUE);
Node gtoa = new Node("g", Integer.MAX_VALUE);
queue.add(atoa);
queue.add(btoa);
queue.add(ctoa);
queue.add(dtoa);
queue.add(etoa);
queue.add(ftoa);
queue.add(gtoa);
//核心算法
while(!queue.isEmpty()) {
Node poll = queue.poll(); //弹出dis最小的点
s.add(poll); //收录这个点
List<Node> list = map.get(poll); //获取邻接点
for (Node node : list) {
if(!s.contains(node)){ //如果临界点没有被收录,计算并更新新距离
Integer dis = distMap.get(poll.name + "-" + node.name) != null ? distMap.get(poll.name + "-" + node.name) :distMap.get(node.name + "-" + poll.name);
int tempDis = poll.distance + dis;
if(dis != null && tempDis < getDis(node, queue).distance) {
node.distance = tempDis;
queue.remove(node);
queue.offer(node); //这两个实现最小堆的节点重排保证dis最小的点在根节点
path.put(node, poll); //path用于记录最小路径
}
}
}
}
System.out.println(s);
//显示a到d的最短路径
Stack<Node> nodeSta= new Stack<>();
nodeSta.add(nodec);
Node tempNode = nodec;
while(!tempNode.equals(nodea)){
Node node = path.get(tempNode);
nodeSta.add(node);
tempNode = node;
}
System.out.println("a到c最短路径:");
while (!nodeSta.isEmpty()) {
System.out.print(nodeSta.pop().name + "->");
if(nodeSta.size() == 1){
System.out.println(nodeSta.pop().name);
}
}
System.out.println("end");
}
多源最短路径问题:求任意两顶点间的最短路径
方法1:直接将单源最短路算法调用|V|遍
T= O( |V|^3 + |E||V|)
方法2: Floyd算法
T= O( |V|^3 )
Floyd算法
使用邻接矩阵保存图,矩阵中D[i,j]保存的是i到j的距离,如果i,j间没有边则初始化为无穷大
依次将每个点K取出,比较之前记录的其他任意两个点如A B间的距离AB如果这个距离大于AK + KB则更新AB的值为AK + KB
void Floyd() {
for ( i = 0; i < N; i++ )
for( j = 0; j < N; j++ ) {
D[i][j] = G[i][j]; //初始化
path[i][j] = -1;
}
for( k = 0; k < N; k++ )
for( i = 0; i < N; i++ )
for( j = 0; j < N; j++ )
if( D[i][k] + D[k][j] < D[i][j] ) {
D[i][j] = D[i][k] + D[k][j]; //更新最短路径值
path[i][j] = k; //用于后续找出任意两点的最短路径
}
}
最小生成树问题: //有等价关系:最小生成树存在↔图连通
是一颗树
无回路
|v|个顶点一定有|v|-1条边
是生成树:
包含全部顶点
V-1条边全部都在图里
边的权重和最小
得到最小生成树的算法:
贪心算法:
什么是“贪”:每一步都要最好的
什么是“好”:权重最小的边
需要约束:
只能用图里有的边
只能正好用掉|V|-1条边
不能有回路
Prim算法:让一棵小树长大 //属于贪心算法
void Prim() {
MST = {s};
while(1) {
V = 未收录顶点中dist最小者;
if( 这样的V不存在) break;
将V收录进MST: dist[V] = 0;
for ( V 的每个邻接点W )
if( W未被收录)
if( E(V,W)< dist[W] ){
dist[W] = E(V,W);
parent[W] = V;
}
}
if( MST中收的顶点不到|V|个)
Error ( “生成树不存在”);
}
首先选择图中一个点为根节点,找到与根节点相连的最短的边,将这条边和这条边上另一个端点收录到树中,接着找到与树中顶点相连的所有边中
最短的边及顶点,在不形成回路的情况下,收录为树的一部分。如此循环知道所有的点都被收录进树中。
Kruskal算法—将森林合并成树
void Kruskal( Graph G ) {
MST = { } ;
while( MST 中不到|V|-1 条边&& E 中还有边) {
从E 中取一条权重最小的边E(v,w) ;
将E(v,w)从E 中删除;
if( E(V,W)不在MST 中构成回路)
将E(V,W)加入MST; else 彻底无视E(V,W);
}
if( MST 中不到|V|-1 条边)
Error ( “生成树不存在”);
}
从图中找到权重最小的边,判断这条边加进mst中时是否构成回路,不构成回路则将其加入到mst中,否则不加入并从图中去掉这条边
接着按同样方式在图中继续找下一条边,知道边数达到v-1,则找到了最小生成树。如果找不到v-1条边,则生成树不存在
如何获取最小的边:使用最小堆
如何判断加入边后是否构成回路:并查集
拓扑排序:
AOV网络:是一个有向图,所有真实的活动表现为图中的顶点,顶点和顶点之间的有向边标识两个事情的先后顺序,这样的图称为AOV网络
拓扑序:如果图中从V到W有一条有向路径, 则V一定排在W之前。满足此条件的顶点序列 称为一个拓扑序
获得一个拓扑序的过程就是拓扑排序
AOV如果有合理的拓扑序,则必定是有向无环 图(Directed Acyclic Graph, DAG)
获得拓扑序的算法:
1.时间复杂度:O(|V|^2)
void TopSort() {
for( cnt= 0; cnt< |V|; cnt++ ) {
V = 未输出的入度为0的顶点;
if( 这样的V不存在) {
Error ( “图中有回路”);
break;
}
输出V,或者记录V的输出序号;
for( V 的每个邻接点W)
Indegree[W]––;
}
}
从图中每次获取入度为0的顶点,输出,遍历这个顶点的临界点,将邻接点的入度减一
2.T= O( |V| + |E| )
voidTopSort() {
for( 图中每个顶点V )
if( Indegree[V]==0 )
Enqueue( V, Q );
while( !IsEmpty(Q) ) {
V = Dequeue( Q );
输出V,或者记录V的输出序号;
cnt++;
for( V 的每个邻接点W )
if( ––Indegree[W]==0 )
Enqueue( W, Q );
}
if( cnt!= |V| )
Error( “图中有回路”);
}
将度为0的顶点放在一个队列中,这样每次都能快速得到度为0的顶点