整理张铭老师《数据结构与算法》笔记
7. 图
7.1 图的概念和抽象数据类型
7.1.1 图的定义和术语
1.G=(V,E) 表示
- V是顶点集合
- E是边集合
2.完全图
3.稀疏图 - 稀疏度(稀疏因子)
- 边条数小于完全图的5%
4.密集图
5.无向图 - 边涉及顶点的偶对无序
- 实际上是双通
6.有向图 - 边涉及顶点的偶对是有序的
7.标号图
8.带权图
9.顶点的度 - 与顶点相关联边的数目,入度 出度
10.子图
11.回路
7.1.2 图的抽象数据类型
7.2 图的存储结构
相邻矩阵
- 图的相邻矩阵,表示顶点之间的邻接关系,对于n个顶点的图,相邻矩阵的空间代价都为O(n2),与边数无关。
- 稀疏因子
在m*n的矩阵中,有t个非零元素,则稀疏因子为t/m+n
若稀疏因子小于0.05,为稀疏因子
邻接表表示
- 无向图同一条边在邻接表中出现两次
- 带权的邻接表表示
- 有向图的邻接表(出边表)
- 有向图的逆邻接表(入边表)
图的邻接表空间代价
- n个顶点e条边的无向图 ,需用(n+2e)个存储单元
- n个顶点e条边的有向图,需用(n+e)个存储单元
7.3 图的遍历
给出一个图G和其中任意一个顶点V0,从V0出发系统访问G中所有顶点,每个顶点访问而且只访问一次
- 深度优先遍历
- 广度优先遍历
- 拓扑排序
图遍历问题:1)非连通图? 2)回路?
解决办法:标志位
//图的遍历算法框架
void graph_traverse(Graph&G) {
// 对图所有顶点的标志位进行初始化
for (int i=0;i<G.VerticesNum();i++)
G.Mark[i] = UNVISITED;
//检查图的所有顶点是否被标记过,如果未被标记,则从该未被标记顶点继续遍历
//do_traverse函数用深度优先或广度优先
for (int i=0;i<G.VerticesNum();i++)
if(G.Mark[i] = UNVISITED)
do_traverse(G,i)
7.3.1 深度优先遍历
- 选取一个未访问的点v0作为源点,访问顶点v0,递归地深搜遍历v0邻接到的其他顶点,重复上述过程直至从v0有路径可达的顶点都被访问过
- 再选取其他未访问顶点作为源点做深搜,直到所有顶点都被访问。
void DFS(Graph& G,int v) { //深度优先搜索的递归实现
G.Mark[v]=VISITED; //把标记位设置为VISITED
Visit(G,v);
for(Edge e= G.FirstEdge(v);G.IsEdge(e);e=G.NextEdge(e))
if(G.Mark[G.ToVertex(e)]==UNVISITED) //返回边的终点是否未被访问
DFS(G,G.ToVertex(e));
PostVisit(G,v) //对顶点v的后访问
}
7.3.2 广度优先遍历
- 从图中的某个顶点v0出发,访问并标记了顶点v0之后,一层层横向搜索v0的所有邻接点,对这些所有邻接点一层层横向搜索,直至所有由v0有路径可达的顶点都已被访问过
- 再选取其他未访问顶点作为源点做广搜,直到所有点都被访问过
void BFS(Graph& G,int v) {
using std::queue;queue<int>Q; //使用STL中的队列
Visit(G,v); //访问顶点v
G.Mark[v]=VISITED; //把标记位设置为VISITED
Q.pop(); //队列顶部元素出队
while(!Q.empty()) { //如果队列非空
int u = Q.front(); //获得队列顶部元素
Q.pop();
for(Edge e= G.FirstEdge(v);G.IsEdge(e);e=G.NextEdge(e)) //所有未访问的邻接点入队
if(G.Mark[G.ToVertex(e)]==UNVISITED) //返回边的终点是否未被访问
Visit(G,G.ToVertex(e));
G.Mark[G.ToVertex(e)]=VISITED;
Q.push(G.ToVertex(e));
}
}}
图搜索的时间复杂度
DFS和BFS每个顶点访问一次,对每一条边处理一次(无向图的每条边从两个方向处理)
- 采用邻接表,有向图代价O(n+e),无向图代价O(n+2e)
- 采用相邻矩阵表示,总代价O(n2)
7.3.3 拓扑排序
对于有向无环图G=(V,E),V里顶点的线性序列称作一个拓扑序列,该顶点序列满足:
若在有向无环图G中从顶点vi到vj有一条路径,则在序列中顶点vi必在顶点vj之前
拓扑排序 将一个有向无环图中所有顶点不违反先决条件关系的前提下排成线性序列的过程称为拓扑排序
//用队列实现的图拓扑排序
void TopsortbyQueue(Graph&G) {
for (int i=0;i<G.VerticesNum();;i++) G.Mark[i]=UNVISITED;//初始化
using std::queue;queue<int>Q;
for(i=0;i<G.VerticesdNum();i++) //入度为0的顶点入队列
if (G.Indegree[i]==0) Q.push(i);
while (!Q.empty()) { //队列非空
int v=Q.front();Q.pop(); //获得队列队顶元素,出队
Visit(G,v); G.Mark[v]=VISITED; //将标记位置设置为VISITED
for(Edge e = G.FirstEdge(v);G.IsEdge(e);e=G.NextEdge(e)) {
G.Indegree[G.ToVertex(e)]--; //相邻的顶点入度减1
if(G.Indegree[G.ToVertex(e)]==0) //顶点入读减为0则入队
Q.push(G.ToVertex(e));
}}
for(i =0;i<G.VerticesNum(); i++) //判断图中是否有环
if(G.Mark[i]==UNVISITED) {
cout<<"此图有环" ; break;
}}
7.4 最短路径
7.4.1 单源最短路径 —Dijkstra算法
给定带权图G=<V,E>,其中每条边(vi,vj)上权W[vi,vj]是一个非负实数。计算从任给的一个源点s到所有其他各结点的最短路径
class Dist { //Dist 类,用于保存最短路径信息
public:
int index; //结点的索引值
int length; //当前最短路径长度
int pre; //路径最后经过的结点
};
void Dijkstra(Graph&G,int s,Dist*&D) //s是源点
D=new Dist[G.VerticesNum()]; //记录最短路径
for (int i=0;i<G.VerticesNum();i++) { // 初始化
G.Mark[i]=UNVISITED;
D[i].index=i;D[i].length=INFINITE ; D[i].pre = s;
}
D[s].length = 0;
MinHeap<Dist>H(G.EdgesNum());
H.Insert(D[s]);
for (i=0;i<G.VerticesNum();i++) {
bool FOUND=false;
Dist d;
while(!H.isEmpty()) {
d=H.RemoveMin(); //获得到s路径长度最小的结点
if(G.Mark[d.index]==UNVISITED) { //如果未访问则跳出循环
FOUND=true;break;
}}
if(!FOUND) break;//若没有符合条件的最短路径则跳出循环
int v =d.index;
G.Mark[v]=VISITED; //将标记位设置为VISITED
for (Edge e=G.FirstEdge(v);G.IsEdge(e);e=G.NextEdge(e))//刷新最短路
if (D[G.ToVertex(e)].length>(D(e).length+G.Weight(e))) {
D[G.ToVertex(e)].length=(D(e).length+G.Weight(e))
D[G.ToVertex(e)].pre=v;
H.Insert(D[G.ToVertex(e)]);
}}
}
Dijkstra 算法时间代价
每次改变D[i].length,不删除,添加一个新值(更小的),作为堆中新元素。旧值被找到时,该结点一定被标记为VISITED,从而被忽略;在最差情况下,它将使堆中元素数目由O(V)增加到O(|E|),总的时间代价为O((|V|+|E|)log|E|)。
Dijkstra算法支持回路,但不支持负权值回路;
7.4.2 每对结点间的最短路径
Floyd算法求每对结点之间的最短路径
基本思想
用相邻矩阵adj来表示带权有向图
初始化adj为相邻矩阵
在矩阵adj(0)上做n次迭代,递归地产生一个矩阵adj(!),adj(2),…adj(n)
经过第k次迭代,adj(k)[i,j]的值等于从结点vi到结点vj路径上所经过的结点序号不大于k的最短路径长度
最短路径组合情况分析
void Floyed(Graph& G,Dist**&D) {
int i,j,v;
D=new Dist*[G.VerticesNum()]; //申请空间
for(i=0;i<G.VerticesNum();i++)
D[i]=new Dist[G.VerticesNum()];
for(i=0;i<G.VerticesNum();i++) //初始化数组D
for(j=0;j<G.VerticesNum();j++) {
if(i==j) {
D[i][j].length = 0;
D[i][j].pre = i; }
else {
D[i][j].length = INFINITE;
D[i][j].pre = -1; }
}
for (v=0;v<G.VerticesNum();v++)
for(Edge e = G.FirstEdge(v);G.IsEdge(e);e = G.NextEdge(e)) {
D[v][G.ToVertex(e)].length=G.Weight(e);
D[v][G.ToVertex(e)].pre=v;
} //加入新结点后,更新那些那些变短路径长度
for(v=0;v<G.VerticesNum();v++)
for(i=0;i<G.VerticesNum();i++)
for(j=0;j<G.VerticesNum();j++)
if(D[i][j].length > (D[i][v].length + D[v][j].length)) {
D[i][j].length = D[i][v].length + D[v][j].length
D[i][j].pre=D[v][j].pre;
}
}
Floyd算法的时间复杂度
三重for循环 O(n3)
7.5 最小生成树
概念
图G的生成树是一棵包含G的所有顶点的树,树上所有权值总和表示代价,那么在G的所有生成树中代价最小的生成树称为图G的最小生成树(MST)
7.5.1 Prim算法
与Dijkstra算法类似—也是贪心法
从图中任意一个顶点开始(例如v0),首先把这个顶点包括在MST,U=(V*,E*)里,初始V*={v0},E*={},然后在那些其一个端点已在MST里,另一个端点还不是MST里的边,找权最小的一条边(vp,vq),并把vq包括进MST里,如此进行下去,每次往MST里加一个顶点和一条权最小的边,直到把所有的顶点都包括进MST里,算法结束时V*=V,E*包括了G中的n-1条边
void Prim(Graph&G,int s,Edge*&MST) //s是源点,MST存边
int MSTtag=0;
MST=new Edge[G.VerticesNum()-1]; //为数组MST申请空间
Dist *D;
D=new Dist[G.VerticesNum()]; //为数组D申请空间
for (int i=0;i<G.VerticesNum();i++) { // 初始化
G.Mark[i]=UNVISITED;
D[i].index=i;D[i].length=INFINITE ; D[i].pre = s;
}
D[s].length = 0;
G.Mark[s]=VISITED;
int v=s;
for (i=0;i<G.VerticesNum()-1;i++) {
for (Edge e=G.FirstEdge(v);G.IsEdge(e);e=G.NextEdge(e))//刷新最短路
if (G.Mark[G.ToVertex((e)]!=VISITED && (D[G.ToVertex(e)].length > e.weight)) {
D[G.ToVertex(e)].length=G.Weight(e);
D[G.ToVertex(e)].pre=v;}
v = minVertex(G,D); //在D数组中找最小值记为v
if(v==-1) return; //非连通,有不可达顶点
G.Mark[v]=VISITED; //标记访问过
Edge edge(D[v].pre,D[v].index,D[v].length); //保存边
AddEdgetoMST(edge,MST,MSTtag++); //将边加入MST
}
}
//在Dist数组中找最小值
int minVertex(Graph &G, Dist *&D) {
int i,v=-1;
int MinDist = INFINITY;
for (i=0;i<G.VerticesNum();i++)
if ((G.Mark[i] == UNVISITED) && (D[i]<MinDist)) {
v=i; //保存当前发现的最小距离顶点
MinDist = D[i];
}
return v;
}
Prim算法时间复杂度
Prim算法框架与Dijkstra算法类似,Prim算法中的距离值不需要累积,直接用最小边
本算法通过直接比较D数组元素,确定代价最小的边需要总时间O(n2);取出权值最小的顶点后,修改D数组共需要时间O(e),因此共需要花费O(n2)时间,算法适合于稠密图,对于稀疏图,可以像Dijkstra算法那样用堆保存距离值。
7.5.2 Kruskal算法
首先将G中n个顶点看成是独立的n个连通分量,这时的状态是有n个顶点而无边的森林,可以记为T=<V,{}>,然后在E中选择代价最小的边,如果该边依附于两个不同的连通分支,那么将该条边加入到T中,否则舍去这条边而选择下一条代价最小的边。以此类推,直到T中所有顶点都在同一个连通分量中为止,此时就得到图G的一棵最小生成树。
void Kruskal(Graph &G,Edge*&MST) //MST存最小生成树的边
ParTree <int> A(G.VerticesNum()); //等价类
MST=new Edge[G.VerticesNum()-1]; //为数组MST申请空间
int MSTtag = 0;
for (v=0;v<G.VerticesNum();v++) //将所有边插入最小堆H中
for(Edge e = G.FirstEdge(v);G.IsEdge(e);e = G.NextEdge(e)) {
if(G.FromVertex(e)<G.ToVertex(e)) //防重复边
H.insert(e);
int EquNum = G.VerticesNum(); //开始有n个独立顶点等价类
while(EquNum > 1) { //当等价类的个数大于1时合并等价类
if(H.isEmpty()) {
cout<<"不存在最小生成树." <<endl;
delete []MST;
MST = NULL;
return;
}
Edge e=H.RemoveMin(); //取权最小的边
int from =G.FromVertex(e); //记录该条边的信息
int to =G.ToVertex(e);
if (A.Different(from,to)) { //边e的两个顶点不在一个等价类
A.Union(from,to); //合并边的两个顶点所在的等价类
AddEdgetoMST(e,MST,MSTtag++); //将e边加到MST
EquNum--; //等价类个数减1
}
}
}
Kruskal算法时间代价接近为O(Nloge)