第六章我们学习到的是图,一种比树还要复杂一点的数据结构。
首先是图的定义: 图G有两个集合V和E组成,记为G=(V,E),其中V是顶点的有穷非空集合,E为V中顶点偶对的有穷集合,
这些顶点偶对成为边。V(G)和E(G)通常分为表图G的顶点集合和边集合,E(G)可以为空集。若E(G)为空,则图G
只有顶点而没有边;若边集E(G)为有向边的集合,则称该图有向图;若边集E(G)为无向边的集合,则称该图为无向图。
然后介绍一下比较重要的图的遍历
图的遍历
图的遍历和树的遍历类似,希望从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫图的遍历。
对于图的遍历来说,如何避免因回路陷入死循环,就需要科学地设计遍历方案,通过有两种遍历次序方案:深度优先遍历和广度优先遍历。
1.深度优先遍历
深度优先遍历,也有称为深度优先搜索,简称DFS。其实,就像是一棵树的前序遍历。
它从图中某个结点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中的所有顶点都被访问到为止。
邻接矩阵表示图代码:
void DFS_AM (AMGraph G,int v) {//图G为邻接矩阵类型,从第v个顶点出发深度优先搜索遍历图G cout<<v; visited[v]=true;//访问第v个顶点,并置访问的标志数组相应的分量值为true for(w=0;w<G.vexnum;w++)//依次检查邻接矩阵v所在的行 if((G.arcs[v][w]!=0)&&(!visited[w])) DFS_AM(G,w);//G.arcs[v][w]表示w是v的邻接点,如果w未访问,则递归调用DFS_AM }
邻接表表示图代码:
void DFS(GraphList g, int i) { EdgeNode *p; visited[i] = TRUE; cout<<g->adjList[i].data; //打印顶点,也可以其他操作 p = g->adjList[i].firstedge; while(p) { if(!visited[p->adjvex]) { DFS(g, p->adjvex); //对访问的邻接顶点递归调用 } p = p->next; } }
2. 广度优先遍历
广度优先遍历,又称为广度优先搜索,简称BFS。图的广度优先遍历就类似于树的层序遍历了。
代码如下:
//广度优先遍历 void BFS(AGraph* G,int v) { ANode *p; queue<int> qu; vector<int> flag(G->n); int w; cout<<v<<" "; flag[v]=1; qu.push(v); while(!qu.empty()) { w = qu.front(); qu.pop(); p = G->adjlist[w]->firstarc; while(p) { if(!flag[p->adjvex]) { cout<<p->adjvex<<" "; flag[p->adjvex] = 1; qu.push(p->adjvex); } p = p->nextarc; } } cout<<endl; }
然后就是图的应用
1.最小生成树
a.普里姆算法
Prim算法
Prim算法是用来解决最小生成树问题的。
基本思想:对图G(V,E)设置集合S,存放已经被访问的顶点,然后每次从集合V-S中选择与集合S的最短距离最小的一个顶点(记为u),访问并加入集合S。之后,令顶点u为中介点,优化所有从u能到达的顶点v与集合S之间的最短距离。这样的操作执行n次(n为顶点个数),直到集合S已包含所有顶点。
核心代码如下:
void prim(Graph G,int vcount,int father[]) { int i,j,k; int lowcost[max_vertexes]; int closeset[max_vertexes],used[max_vertexes]; int min; for (i=0;i<vcount;i++) { /* 最短距离初始化为其他节点到1号节点的距离 */ lowcost[i]=G[0][i]; /* 标记所有节点的依附点皆为默认的1号节点 */ closeset[i]=0; used[i]=0; father[i]=-1; } used[0]=1; /*第一个节点是在U集合里的*/ /* vcount个节点至少需要vcount-1条边构成最小生成树 */ for (i=1;i<=vcount-1;i++) { j=0; min = infinity; /* 找满足条件的最小权值边的节点k */ for (k=1;k<vcount;k++) /* 边权值较小且不在生成树中 */ if ((!used[k])&&(lowcost[k]<min)) { min = lowcost[k]; j=k; } father[j]=closeset[j]; used[j]=1;;//把第j个顶点并入了U中 for (k=1;k<vcount;k++) /* 发现更小的权值 */ if (!used[k]&&(G[j][k]<lowcost[k])) { lowcost[k]=G[j][k];/*更新最小权值*/ closeset[k]=j;;/*记录新的依附点*/ } } }
b.kruskal算法
kruskal算法
基本思想:假设连通网N=(V,{E})。则令最小生成树的初始状态为只有n个顶点而无边的非连通图T=(V,{}),图中每个顶点自成一个连通分量。在E中选择最小代价的边,若该边依附的顶点落在T中不同的连通分量中,则将该边加入到T中,否则舍去此边而选择下一条代价最小的边,依次类推,直到T中所有顶点都在同一连通分量上为止。
核心代码:
void MiniSpanTree_Kruskal(MGraph G) { int i, j, n , m; int k = 0; int parent[MAXVEX];/* 定义一数组用来判断边与边是否形成环路 */ Edge edges[MAXEDGE];/* 定义边集数组,edge的结构为begin,end,weight,均为整型 */ /* 此处省略将邻接矩阵G转换为边集数组edges并按权由小到大排列的代码*/ for (i = 0; i < G.numVertexes; i++) parent[i] = 0; cout << "打印最小生成树:" << endl; for (i = 0; i < G.numEdges; i++)/* 循环每一条边 */ { n = Find(parent, edges[i].begin); m = Find(parent, edges[i].end); if (n != m)/* 假如n与m不等,说明此边没有与现有的生成树形成环路 */ { parent[n] = m;/* 将此边的结尾顶点放入下标为起点的parent中。 */ /* 表示此顶点已经在生成树集合中 */ cout << "(" << edges[i].begin << ", " << edges[i].end << ") " << edges[i].weight << endl; } }
本周回顾:
上周确实非常忙碌,所以pta上作业完成程度只有一半,还没对作业进行整理总结,而且没有对第四第五章进行复习,导致小测成绩不尽人意,而
学到图之后,要处理的东西也越来越多,更需要花时间去消化和理解,对于算法的理解是很重要的,然后提高将算法转为可运行的代码的能力,这
个过程我觉得会很辛苦,但是收获也是很多的,所以还是给自己打打气,下周已没有活动,是时候专心去打代码了。
学习资料推荐:
帮助理解普里姆算法和克鲁斯卡尔算法的大牛博客:https://www.cnblogs.com/PJQOOO/p/3855017.html
帮助了解路径规划(最短路径)的博客:https://www.cnblogs.com/zhuweisky/archive/2005/09/29/246677.html