一、引言:图的表示方式
在图论算法中,高效地存储图结构是解决问题的关键。两种最常用的存储方式是邻接矩阵和邻接表,它们各有优缺点,适用于不同的场景。本文将深入探讨这两种存储结构,帮助读者根据具体需求选择合适的表示方法。
二、邻接矩阵:直观的二维数组表示
1.基本概念
邻接矩阵是一个 V* V 的二维数组(V 为顶点数),其中:
-
矩阵元素
matrix[i][j] = 1
表示顶点 i 和 j之间有边 -
matrix[i][j] = 0
表示无边
2.代码实现
#include <iostream> using namespace std; int main() { // 定义邻接矩阵(无向图) int neighbor[7][7] = { {0,0,0,0,0,0,0}, {0,0,1,1,0,0,0}, // 顶点1连接2和3 {0,1,0,1,0,1,0}, // 顶点2连接1、3和5 {0,1,1,0,1,1,1}, // 顶点3连接1、2、4、5、6 {0,0,0,1,0,0,0}, // 顶点4连接3 {0,0,1,1,0,0,1}, // 顶点5连接3和6 {0,0,0,1,0,1,0} // 顶点6连接3和5 }; // 计算顶点n的邻居数量 int n; cin >> n; int sum = 0; for(int i = 1; i <= 6; i++) { if(neighbor[n][i] == 1) { sum++; } } cout << "顶点" << n << "有" << sum << "个邻居"; return 0; }
3.特点分析
-
无向图:矩阵关于主对角线对称(
matrix[i][j] == matrix[j][i]
) -
有向图:非对称,
matrix[i][j]
表示从 i到 j的边 -
空间复杂度:O(V^2)
-
查询效率:O(1) 判断任意两点是否相邻
4.优缺点
优点 | 缺点 |
---|---|
直观易懂 | 空间占用大(O(V^2)) |
快速判断邻接关系 | 添加/删除顶点效率低 |
方便计算度数 | 不适合稀疏图(边数远小于V^2) |
三、邻接表:高效的动态存储
1.基本概念
邻接表为每个顶点维护一个链表(或动态数组),存储该顶点的所有邻居:
-
顶点 $i$ 的邻居列表包含所有与 $i$ 相邻的顶点
-
无向图中每条边存储两次
-
有向图中只存储出边或入边
2.代码实现
#include <iostream> #include <vector> using namespace std; const int N = 100; // 最大顶点数 int main() { vector<int> neighbor[N]; // 邻接表 // 构建无向图 neighbor[1].push_back(2); // 1-2 neighbor[2].push_back(1); neighbor[1].push_back(3); // 1-3 neighbor[3].push_back(1); neighbor[2].push_back(5); // 2-5 neighbor[5].push_back(2); // 计算顶点x的邻居数量 int x; cin >> x; cout << "顶点" << x << "有" << neighbor[x].size() << "个邻居"; return 0; }
3.DFS遍历实现
#include <iostream> #include <vector> using namespace std; vector<int> neighbor[8]; // 邻接表 bool visited[8]; // 访问标记数组 void dfs(int current) { cout << current << " "; // 访问当前顶点 visited[current] = true; // 标记已访问 // 递归访问所有未访问邻居 for(int i = 0; i < neighbor[current].size(); i++) { int next = neighbor[current][i]; if(!visited[next]) { dfs(next); } } } int main() { // 构建图结构(代码同前) // ... // 从顶点0开始DFS遍历 dfs(0); return 0; }
4.特点分析
-
空间复杂度:O(V + E)(E 为边数)
-
查询效率:O((v)) 获取顶点 v 的邻居
-
动态性:高效添加/删除边
5.优缺点
优点 | 缺点 |
---|---|
节省内存(稀疏图) | 判断两点是否相邻较慢 |
高效遍历邻接点 | 实现相对复杂 |
易于添加/删除边 | 存储有向图时需区分出/入边 |
四、’对比总结:如何选择存储结构
1.性能对比表
特性 | 邻接矩阵 | 邻接表 |
---|---|---|
空间占用 | O(V^2) | O(V + E) |
检查邻接 | O(1) | O(\text{degree}(v)) |
遍历邻居 | O(V) | O(\text{degree}(v)) |
添加边 | O(1) | O(1) |
删除边 | O(1) | O(\text{degree}(v)) |
添加顶点 | O(V^2) | O(1) |
2.适用场景建议
-
邻接矩阵适用场景:
-
稠密图(边数接近 V^2)
-
需要频繁检查两点是否相邻
-
图规模较小
-
需要实现图论中的数学运算
-
-
邻接表适用场景:
-
稀疏图(边数远小于 V^2)
-
需要遍历所有邻接点
-
图规模较大
-
需要动态添加/删除顶点
-
五、实战应用:DFS遍历比较
1.邻接矩阵DFS实现
int visited[8] = {}; int villager[8][8] = { {0,1,0,1,1,0,0,0}, {1,0,1,0,1,0,0,0}, {0,1,0,0,0,1,0,0}, {1,0,0,0,1,0,1,0}, {1,1,0,0,0,0,1,0}, {0,0,1,0,0,0,0,0}, {0,0,0,1,1,0,0,1}, {0,0,0,0,0,0,1,0}}; void dfs_matrix(int current) { visited[current] = 1; cout << current << " "; for(int j = 0; j < 8; j++) { if(villager[current][j] == 1 && visited[j] == 0) dfs_matrix(j); } }
2.邻接表DFS实现
vector<int> neighbor[8]; bool visited[8]; void dfs_list(int current) { cout << current << " "; visited[current] = true; for(int j = 0; j < neighbor[current].size(); j++) { int next = neighbor[current][j]; if(!visited[next]) dfs_list(next); } }
3.性能分析
在稠密图中,邻接矩阵的DFS效率更高;在稀疏图中,邻接表的DFS更优:
-
邻接矩阵DFS:时间复杂度 O(V^2)
-
邻接表DFS:时间复杂度 O(V + E)
六、总结与扩展
-
邻接矩阵适合稠密图和小规模图
-
邻接表适合稀疏图和大规模图
-
实际应用中常根据具体需求选择合适结构