数据结构与算法——图(邻接表)及深度遍历、广度遍历
1. 图是什么
从公司回家有三条路线,考虑路况和安全性怎么选择?不考虑安全和路况的话又怎么选择?
在计算机科学中,一个图就是一些顶点的集合,这些顶点通过一系列边结对(连接)。顶点用圆圈表示,边就 是这些圆圈之间的连线。顶点之间通过边连接。注意:顶点有时也称为节点或者交点,边有时也称为链接。 社交网络,每一个人就是一个顶点,互相认识的人之间通过边联系在一起, 边表示彼此的关系。这种关系可以 是单向的,也可以是双向的!同时,边可以是双向的,也可以是单向的!
地图导航 - 起点、终点和分叉口(包括十字路口·、T 字路口等)都是顶点,导航经过的两顶点的路径就是边!
如上图所示,我们导航从一个点到另外一个点可以有条路径,路径不同,路况就不同,拥堵程度不同,所以导 致不同路径所花的时间也不一样,这种不同我们可以使用边的权重来表示,即根据每条边的实际情况给每一条 边分配一个正数或者负数值。考虑如下机票图,各个城市就是顶点,航线就是边。那么权重就是机票价格。
树和链表也都是图的特例!
如果我们有一个编程问题可以通过顶点和边表示,那么我们就可以将问题用图画出来,然后使用相应的图 算法来找到解决方案。
2. 图的表示
邻接列表
在邻接列表实现中,每一个顶点会存储一个从它这里开始的相邻边的列表。比如,如果顶点 B 有一条边到 A、 C 和 E,那么 B 的列表中会有 3 条边
邻接列表只描述指向外部的边。B 有一条边到 A,但是 A 没有边到 B8,97所9以43B84没0有1出11现1在 A 的邻接列表中。 查找两个顶点之间的边或者权重会比较费时,因为遍历邻接列表直到找到为止。
邻接矩阵
由二维数组对应的行和列都表示顶点,由两个顶点所决定的矩阵对应元素数值表示这里两个顶点是否相连(如, 0 表示不相连,非 0 表示相连和权值)、如果相连这个值表示的是相连边的权重。例如,广西到北京的机票, 我们用邻接矩阵表示:
往这个图中添加顶点的成本非常昂贵,因为新的矩阵结果必须重新按照新的行/列创建,然后将已有的数据复制 到新的矩阵中。
邻接列表与邻接矩阵如何选择?看下表:
大多数时候,选择邻接列表是正确的。(在图比较稀疏的情况下,每一个顶点都只会和少数几个顶点相 连,这种情况下邻接列表是最佳选择。如果这个图比较密集,每一个顶点都和大多数其他顶点相连,那么邻接 矩阵更合适。)
注意: V 表示图中顶点的个数,E 表示边的个数。
3. 图的算法实现
邻接表结构的定义
1 #define MaxSize 1024 2 3 //与节点连接的边的定义 4 typedef struct _EdgeNode 5 { 6 int adjvex; //邻接的顶点 7 int weight; //权重 8 struct _EdgeNode *next; //下一条边 9 }EdgeNode; 10 11 //顶点节点 12 typedef struct _VertexNode 13 { 14 char data; //节点数据 15 struct _EdgeNode *first; //指向邻接第一条边 16 }VertexNode, AdjList; 17 18 typedef struct _AdjListGraph 19 { 20 AdjList *adjlist; 21 int vex; //顶点数 22 int edge; //边数 23 }AdjListGraph;
邻接表的初始化
1 /*图的初始化*/ 2 void Init(AdjListGraph &G) 3 { 4 G.adjlist = new AdjList[MaxSize]; 5 G.edge = 0; 6 G.vex = 0; 7 }
邻接表的创建
1 /*图的创建*/ 2 void Create(AdjListGraph &G) 3 { 4 cout << "请输入该图的顶点数以及边数:" << endl; 5 cin>> G.vex >> G.edge; 6 cout<<"请输入相关顶点:"<< endl; 7 for(int i=0; i<G.vex; i++) 8 { 9 cin >> G.adjlist[i].data; 10 G.adjlist[i].first = NULL; 11 } 12 char v1=0, v2=0; //保存输入的顶点的字符 13 int i1, i2; //保存顶点在数组中的下标 14 cout<<"请输入想关联边的顶点:"<< endl; 15 16 for(int i=0; i<G.edge; i++) 17 { 18 cin >>v1 >>v2; 19 i1 = Location(G, v1); 20 i2 = Location(G, v2); 21 22 if(i1!=-1 && i2!=-1) //寻找到位置 23 { 24 EdgeNode *temp = new EdgeNode; 25 temp->adjvex = i2; 26 temp->next = G.adjlist[i1].first; 27 G.adjlist[i1].first = temp; 28 } 29 } 30 } 31 32 /*通过顶点对应的字符寻找顶点在图中的邻接点*/ 33 int Location(AdjListGraph &G, char c) 34 { 35 for(int i=0; i<G.vex; i++) 36 { 37 if(G.adjlist[i].data == c) 38 { 39 return i; 40 } 41 } 42 return -1; 43 }
4.邻接表的深度遍历
深度优先遍历思想:
- 首先以一个未被访问过的顶点作为起始顶点,沿当前顶点的边走到未访问过的顶点;
- 当没有未访问过的顶点时,则回到上一个顶点,继续试探别的顶点,直到所有的顶点都被访问过。
使用深度优先搜索来遍历这个图的具体过程是:
1. 首先从一个未走到过的顶点作为起始顶点,比如 A 顶点作为起点。
2. 沿 A 顶点的边去尝试访问其它未走到过的顶点,首先发现 E 号顶点还没有走到过,于是访问 E 顶点。
3. 再以 E 顶点作为出发点继续尝试访问其它未走到过的顶点,接下来访问 D 顶点。
4. 再尝试以 D 顶点作为出发点继续尝试访问其它未走到过的顶点。
5. 但是,此时沿 D 顶点的边,已经不能访问到其它未走到过的顶点,接下来返回到 E 顶点。
6. 返回到 E 顶点后,发现沿 E 顶点的边也不能再访问到其它未走到过的顶点。此时又回到顶点 A(D->E->A),再以 A 顶点作为出发点继续访问其它未走到过的顶点,于是接下来访问 C 顶点。
7. 。。。。。。。。。。
8. 最终访问的结果是 A -> E -> D -> C -> B
1 #include <iostream> 2 3 using namespace std; 4 5 #define MaxSize 1024 6 7 //与节点连接的边的定义 8 typedef struct _EdgeNode 9 { 10 int adjvex; //邻接的顶点 11 int weight; //权重 12 struct _EdgeNode *next; //下一条边 13 }EdgeNode; 14 15 //顶点节点 16 typedef struct _VertexNode 17 { 18 char data; //节点数据 19 struct _EdgeNode *first; //指向邻接第一条边 20 }VertexNode, AdjList; 21 22 typedef struct _AdjListGraph 23 { 24 AdjList *adjlist; 25 int vex; //顶点数 26 int edge; //边数 27 }AdjListGraph; 28 29 bool visited[MaxSize]; //全局数组,用来记录节点是否已被访问 30 31 int Location(AdjListGraph &G, char c); 32 33 /*图的初始化*/ 34 void Init(AdjListGraph &G) 35 { 36 G.adjlist = new AdjList[MaxSize]; 37 G.edge = 0; 38 G.vex = 0; 39 for (int i = 0; i < MaxSize; i++) 40 { 41 visited[i] = false; 42 } 43 } 44 45 /*图的创建*/ 46 void Create(AdjListGraph &G) 47 { 48 cout << "请输入该图的顶点数以及边数:" << endl; 49 cin>> G.vex >> G.edge; 50 cout<<"请输入相关顶点:"<< endl; 51 for(int i=0; i<G.vex; i++) 52 { 53 cin >> G.adjlist[i].data; 54 G.adjlist[i].first = NULL; 55 } 56 57 char v1=0, v2=0;//保存输入的顶点的字符 58 int i1, i2; //保存顶点在数组中的下标 59 cout<<"请输入想关联边的顶点:"<< endl; 60 for(int i=0; i<G.edge; i++) 61 { 62 cin >>v1 >>v2; 63 i1 = Location(G, v1); 64 i2 = Location(G, v2); 65 66 //寻找到位置 67 if(i1!=-1 && i2!=-1) 68 { 69 EdgeNode *temp = new EdgeNode; 70 temp->adjvex = i2; 71 temp->next = G.adjlist[i1].first; 72 G.adjlist[i1].first = temp; 73 } 74 } 75 } 76 77 /*通过顶点对应的字符寻找顶点在图中的邻接点*/ 78 int Location(AdjListGraph &G, char c) 79 { 80 for(int i=0; i<G.vex; i++) 81 { 82 if(G.adjlist[i].data == c) 83 { 84 return i; 85 } 86 } 87 return -1; 88 } 89 90 /*对图上的顶点进行深度遍历*/ 91 void DFS(AdjListGraph &G,int v) 92 { 93 int next = -1; 94 if(visited[v]) return; 95 cout<<G.adjlist[v].data<<" "; 96 visited[v] = true; //设置为已访问 97 EdgeNode *temp = G.adjlist[v].first; 98 99 while(temp) 100 { 101 next = temp->adjvex; 102 temp = temp->next; 103 if(visited[next] == false) 104 { 105 DFS(G, next); 106 } 107 } 108 } 109 110 /*对所有顶点进行深度遍历*/ 111 void DFS_Main(AdjListGraph &G) 112 { 113 for(int i=0; i<G.vex; i++) 114 { 115 if(visited[i] == false) 116 { 117 DFS(G, i); 118 } 119 } 120 } 121 122 int main() 123 { 124 AdjListGraph G; 125 //初始化 126 Init(G); 127 //创建图 128 Create(G); 129 //深度遍历 130 DFS_Main(G); 131 system("pause"); 132 }
5.邻接表的广度遍历
广度优先遍历思想
- 首先以一个未被访问过的顶点作为起始顶点,访问其所有相邻的顶点;
- 然后对每个相邻的顶点,再访问它们相邻的未被访问过的顶点,直到所有顶点都被访问过,遍历结束
1 /*对图上的顶点进行广度遍历*/ 2 void BFS(AdjListGraph &G,int v) 3 { 4 queue <int> q; 5 int cur; 6 int next; 7 q.push(v); 8 9 while (!q.empty()) //队列非空 10 { 11 cur = q.front(); //取队头元素 12 13 //当前顶点还没有被访问 14 if (visited[cur] == false) 15 { 16 cout << G.adjlist[cur].data << " "; 17 visited[cur] = true; //设置为已访问 18 } 19 20 q.pop(); //出队列 21 EdgeNode *tp = G.adjlist[cur].first; 22 while (tp != NULL) 23 { 24 next = tp->adjvex; 25 tp = tp->next; 26 q.push(next); //将第一个邻接点入队 27 } 28 } 29 } 30 31 /*对所有顶点进行广度遍历*/ 32 void BFS_Main(AdjListGraph &G) 33 { 34 for (int i = 0; i < G.vex; i++) 35 { 36 if (visited[i] == false) 37 { 38 BFS(G,i); 39 } 40 } 41 }
6.图的导航-最短路径算法
从起点开始访问所有路径,则到达终点节点的路径有多条,其中路径权值最短的一条则为最短路径。最短路径算法有 深度优先遍历、广度优先遍历、Bellman-Ford 算法、弗洛伊德算法、 SPFA(Shortest Path Faster Algorithm)算法和迪 杰斯特拉算法等。
源码实现
1 #include <iostream> 2 3 using namespace std; 4 5 #define MaxSize 1024 6 7 //与节点连接的边的定义 8 typedef struct _EdgeNode 9 { 10 int adjvex; //邻接的顶点 11 int weight; //权重 12 struct _EdgeNode *next; //下一条边 13 }EdgeNode; 14 15 //顶点节点 16 typedef struct _VertexNode 17 { 18 char data; //节点数据 19 struct _EdgeNode *first; //指向邻接第一条边 20 }VertexNode, AdjList; 21 22 typedef struct _AdjListGraph 23 { 24 AdjList *adjlist; 25 int vex; //顶点数 26 int edge; //边数 27 }AdjListGraph; 28 29 bool visited[MaxSize]={0}; //全局数组,用来记录节点是否已被访问 30 31 int Location(AdjListGraph &G, char c); 32 33 /*图的初始化*/ 34 void Init(AdjListGraph &G) 35 { 36 G.adjlist = new AdjList[MaxSize]; 37 G.edge = 0; 38 G.vex = 0; 39 for (int i = 0; i < MaxSize; i++) 40 { 41 visited[i] = false; 42 } 43 } 44 45 /*图的创建*/ 46 void Create(AdjListGraph &G) 47 { 48 cout << "请输入该图的顶点数以及边数:" << endl; 49 cin>> G.vex >> G.edge; 50 cout<<"请输入相关顶点:"<< endl; 51 52 for(int i=0; i<G.vex; i++) 53 { 54 cin >> G.adjlist[i].data; 55 G.adjlist[i].first = NULL; 56 } 57 58 char v1 = 0, v2 = 0;//保存输入的顶点的字符 59 int i1, i2; //保存顶点在数组中的下标 60 int weight = 0; 61 cout<<"请输入想关联边的顶点及权重:"<< endl; 62 63 for(int i=0; i<G.edge; i++) 64 { 65 cin >>v1 >>v2 >>weight; 66 i1 = Location(G, v1); 67 i2 = Location(G, v2); 68 69 //寻找到位置 70 if(i1!=-1 && i2!=-1) 71 { 72 EdgeNode *temp = new EdgeNode; 73 temp->adjvex = i2; 74 temp->next = G.adjlist[i1].first; 75 temp->weight = weight; 76 G.adjlist[i1].first = temp; 77 } 78 } 79 } 80 81 /*通过顶点对应的字符寻找顶点在图中的邻接点*/ 82 int Location(AdjListGraph &G, char c) 83 { 84 for(int i=0; i<G.vex; i++) 85 { 86 if(G.adjlist[i].data == c) 87 { 88 return i; 89 } 90 } 91 return -1; 92 } 93 94 int min_weights = 0x7FFFFFFF; //最大的整数 2 的 32 次方-1 95 int steps = 0; 96 int path[MaxSize] = {0}; //保存走过的路径 97 int shortest_path[MaxSize] = {0}; //保存最短的路径 98 99 /*对图上的顶点进行深度遍历*/ 100 void DFS(AdjListGraph &G,int start, int end, int weights) 101 { 102 int cur = -1; 103 //if(visited[start]) return; 104 cur = start; 105 106 //已找到终点,不用继续遍历 107 if(cur == end) 108 { 109 //打印所经过的所有路径 110 for (int i = 0; i < steps; i++) /// 输出所有可能的路径 111 { 112 cout<<G.adjlist[path[i]].data<<" "; //输出路径 113 } 114 115 printf("\t\t 该路径对应的长度是:%d\n", weights);//输出对应路径长度 116 117 if (min_weights > weights) //更新最小路径 118 { 119 min_weights = weights; 120 memcpy(shortest_path, path, steps*sizeof(int)); 121 } 122 } 123 124 visited[start] = true; //设置为已访问 125 EdgeNode *temp = G.adjlist[start].first; 126 127 while(temp) 128 { 129 int weight = temp->weight; 130 cur = temp->adjvex; 131 if(visited[cur] == false) 132 { 133 visited[cur] = true; //标记城市 i 已经在路径中 134 path[steps++] = cur; //保存路径到 path 数组中 135 DFS(G, cur, end, weights + weight); 136 visited[cur] = false; //之前一步探索完毕后,取消对城市 i 的标记以便另一条路径选择顶点 137 path[--steps] = 0; 138 } 139 temp = temp->next; 140 } 141 } 142 143 int main() 144 { 145 AdjListGraph G; 146 147 //初始化 148 Init(G); 149 150 //创建图 151 Create(G); 152 153 char start, end; 154 cout<<"请输入要查询的最短路径的起点和终点:"<<endl; 155 cin >>start >>end; 156 157 //求两点间的最短路径 158 DFS(G, Location(G, start), Location(G, end), 0); 159 cout<<"最小路径长度为:"<<min_weights<<endl; 160 cout<<"路径:"; 161 int i=0; 162 163 while(i<MaxSize && shortest_path[i]>0) 164 { 165 cout<<G.adjlist[shortest_path[i]].data<<" "; 166 i++; 167 } 168 cout<<endl; 169 170 system("pause"); 171 }