第六章学习小结
图和树有点像,或者说是变化更多更复杂的森林也不为过。
先清晰一下概念:
图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。在图中的数据元素,我们称之为顶点(Vertex),顶点集合有穷非空。在图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。
图按照边的有无方向分为无向图和有向图。无向图由顶点和边组成,有向图由顶点和弧构成。弧有弧尾和弧头之分,带箭头一端为弧头。
图按照边或弧的多少分稀疏图和稠密图。如果图中的任意两个顶点之间都存在边叫做完全图,有向的叫有向完全图。若无重复的边或顶点到自身的边则叫简单图。
图中顶点之间有邻接点、依附的概念。无向图顶点的边数叫做度。有向图顶点分为入度和出度。
图上的边或弧带有权则称为网。
图中顶点间存在路径,两顶点存在路径则说明是连通的,如果路径最终回到起始点则称为环,当中不重复的叫简单路径。若任意两顶点都是连通的,则图就是连通图,有向则称为强连通图。图中有子图,若子图极大连通则就是连通分量,有向的则称为强连通分量。
无向图中连通且n个顶点n-1条边称为生成树。有向图中一顶点入度为0其余顶点入度为1的叫有向树。一个有向图由若干棵有向树构成生成森林。
--------------------------------------------
基于此大概还是对于复杂,变化多的非线性数据进行处理,以下是几种存储方法。
邻接矩阵的表示,其对于稀疏图的处理很浪费空间,所以一般用于稠密图:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> using namespace std; enum Graphkind{ DG, DN, UDG, UDN }; //{有向图,无向图,有向网,无向网} typedef struct Node { int * vex; //顶点数组 int vexnum; //顶点个数 int edge; //图的边数 int ** adjMatrix; //图的邻接矩阵 enum Graphkind kind; }MGraph; void createGraph(MGraph & G,enum Graphkind kind) { cout << "输入顶点的个数" << endl; cin >> G.vexnum; cout << "输入边的个数" << endl; cin >> G.edge; //输入种类 //cout << "输入图的种类:DG:有向图 DN:无向图,UDG:有向网,UDN:无向网" << endl; G.kind = kind; //为两个数组开辟空间 G.vex = new int[G.vexnum]; G.adjMatrix = new int*[G.vexnum]; cout << G.vexnum << endl; int i; for (i = 0; i < G.vexnum; i++) { G.adjMatrix[i] = new int[G.vexnum]; } for (i = 0; i < G.vexnum; i++) { for (int k = 0; k < G.vexnum; k++) { if (G.kind == DG || G.kind == DN) { G.adjMatrix[i][k] = 0; } else { G.adjMatrix[i][k] = INT_MAX; } } } /*//输入每个元素的信息,这个信息,现在还不需要使用 for (i = 0; i < G.vexnum; i++) { cin >> G.vex[i]; }*/ cout << "请输入两个有关系的顶点的序号:例如:1 2 代表1号顶点指向2号顶点" << endl; for (i = 0; i < G.edge; i++) { int a, b; cin >> a; cin >> b; if (G.kind == DN) { G.adjMatrix[b - 1][a - 1] = 1; G.adjMatrix[a - 1][b - 1] = 1; } else if (G.kind == DG) { G.adjMatrix[a - 1][b - 1] = 1; } else if (G.kind == UDG) { int weight; cout << "输入该边的权重:" << endl; cin >> weight; G.adjMatrix[a - 1][b - 1] = weight; } else { int weight; cout << "输入该边的权重:" << endl; cin >> weight; G.adjMatrix[b - 1][a - 1] = weight; G.adjMatrix[a - 1][b - 1] = weight; } } } void print(MGraph g) { int i, j; for (i = 0; i < g.vexnum; i++) { for (j = 0; j < g.vexnum; j++) { if (g.adjMatrix[i][j] == INT_MAX) cout << "∞" << " "; else cout << g.adjMatrix[i][j] << " "; } cout << endl; } } void clear(MGraph G) { delete G.vex; G.vex = NULL; for (int i = 0; i < G.vexnum; i++) { delete G.adjMatrix[i]; G.adjMatrix[i] = NULL; } delete G.adjMatrix; } int main() { MGraph G; cout << "有向图例子:" << endl; createGraph(G, DG); print(G); clear(G); cout << endl; cout << "无向图例子:" << endl; createGraph(G, DN); print(G); clear(G); cout << endl; cout << "有向图网例子:" << endl; createGraph(G, UDG); print(G); clear(G); cout << endl; cout << "无向图网例子:" << endl; createGraph(G, UDN); print(G); clear(G); cout << endl; return 0; }
邻接表代码的实现,主要用于稀疏图
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> #include<string> using namespace std; typedef string Vertextype; //表结点结构 struct ArcNode { int adjvex; //某条边指向的那个顶点的位置(一般是数组的下标)。 ArcNode * nextarc; //指向下一个表结点 int weight; //这个只有网图才需要使用。 }; //头结点 struct Vnode { Vertextype data; //这个是记录每个顶点的信息(现在一般都不需要怎么使用) ArcNode * firstarc; //指向第一条依附在该顶点边的信息(表结点) }; // struct Graph { int kind; //图的种类(有向图:0,无向图:1,有向网:2,无向网:3) int vexnum; //图的顶点数 int edge; //图的边数 Vnode * node; //图的(顶点)头结点数组 }; void createGraph(Graph & g,int kind) { cout << "请输入顶点的个数:" << endl; cin >> g.vexnum; cout << "请输入边的个数(无向图/网要乘2):" << endl; cin >> g.edge; g.kind = kind; //决定图的种类 g.node = new Vnode[g.vexnum]; int i; cout << "输入每个顶点的信息:" << endl;//记录每个顶点的信息 for (i = 0; i < g.vexnum; i++) { cin >> g.node[i].data; g.node[i].firstarc=NULL; } cout << "请输入每条边的起点和终点的编号:" << endl; for (i = 0; i < g.edge; i++) { int a; int b; cin >> a; //起点 cin >> b; //终点 ArcNode * next=new ArcNode; next->adjvex = b - 1; if(kind==0 || kind==1) next->weight = -1; else {//如果是网图,还需要权重 cout << "输入该边的权重:" << endl; cin >> next->weight; } next->nextarc = NULL; //将边串联起来 if (g.node[a - 1].firstarc == NULL) { g.node[a - 1].firstarc=next; } else { ArcNode * p; p = g.node[a - 1].firstarc; while (p->nextarc)//找到该链表的最后一个表结点 { p = p->nextarc; } p->nextarc = next; } } } void print(Graph g) { int i; cout << "图的邻接表为:" << endl; for (i = 0; i < g.vexnum; i++) { cout << g.node[i].data<<" "; ArcNode * now; now = g.node[i].firstarc; while (now) { cout << now->adjvex << " "; now = now->nextarc; } cout << endl; } } int main() { Graph g; cout << "有向图的例子" << endl; createGraph(g,0); print(g); cout << endl; cout << "无向图的例子" << endl; createGraph(g, 1); print(g); cout << endl; return 0; }
十字链表的建立,其可以很好的处理顶点的入度:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
typedef string Vertextype; //表结点结构 struct ArcNode { int tailvex; //弧尾的下标,一般都是和对应的头结点下标相同 int headvex; //弧头的下标 ArcNode * hlink; //指向下一个弧头同为headvex的表结点 ,边是箭头的那边 ArcNode * tlink; //指向下一个弧尾为tailvex的表结点,边不是箭头的那边 int weight; //只有网才会用这个变量 }; //头结点 struct Vnode { Vertextype data; //这个是记录每个顶点的信息(现在一般都不需要怎么使用) ArcNode *firstin; //指向第一条(入度)在该顶点的表结点 ArcNode *firstout; //指向第一条(出度)在该顶点的表结点 };
还有邻接多重表,以下是无向图的存储结构:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<iostream> #include<string> using namespace std; //表结点 struct ArcNode { int mark; //标志位 int ivex; //输入边信息的那个起点 ArcNode * ilink; //依附在顶点ivex的下一条边的信息 int jvex; //输入边信息的那个终点 ArcNode * jlink; //依附在顶点jvex的下一条边的信息 int weight; }; //头结点 struct VexNode { string data; //顶点的信息,如顶点名称 ArcNode * firstedge; //第一条依附顶点的边 }; struct Graph { int vexnum; //顶点的个数 int edge; //边的个数 VexNode *node; //保存顶点信息 }; void createGraph(Graph & g) { cout << "请输入顶点的个数:" << endl; cin >> g.vexnum; cout << "请输入边的个数(无向图/网要乘2):" << endl; cin >> g.edge; g.node = new VexNode[g.vexnum]; int i; cout << "输入每个顶点的信息:" << endl;//记录每个顶点的信息 for (i = 0; i < g.vexnum; i++) { cin >> g.node[i].data; g.node[i].firstedge = NULL; } cout << "请输入每条边的起点和终点的编号:" << endl; for (i = 0; i < g.edge; i++) { int a, b; cin >> a; cin >> b; ArcNode * next = new ArcNode; next->mark = 0; next->ivex = a - 1; //首先是弧头的下标 next->jvex = b - 1; //弧尾的下标 next->weight = -1; next->ilink = NULL; next->jlink = NULL; //更新顶点表a-1的信息 if (g.node[a - 1].firstedge == NULL) { g.node[a - 1].firstedge = next; } else { ArcNode * now; now = g.node[a - 1].firstedge; while (1) { if (now->ivex == (a - 1) && now->ilink == NULL) { now->ilink = next; break; } else if (now->ivex == (a - 1) && now->ilink != NULL) { now = now->ilink; } else if (now->jvex == (a - 1) && now->jlink == NULL) { now->jlink = next; break; } else if (now->jvex == (a - 1) && now->jlink != NULL) { now = now->jlink; } } } //更新顶点表b-1 if (g.node[b - 1].firstedge == NULL) { g.node[b - 1].firstedge = next; } else { ArcNode * now; now = g.node[b - 1].firstedge; while (1) { if (now->ivex == (b - 1) && now->ilink == NULL) { now->ilink = next; break; } else if (now->ivex == (b - 1) && now->ilink != NULL) { now = now->ilink; } else if (now->jvex == (b - 1) && now->jlink == NULL) { now->jlink = next; break; } else if (now->jvex == (b - 1) && now->jlink != NULL) { now = now->jlink; } } } } } void print(Graph g) { int i; for (i = 0; i < g.vexnum; i++) { cout << g.node[i].data << " "; ArcNode * now; now = g.node[i].firstedge; while (now) { cout << "ivex=" << now->ivex << " jvex=" << now->jvex << " "; if (now->ivex == i) { now = now->ilink; } else if (now->jvex == i) { now = now->jlink; } } cout << endl; } } int main() { Graph g; createGraph(g); print(g); system("pause"); return 0; }
搞完之后,就牵扯到图的遍历了,分为深度搜索优先(逐渐深入)和广度搜索优先(对一层一层进行处理)
然后在应用里面学到了最小生成树,主要引入为计算多个带权值的最小路径。
普里姆算法(按点生成):
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<stdio.h> #include<string.h> #define INF 1000000 //无穷大 #define MAXN 21 //顶点个数最大值 int n, m; //顶点个数、边数 int Edge[MAXN][MAXN]; //邻接矩阵 int lowcost[MAXN]; //记录集合T1中各顶点到集合T内各顶点权值最小的边的权值 int nearvex[MAXN]; //记录集合T1中各顶点距离顶点集合T内哪个顶点最近;当nearvex[i] = -1时,表示顶点i属于集合T void prim(int u0) //从顶点u0出发执行prim算法 { int i, j; //循环变量 int sumweight = 0; //生成树的权值 for(i = 1; i <= n; i++) //初始化lowcost数组和nearvex数组 { lowcost[i] = Edge[u0][i]; nearvex[i] = u0; } nearvex[u0] = -1; for(i = 1; i < n; i++) { int min = INF; int v = -1; //在lowcost数组中找最小值 for(j = 1; j <=n; j++) if(nearvex[j]!=-1 && lowcost[j]<min) { v = j; min = lowcost[v]; } if(v != -1) //表示找到权值最小的边 { printf("%d %d %d\n", nearvex[v], v, lowcost[v]); nearvex[v] = -1; //将选出的边的状态置为-1,表示在集合T中 sumweight += lowcost[v]; //由于集合T新加入了顶点v,所以要更新lowcost中顶点v与T1集合中各顶点的距离,同时将更新的顶点记录到nextvex中 for(j = 1; j <= n; j++) if(nearvex[j]!=-1 && Edge[v][j]<lowcost[j]) { lowcost[j] = Edge[v][j]; nearvex[j] = v; } } } printf("The weight of MST is %d\n", sumweight); } int main() { int i, j; int u, v, w; scanf("%d%d", &n, &m); memset(Edge, 0, sizeof(Edge)); //构造邻接矩阵 for(i = 1; i <= m; i++) { scanf("%d%d%d", &u, &v, &w); Edge[u][v] = Edge[v][u] = w; } for(i = 1; i <= n; i++) for(j = 1; j <=n; j++) { if(i == j) Edge[i][j] = 0; else if(Edge[i][j] == 0) Edge[i][j] = INF; } printf("The lines chosen are :\n"); prim(1); //从顶点1出发构造最小生成树 return 0;
克鲁斯卡尔算法(按边生成):
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <stdio.h> #include <stdlib.h> #define MAXEDGE 20 #define MAXVEX 20 #define INFINITY 65535 typedef struct { int arc[MAXVEX][MAXVEX]; int numVertexes, numEdges;//顶点数,边数 }MGraph; typedef struct { int begin; int end; int weight; }Edge; //对边集数组Edge结构的定义 //创建图的邻接矩阵 void CreateMGraph(MGraph *G) { int i, j; G->numEdges=11; G->numVertexes=7; for (i = 0; i < G->numVertexes; i++) { for ( j = 0; j < G->numVertexes; j++) { if (i==j) G->arc[i][j]=0; else G->arc[i][j] = G->arc[j][i] = INFINITY; } } G->arc[0][1]=7; G->arc[0][3]=5; G->arc[1][2]=8; G->arc[1][3]=9; G->arc[1][4]=7; G->arc[2][4]=5; G->arc[3][4]=15; G->arc[3][5]=6; G->arc[4][5]=8; G->arc[4][6]=9; G->arc[5][6]=11; for(i = 0; i < G->numVertexes; i++) { for(j = i; j < G->numVertexes; j++) { G->arc[j][i] =G->arc[i][j]; } } } //快速排序的条件 int cmp(const void* a, const void* b) { return (*(Edge*)a).weight - (*(Edge*)b).weight; } //找到根节点 int Find(int *parent, int f) { while ( parent[f] > 0) { f = parent[f]; } return f; } // 生成最小生成树 void MiniSpanTree_Kruskal(MGraph G) { int i, j, n, m; int k = 0; int parent[MAXVEX]; //用于寻找根节点的数组 Edge edges[MAXEDGE]; //定义边集数组,edge的结构为begin,end,weight,均为整型 // 用来构建边集数组并排序(将邻接矩阵的对角线右边的部分存入边集数组中) for ( i = 0; i < G.numVertexes-1; i++) { for (j = i + 1; j < G.numVertexes; j++) { if (G.arc[i][j] < INFINITY) { edges[k].begin = i; //编号较小的结点为首 edges[k].end = j; //编号较大的结点为尾 edges[k].weight = G.arc[i][j]; k++; } } } //为边集数组Edge排序 qsort(edges, G.numEdges, sizeof(Edge), cmp); for (i = 0; i < G.numVertexes; i++) parent[i] = 0; printf("打印最小生成树:\n"); for (i = 0; i < G.numEdges; i++) { n = Find(parent, edges[i].begin);//寻找边edge[i]的“首节点”所在树的树根 m = Find(parent, edges[i].end);//寻找边edge[i]的“尾节点”所在树的树根 //假如n与m不等,说明两个顶点不在一棵树内,因此这条边的加入不会使已经选择的边集产生回路 if (n != m) { parent[n] = m; printf("(%d, %d) %d\n", edges[i].begin, edges[i].end, edges[i].weight); } } } int main(void) { MGraph G; CreateMGraph(&G); MiniSpanTree_Kruskal(G); return 0; }
很完美!接下来是一道作业题
给定一个有N个顶点和E条边的无向图,请用DFS和BFS分别列出其所有的连通集。假设顶点从0到N−1编号。进行搜索时,假设我们总是从编号最小的顶点出发,按编号递增的顺序访问邻接点。
输入格式:
输入第1行给出2个整数N(0)和E,分别是图的顶点数和边数。随后E行,每行给出一条边的两个端点。每行中的数字之间用1空格分隔。
输出格式:
按照"{ v1 v2 ... vk }"的格式,每行输出一个连通集。先输出DFS的结果,再输出BFS的结果。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include <iostream> #include <string.h> #include <queue> using namespace std; int map[10][10]; bool visited[10]; int result[10]; int k; int n, m; //深搜算法 void dfs(int x) { result[k++] = x; visited[x] = true; for (int i = 0; i < n; i++) { if (map[x][i] == 1 && !visited[i]) { dfs(i); } } } /* 广搜 */ void bfs(int x) { queue<int> q; q.push(x); visited[x] = 1; result[k++] = x; while (!q.empty()) { int l = q.front(); q.pop(); for (int i = 0; i < n; i++) { if (map[l][i] == 1 && !visited[i]) { visited[i] = 1; result[k++] = i; q.push(i); } } } } int main() { cin >> n >> m; memset(visited, 0, sizeof(visited)); for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) map[i][j] = 0; while (m--) { int i, j; cin >> i >> j; map[i][j] = 1; map[j][i] = 1; } ///* 列出图深搜所有的连通集 */ for (int i = 0; i < n; i++) { k = 0; if (!visited[i]) { dfs(i); cout << "{ "; for (int i = 0; i < k; i++) cout << result[i] << " "; cout << "}" << endl; } } memset(visited, 0, sizeof(visited)); /* 列出图广搜所有的连通集 */ for (int i = 0; i < n; i++) { k = 0; if (!visited[i]) { bfs(i); cout << "{ "; for (int i = 0; i < k; i++) cout << result[i] << " "; cout << "}" << endl; } } }
其实就考了个深搜和广搜的处理,没什么技术含量。。。
007的还没肝完,写完了也许会在总结一下?