数据结构 第六章学习小结
数据结构第六章学习小结
- 图
- 6.1 图的定义和基本术语
- 6.1.1 图的定义
- 6.1.2 图的基本术语
- 6.2 案例引入
- 6.3 图的类型定义
View Code
- 6.4 图的存储结构
- 6.4.1 邻接矩阵
- 1.图的邻接矩阵存储表示
#define Maxint 32767 //表示极大值, 即 ∞ #define MVNum 100 //最大顶点数 typedef char VerTexType; //假设顶点的数据类型为字符型 typedef int ArcType; //假设边的权值类型为整型 typedef struct { VerTexType vexs [MVNum] ; //顶点表 ArcType arcs[MVNu] [MVNum]; //邻接矩阵 int vexnum,arcnum; //图的当前点数和边数 ) AMGraph;
- 2.算法:采用邻接矩阵表示法创建无向网 (时间复杂度是O(n^2))
Status CreateUDN(AMGraph &G) {//采用邻接矩阵表示法,创建无向网G cin>>G.vexnum>>G.arcnum; //输人总顶点数,总边数 for(i=0;i<G.vexnum;++i) //依次输入点的信息 cin>>G.vexs[i); for(i=0;i<G.vexnum;++i)//初始化邻接矩阵,边的权值均置为极大值Maxint for (j =0; j <G. vexnum; ++j) G.arcs[i) [j)=Maxint; for(k=0;k<G.arcnum;++k) //构造邻接矩阵 { cin>>vl>>v2>>w; //输人一条边依附的顶点及权值 i=LocateVex(G,vl);j=LocateVex(G,v2); //确定vl和v2在G中的位置,即顶点数组的下标 G.arcs[i) [j)=w; //边<vl, v2>的权值置为w G.arcs[j] [i]=G.arcs[i] [j]; //置<vl, v2>的对称边<v2, vl>的权值为w } return OK; }
- 3.优缺点
(1)优点 1.便于判断两个顶点之间是否有边, 即根据A[z][j] = 0或1来判断。 2.便于计算各个顶点的度。对千无向图,邻接矩阵第凶于元素之和就是顶点l的度;对于有向图,第i行元素之和就是顶点 i 的出度,第i 列元素之和就是顶点l 的入度。 (2) 缺点 1.不便于增加和删除顶点。 2.不便于统计边的数目,需要扫描邻接矩阵所有元素才能统计完毕,时间复杂度为O(n^2) 3.空间复杂度高。如果是有向图,n个顶点需要n2个单元存储边。如果是无向图,因其邻接矩阵是对称的,所以对规模较大的邻接矩阵可以采用压缩存储的方法,仅存储下三角(或上三角)的元素,这样需要n(n-1)/2个单元即可。但无论以何种方式存储,邻接矩阵表示法的空间复杂度均为O(n^2),这对于稀疏图而言尤其浪费空间。
- 1.图的邻接矩阵存储表示
- 6.4.2 邻接表
- 1.图的邻接表存储表示
#define MVNum 100 //最大顶点数 typedef struct ArcNode //边结点 { int adjvex; //该边所指向的顶点的位置 struct ArcNode * nextarc; //指向下一条边的指针 Otherinfo info; ///和边相关的信息 }ArcNode; typedef struct VNode //顶点信息 { VerTexType data; ArcNode *firstarc; ///指向第一条依附该顶点的边的指针 } VNode,AdjList[MVNum]; //AdjList表示邻接表类型 typedef struct //邻接表 { AdjList vertices; int vexnum,arcnum; //图的当前顶点数和边数 }ALGraph;
- 2.算法:采用邻接表表示法创建无向图 (时间复杂度是O(n + e))
Status CreateUDG(ALGraph &G) {//采用邻接表表示法, 创建无向图 G cin>>G.vexnum>>G.arcnum; //输入总顶点数, 总边数 for(i=O;i<G.vexnum;++i) //输入各点,构造表头结点表 { cin»G.vertices[i) .data; //输入顶点值 G.vertices[i) .firstarc=NULL;//初始化表头结点的指针域为NULL } for(k=O;k<G.arcnum;++k) //输入各边, 构造邻接表 { cin>>vl>>v2; //输入 一条边依附的两个顶点 i=LocateVex(G,vl); j =LocateVex(G,v2); //确定vl和 v2在G中位置, 即顶点在G.vertices中的序号 pl=new ArcNode; //生成一个新的边结点*pl pl->adjvex=j; //邻接点序号为j pl->nextarc=G. vertices [ i] . firstarc; G. vertices [ i] . firstarc= pl; //将新结点*pl插入顶点Vi的边表头部 p2=new ArcNode; //生成另一个对称的新的边结点*p2 p2->adjvex=i; //邻接点序号为1 p2->nextarc=G.vertices[j] .firstarc; G.vertices[j] .firstarc=p2; //将新结点*p2插入顶点Vj的边表头部 } return OK; }
- 3.优缺点
(1) 优点 1.便于增加和删除顶点。 2.便于统计边的数目, 按顶点表 顺 序扫描 所 有边表可得到边的数目,时间复杂度为 O(n + e) 3.空间效率高。对于一个具有n个顶点e条边的图 G, 若 G 是无向图,则在其邻接表表示中有 n 个顶点表结点和 2e 个边表结点;若 G 是有向图,则在它的邻接表表示或逆邻接表表示中均有 n 个顶点表结点和e个边表结点。因此,邻接表或逆邻接表表示的空间复杂度为 O(n + e), 适合表示稀疏图。对于稠密图,考虑到邻接表中要附加链域,因此常采取邻接矩阵表示法。 (2) 缺点 1.不便于判断顶点之间是否有边,要判定 V;和v丿之间是否有边,就需扫描第!个边表,最坏情况下要耗费 O(n)时间。 2.不便于计算有向图各个顶点的度。对千无向图,在邻接表表示中顶点V;的度是第i个边表中的结点个数。 在有向图的邻接表中,第 i 个边表上的结点个数是顶点 V;的出度,但求 V的入度较困难,需遍历各顶点的边表。若有向图采用逆邻接表表示,则与邻接表表示相反,求顶点的入度容易,而求顶点的出度较难。
- 1.图的邻接表存储表示
- 6.4.3 十字链表
- 有向图的十字链表存储表示
#define MAX_ VERTEX_NUM 20 typedef strut ArcBox { int tailvext,headvex;//该弧的尾和头顶点的位置 struct ArcBox *hlink, *tlink; //分别为弧头相同和弧尾相同的弧的链域 InfoType *info; I//该弧相关信息的指针 ) ArcBox; typedef struct VexNode { VertexType data; ArcBox *firstin,*firstout;//分别指向该顶点第一条入弧和出弧 }VexNode; typedef struct { VexNode xlist [MAX_VERTEX_NUM]; //表头向量 int vexnnm, arcnum; //有向图的当前顶点数和弧数 }OLGraph;
- 有向图的十字链表存储表示
- 6.4.4 邻接多重表
- 无向图的邻接多重表存储表示
#define MAX_VERTEX_NUM 20 typedef enum{unvisited,visited} Visitlf; typedef struct EBox { Visitlf mark; //访问标记 int ivex, jvex; //该边依附的两个顶点的位置 struct EBox *ilink, *jlink; //分别指向依附这两个顶点的下一条边 InfoType *info; //该边信息指针 }Ebox; typedef struct VexBox { VertexType data; EBox *firstedge; //指向第一条依附该顶点的边 }VexBox; typedef struct { VexBox adjmulist [MAX_VERTEX_NUM]; int vexnum, edgenum;//无向图的当前顶点数和边数 }AMLGraph;
- 无向图的邻接多重表存储表示
- 6.4.1 邻接矩阵
- 6.5 图的遍历
- 6.5.1 深度优先搜索
- 算法:深度优先搜索遍历连通图
bool visited [MVNum) ; //访问标志数组, 其初值为 "false" void DFS(Graph G,int v) {//从第 v 个顶点出发递归地深度优先遍历图G cout<<v;visited[v)=true; //访问第 v 个顶点, 并置访问标志数组相应分量值为 true for(w=FirstAdjVex(G,v);w>=O;w=NextAdjVex(G,v,w)) //依次检查 v 的所有邻接点 w , FirstAdjVex (G, v)表示 v 的第一个邻接点 //NextAdjVex(G,v,w)表示 v 相对千 w 的下一个邻接点, w匀表示存在邻接点 if(!visited[w]) DFS(G,w); //对 v 的尚未访问的邻接顶点 w 递归调用 DFS }
- 算法:深度优先搜索遍历非连通图
void DFSTraverse(Graph G) {//对非连通图G做深度优先遍历 for(v=O;v<G.vexnum;++v) visited[v]=false; //访问标志数组初始化 for(v=O;v<G.vexnum;++v) //循环调用算法6.3 if(!visited[v]) DFS(G,v); //对尚未访问的顶点调用DFS }
- 算法 :采用邻接矩阵表示图的深度优先搜索遍历
void DFS_AM(AMGraph G,int v) {//图G为邻接矩阵类型,从第v个顶点出发深度优先搜索遍历图G cout<<v;visited[v]=true; //访问第v个顶点,并置访问标志数组相应分量值为true for(w=O;w<G.vexnum;w++) //依次检查邻接矩阵 v所在的行 if((G.arcs[v] [w] !=O}&&(!visited[w]}} DFS(G,w}; //G.arcs[v] [w] ! =0表示w是v的邻接点, 如果w未访问, 则递归调用DFS }
- 算法 :采用邻接表表示图的深度优先搜索遍历
-
void DFS_AL (ALGraph G,int v) {//图G为邻接表类型, 从第v个顶点出发深度优先搜索遍历图G cout<<v;visited[v]=true; //访问第v个顶点,并置访问标志数组相应分量值为true p=G.vertices[v] .firstarc; //p指向v的边链表的第一个边结点 while(p!=NULL) //边结点非空 { w=p->adjvex; //表示w是v的邻接点 if(!visited[w]) DFS(G,w); //如果w未访问, 则递归调用DFS p=p->nextarc; //p指向下一个边结点 } }
- 算法分析
- 当用邻接矩阵表示图时,查找每个顶点的邻接点的时间复杂度为 O(n2 ), 其中 n为图中顶点数。而当以邻接表做图的存储结构时,查找邻接点的时间复杂度为O(e), 其中e为图中边数。由此,当以邻接表做存储结构时,深度优先搜索遍历图的时间复杂度为 O(n + e)。
- 算法:深度优先搜索遍历连通图
- 6.5.2 广度优先搜索
- 算法:广度优先搜索遍历连通图
void BFS{Graph G,int v) {//按广度优先非递归遍历连通图G cout<<v;visited[v]=true;//访问第v个顶点,并置访问标志数组相应分量值为true InitQueue(Q); //辅助队列Q初始化, 置空 EnQueue(Q,v); //v进队 while { ! QueueEmpty {Q)) //队列非空 { DeQueue (Q, u); //队头元素出队并置为u for(w=FirstAdjVex(G,u);w>=O;w=NextAdjVex(G,u,w)) //依次检查u的所有邻接点w, FirstAdjVex(G,u)表示u的第一个邻接点 //NextAdjVex(G,u,w)表示u相对于w的下一个邻接点,w;;,.o表示存在邻接点 if (!visited [w]) // w为u的尚未访问的邻接顶点 { cout<<w; visited[w]=true; //访问 w, 并置访问标志数组相应分扯值为true EnQueue (Q, w) ; //w进队 } } }
- 算法分析
-
广度优先搜索遍历图的时间复杂度和深度优先搜索遍历相同,即当用邻接矩阵存储时,时间复杂度为O(n^2 ); 用邻接表存储时,时间复杂度为O(n+ e)。两种遍历方法的不同之处仅仅在于对顶点访问的顺序不同。
-
- 算法:广度优先搜索遍历连通图
- 6.5.1 深度优先搜索
- 6.6 图的应用
- 6.6.1 最小生成树
- 1. 普里姆 Prim算法
- 算法实现
struct { VerTexType adjvex; //最小边在U中的那个顶点 ArcType lowcost; //最小边上的权值 ) closedge [MVNum) ; void MiniSpanTree_Prim(AMGraph G,VerTexType u) {//无向网G以邻接矩阵形式存储, 从顶点u出发构造G的最小生成树T, 输出T的各条边 k=LocateVex(G,u); Ilk 为顶点 u 的下标 for(j=O;j<G.vexnum;++j) //对v-u 的每一个顶点 Vj, 初始化 closedge[j] if(j!=k) closedge[j]={u,G.arcs[k][j]}; //{adjvex, lowcost} closedge[k].lowcost=O; //初始, U={u} for(i=l;i<G.vexnum;++i) {//选择其余 n-1 个顶点,生成 n-1 条边(n=G.vexnum) k=Min(closedge); //求出 T 的下一个结点:第 K 个顶点, closedge[k]中存有当前最小边 u0=closedge[k] .adjvex; //u0 为最小边的一个顶点,u0∈U v0=G.vexs[k]; //v0 为最小边的另一个顶点, v0∈V-U cout<<u0<<v0; //输出当前的最小边(u0, v0) closedge[k] .lowcost=0; 第k个顶点并入u集 for(j=O;j<G.vexnum;++j) if(G.arcs[k] [j]<closedge[j] .lowcost) //新顶点并入u 后重新选择最小边 closedge [j l={G.vexs [kl ,G.arcs [kl [j l}; } }
- 算法分析
-
普里姆算法的时间复杂度为 O(n^2 ), 与网中的边数无关, 因此适用千求稠密网的最小生成树。
-
- 算法实现
- 2. 克鲁斯卡尔 Kruskal算法
- 算法实现
struct { VerTexType Head; //边的始点 VerTexType Tail; //边的终点 ArcType lowcost; //边上的权值 } Edge [ arcnum] ; int Vexset[MVNum]; void MiniSpanTree_ Kruskal(AMGraph G) {//无向网G以邻接矩阵形式存储,构造G的最小生成树T, 输出T的各条边 Sort (Edge); //将数组 Edge 中的元素按权值从小到大排序 for(i=O;i<G.vexnum;++i) //辅助数组,表示各顶点自成一个连通分量 Vexset[i]=i; for(i=O;i<G.arcnum;++i) //依次查看数组 Edge 中的边 { vl=LocateVex(G,Edge[i] .Head); //vl 为边的始点 Head 的下标 v2=LocateVex(G,Edge[i] .Tail); //v2 为边的终点 Tail的下标 vsl=Vexset[vl]; //获取边 Edge[i]的始点所在的连通分量 vsl vs2=Vexset[v2]; //获取边 Edge[i]的终点所在的连通分量 vs2 if(vsl!=vs2) //边的两个顶点分属不同的连通分量 { cout«Edge[i] .Head «Edge[i] .Tail;//输出此边 for(j=O;j<G.vexnurn;++j) //合并 VS1 和 VS2 两个分益, 即两个集合统一编号 if(Vexset[ j] ==vs2) Vexset [ j] =vsl; / /集合编号为 vs2 的都改为 vsl } } }
- 算法分析
-
克鲁斯卡尔算法的时间复杂度为 O(elog泸),与网中的边数有关,与普里姆算法相比,克鲁斯卡尔算法更适合千求稀疏网的最小生成树。
-
- 算法实现
- 1. 普里姆 Prim算法
- 6.6.2 最短路径
- 从某个源点到其余各顶点的最短路径
- 迪杰斯特拉 Dijkstra算法
- 算法实现
void ShortestPath_DIJ(AMGraph G, int v0) {//用Dijkstra算法求有向网G的vO顶点到其余顶点的最短路径 n=G. vexnum; //n为G 中顶点的个数 for (v= O;v<n; ++v) //n个顶点依次初始化 { S[v]=false; //S初始为空集 D[v]=G.arcs[v0][v]; //将v0到各个终点的最短路径长度初始化为弧上的权值 if(D[v]<Maxlnt) Path[v]=v0;//如果v0和v之间有弧, 则将v的前驱置为v0 else Path[v]=-1; //如果v0 和v之间无弧, 则将v的前驱置为-1 } S [v0] = true; //将v0加人 S D [v0]=0; //源点到源点的距离为 0 /*初始化结束, 开始主循环, 每次求得vO到某个顶点v的最短路径, 将v加到s集*/ for( i=l; i<n;++i) //对其余 n-1个顶点,依次进行计算 { min= Maxlnt; for(w= 0;w<n;++w) if (! S [w] &&D [w] <min) {v=w;min=D[w];} //选择一条当前的最短路径,终点为v S[v]=true; //将v加入S for(w=0;w<n;++w) //更新从v。出发到集合v-s上所有顶点的最短路径长度 if (! S [w) && (D [v) +G. arcs [v) [w) <D [w])) { D [w] =D [v] +G. arcs [v] [w]; //更新 D[w] Path[w]=v; ////更改w的前驱为v } } }
- 算法分析
-
求解最短路径的主循环共进行 n - 1 次, 每次执行的时间是 O(n), 所以算法的时间复杂度是O(n^2)。 如果用带权的邻接表作为有向图的存储结构, 则虽然修改D 的时间可以减少,但由于在D 向址中选择最小分量的时间不变, 所以时间复杂度仍为 O(n2)。
-
- 算法实现
- 迪杰斯特拉 Dijkstra算法
- 每一对顶点之间的最短路径
- 弗洛伊德 Floyd算法
- 算法实现
void ShortestPath_Floyd(AMGraph G) {//用Floyd算法求有向网G中各对顶点1和)之间的最短路径 for (i=O; i < G. vexnum; ++i) //各对结点之间初始已知路径及距离 for(j=O;j <G.vexnum;++j) { D [ i J [ j J =G. arcs [ i J [ j J ; if(D[i] [j]<Maxint) Path[i] [j]=i;//如果 l.和]之间有弧,则将j的前驱置为l else Path[i] [j]=-1; } for (k=O; k < G. vexnum; ++k) for (i=O; i <G.vexnum;++i) for(j=O;j <G.vexnum;++j) if(D[i] [k]+D[k] [j] <D[i] [j]) //从i经k到]的一条路径更短 { D[i] [j]=D[i] [k]+D[k] [j];//更新D[i) [j J Path[i] [j]=Path[k] [j];/更改]的前驱为K } }
- 算法实现
- 弗洛伊德 Floyd算法
- 从某个源点到其余各顶点的最短路径
- 6.6.3 拓扑排序
- 算法实现
Status TopologicalSort(ALGraph G,int topo[]) {//有向图G采用邻接表存储结构 //若 G 无回路,则生成 G 的一个拓扑序列 topo []并返回 OK, 否则 ERROR FindinDegree(G,indegree); //求出各顶点的入度存入数组 indegree中 InitStack(S); //栈 s初始化为空 for(i=O;i<G.vexnum;++i) if (! indegree [i)) Push (S, i); //入度为0者进栈 m=0; ////对输出顶点计数,初始为0 while (! StackEmpty (S)) ////栈s非空 { Pop (S, i); //将栈顶顶点Vi出栈 topo[m)=i; //将Vi保存在拓扑序列数组 topo中 ++m; //对输出顶点计数 p=G.vertices[i) .firstarc; //p指向Vi的第一个邻接点 while (p ! =NULL) { k=p->adjvex; //vk为 m 的邻接点 --indegree[k); //vi的每个邻接点的入度减1 if(indegree[k)==0) Push(S,k); //若入度减为0, 则入栈 p=p->nextarc; //p指向顶点Vi下一个邻接结点 } } if(m<G.vexnum) return ERROR; //该有向图有回路 else return OK; }
- 算法分析
-
总的时间复杂度为O(n + e)。
-
- 算法实现
- 6.6.4 关键路径算法
- 算法实现
Status CriticalPath(ALGraph G) {//G为邻接表存储的有向网,输出G的各项关键活动 if(!TopologicalOrder(G,topo)) return ERROR; //调用拓扑排序算法,使拓扑序列保存在topo中,若调用失败, 则存在有向环, 返回ERROR n=G. vexnum; / /n为顶点个数 for(i=0;i<n; 丘+) //给每个事件的最早发生时间置初值0 ve[i]=0; /*- - - - 按拓扑次序求每个事件的最早发生时间 ----*/ for (i=O; i<n; i++) { k=topo[i]; //取得拓扑序列中的顶点序号K p=G.vertices[k].firstarc; //p指向k的第一个邻接顶点 while{p!=NULL) { //依次更新k的所有邻接顶点的最早发生时间 j=p->adjvex; //j为邻接顶点的序号 if(ve[j]<ve[k]+p->weight) //更新顶点 J 的最早发生时间 ve[j] ve[j]=ve[k]+p->weight; p=p->nextarc;//p指向k的下一个邻接顶点 } } for(i=O;i<n;i++) //给每个事件的最迟发生时间置初值 ve[n-1] vl[i]=ve[n-1]; /*-------------按逆拓扑次序求每个事件的最迟发生时间------------*/ for(i=n-l;i>=O;i--) { k=topo[i); //取得拓扑序列中的顶点序号K p=G.vertices[k) .firstarc; //p指向k的第一个邻接顶点 while(p!=NULL) //根据k的邻接点,更新k的最迟发生时间 { j=p->adjvex; //j为邻接顶点的序号 if(vl[k]>vl[j)-p->weight) //更新顶点 K 的最迟发生时间 vi [k] vl[k)=vl[j)-p->weight; p=p->nextarc; //p指向k的下一个邻接顶点 } } /*---------- - - - -- -判断每一活动是否为关键活动-------*/ for(i=0;i<n;i++)//每次循环针对m为活动开始点的所有活动 { p=G.vertices[i) .firs七arc; //p指向1的第一个邻接项点 while (p ! =NULL) { j=p->adjvex; //j为l.的邻接顶点的序号 e=ve[i); //计算活动<Vi, Vj>的最早开始时间 l=vl[j)-p->weight; //计算活动<Vi, Vj>的最迟开始时间 if (e==l) //若为关键活动,则输出<vi, Vj> cout<<G.vertices[i] .data <<G.vertices[j] .data; p=p->nextarc; //p指向 i 的下一个邻接顶点 } } }
- 算法分析
-
求关键路径算法的时间复杂度为 O(n+e)。
-
- 算法实现
- 6.6.1 最小生成树
- 6.7 案例分析与实现
- 六度空间理论
- 6.8 小结
- 6.1 图的定义和基本术语
- 心得体会
- 第六章主要学习图,有很多概念和算法学习,而且有些算法很容易造成混淆;总体感觉也觉得自己这章学的有点乱,但是经过知识构建,再仔细分析一下不同算法的不同点就可以挺好的区分开来。而且最近老师通过上课演示跟小测题目帮助我们更加的了解,更好的掌握DFS和BFS等算法。
- 这章主要还是得分清什么情况最好用什么算法,然后每个算法的过程实现又是怎样的,这章内容比较多,需要多看多复习。
- 有向图十字链表、无向图邻接多重表,最短路径Floyd算法,拓扑排序、关键路径老师都没有教学,自己大概看了看,比较乱比较难,然后就看不进去了,大概理解了一下。
- 有些错题还是得记录一下
- 小测:使用邻接矩阵a存储无向网络,若i号顶点与j号顶点之间不存在边,则a[i][j]值为多少?答案:若权值是一个正整数,可以设为0或负数。若权值是整数,则设为一个大于所有边权值的数(INT_MAX)
- 小测:对于连通图,其连通分量是什么? 本身(注意审题)