文章目录
- 1 概念
- 2 无向图的邻接表
- 2.1 示例
- 2.2 Mermaid 图示例
- 2.3 C++实现
- 2.3.1 简单实现
- 2.3.2 优化封装
- 2.4 总结
- 3 有向图的邻接表
- 3.1 示例
- 3.2 C++实现
- 3.3 总结
- 4 邻接图的遍历
- 5 拓展补充
- References
数据结构
1 概念
-
优点:空间效率高,适合稀疏图。动态性强,可以方便地添加或删除边。
- 邻接表表示法是一种高效表示稀疏图的方式。//
-
缺点:查找某条边是否存在的时间复杂度较高。
-
示例:
A: B -> D
B: A -> C -> D
C: B
D: A -> B
- 示例解释:顶点
A
连接到B
和D
,顶点B
连接到A
、C
和D
,以此类推。
2 无向图的邻接表
2.1 示例
假设有以下无向图,其中节点 A、B、C、D、E 表示城市,边表示城市之间的道路,权重表示道路的距离。
在邻接表(链式)表示法中,图的边权重是预先给定的,而不是通过某种计算得到的。它们通常是图的定义的一部分,表示从一个顶点到另一个顶点的距离、时间或其他成本。例如,在地图中的路径权重可以表示两个地点之间的距离。
A----3----B| |4 2| |C----1----D| 5 |E
对应的邻接表为:
A -> B(3) -> C(4)
B -> A(3) -> D(2)
C -> A(4) -> D(1) -> E(5)
D -> B(2) -> C(1)
E -> C(5)
2.2 Mermaid 图示例
e.g. 顶点 A
的邻接表:
- 顶点
A
连接到顶点B
,边的权重是3
。 - 顶点
A
再连接到顶点C
,边的权重是4
。 - 最后一个节点指向
^
表示链表结束。
2.3 C++实现
2.3.1 简单实现
#include <iostream>
#include <vector>
#include <unordered_map>using namespace std;// 表示图的结构: `Edge` 结构体表示图的边,包括目的顶点 `dest`、边的权重 `weight` 和指向下一条边的指针 `next`。
struct Edge {int dest; // 目的顶点int weight; // 边的权重Edge* next; // 指向下一条边的指针
};// `Vertex` 结构体表示图的顶点,包括顶点数据 `data` 和指向第一个相邻顶点的指针 `first`。
struct Vertex {char data; // 顶点数据Edge* first; // 指向第一个相邻顶点的指针
};// 初始化图: `addEdge` 函数用于向图中添加边,每次添加新边时,会将其插入到链表的头部。
void addEdge(Vertex vertices[], int src, int dest, int weight) {Edge* newEdge = new Edge{dest, weight, vertices[src].first};vertices[src].first = newEdge;
}void printGraph(Vertex vertices[], int V) {for (int i = 0; i < V; ++i) {cout << "顶点 " << vertices[i].data << " 的邻接表: ";Edge* edge = vertices[i].first;while (edge) {cout << " -> " << vertices[edge->dest].data << " (权重 " << edge->weight << ")";edge = edge->next;}cout << endl;}
}int main() {const int V = 5;Vertex vertices[V] = {{'A', nullptr}, {'B', nullptr}, {'C', nullptr}, {'D', nullptr}, {'E', nullptr}};// 根据图添加边addEdge(vertices, 0, 1, 3); // A -> BaddEdge(vertices, 0, 2, 4); // A -> CaddEdge(vertices, 1, 0, 3); // B -> AaddEdge(vertices, 1, 3, 2); // B -> DaddEdge(vertices, 2, 0, 4); // C -> AaddEdge(vertices, 2, 3, 1); // C -> DaddEdge(vertices, 2, 4, 5); // C -> EaddEdge(vertices, 3, 1, 2); // D -> BaddEdge(vertices, 3, 2, 1); // D -> CaddEdge(vertices, 4, 2, 5); // E -> CprintGraph(vertices, V);return 0;
}
封装实现:
优点:
- 直接性:直接使用链表来表示邻接表,比较直观。
- 高效性:链表的插入操作比较高效。
缺点: - 复杂性:需要手动管理内存,容易出现内存泄漏问题。
- 灵活性:不如 STL 容器灵活,操作起来相对繁琐。
#include <iostream>
#include <vector>
#include <unordered_map>using namespace std;struct Edge {int dest;int weight;Edge* next;
};struct Vertex {char data;Edge* first;
};class Graph {
public:// 构造函数,初始化顶点数量和顶点数组Graph(int vertices) : V(vertices) {vertex_list = new Vertex[V];for (int i = 0; i < V; ++i) {vertex_list[i].data = 'A' + i;vertex_list[i].first = nullptr;}}// 析构函数,释放所有动态分配的内存~Graph() {for (int i = 0; i < V; ++i) {Edge* current = vertex_list[i].first;while (current) {Edge* temp = current;current = current->next;delete temp;}}delete[] vertex_list;}void AddEdge(int src, int dest, int weight) {Edge* newEdge = new Edge{dest, weight, vertex_list[src].first};vertex_list[src].first = newEdge;}void PrintGraph() const {for (int i = 0; i < V; ++i) {cout << "顶点 " << vertex_list[i].data << " 的邻接表: ";Edge* edge = vertex_list[i].first;while (edge) {cout << " -> " << vertex_list[edge->dest].data << " (权重 " << edge->weight << ")";edge = edge->next;}cout << endl;}}private:int V;Vertex* vertex_list;
};int main() {Graph g(5);// 根据图添加边 g.AddEdge(0, 1, 3); // A -> B g.AddEdge(0, 2, 4); // A -> C g.AddEdge(1, 0, 3); // B -> A g.AddEdge(1, 3, 2); // B -> D g.AddEdge(2, 0, 4); // C -> A g.AddEdge(2, 3, 1); // C -> D g.AddEdge(2, 4, 5); // C -> E g.AddEdge(3, 1, 2); // D -> B g.AddEdge(3, 2, 1); // D -> C g.AddEdge(4, 2, 5); // E -> C g.PrintGraph();return 0;
}
执行后, 输出如下:
顶点 A 的邻接表: -> C (权重 4) -> B (权重 3) # 对于顶点 `A` 的邻接表:A -> C(4) -> B(3) 或者 A -> B(3) -> C(4) 都是正确的,它们表示的图结构是一样的。关键在于每个顶点的邻接节点及其对应的边权重是否正确记录。
顶点 B 的邻接表: -> D (权重 2) -> A (权重 3)
顶点 C 的邻接表: -> E (权重 5) -> D (权重 1) -> A (权重 4)
顶点 D 的邻接表: -> C (权重 1) -> B (权重 2)
顶点 E 的邻接表: -> C (权重 5)
2.3.2 优化封装
优点:
- 简洁性:代码更简洁,易于阅读和维护。
- 内存管理:使用 STL 容器,不需要手动管理内存,减少内存泄漏风险。
- 灵活性:STL 容器操作更灵活,提供了更多的功能。
缺点: - 抽象程度:链表的表示方式被隐藏在 STL 容器中,可能不够直观。
#include <iostream>
#include <vector>class Graph {
public:Graph(int vertices): vertices_(vertices) {adj_list_.resize(vertices_);}void AddEdge(int u, int v, int weight) {adj_list_[u].emplace_back(v, weight);adj_list_[v].emplace_back(u, weight); // 对于无向图,需要双向添加边}void PrintAdjList() const {for (int v = 0; v < vertices_; ++v) {std::cout << static_cast<char>('A' + v) << ": ";for (const auto& edge : adj_list_[v]) {std::cout << static_cast<char>('A' + edge.first) << " (权重 " << edge.second << ") ";}std::cout << std::endl;}}private:int vertices_;std::vector<std::vector<std::pair<int, int>>> adj_list_;
};int main() {Graph g(5);// 根据图添加边g.AddEdge(0, 1, 3); // A -> Bg.AddEdge(0, 2, 4); // A -> Cg.AddEdge(1, 3, 2); // B -> Dg.AddEdge(1, 4, 2); // B -> Eg.AddEdge(2, 3, 1); // C -> Dg.AddEdge(3, 4, 5); // D -> Eg.PrintAdjList();return 0;
}
2.4 总结
在邻接表表示法中,链表中顶点的顺序实际上是不重要的。邻接表的主要目的是表示每个顶点的邻接关系以及对应的边权重,因此,顶点的顺序并不会影响图的表示和算法的正确性。
总体来看,第二种封装方式更符合现代 C++ 编程规范,更加推荐。主要原因如下:
- 简洁性和可维护性:使用 STL 容器使代码更简洁,易于维护和扩展。
- 内存管理:STL 容器自动管理内存,减少内存泄漏的风险。
- 灵活性:STL 容器提供了丰富的操作接口,使用更加灵活。
当然, 如果你需要对图进行非常细粒度的控制,或者在非常严格的性能要求下,第一种封装方式可能更适合。
3 有向图的邻接表
假设有以下有向图,其中节点 A、B、C、D、E 表示城市,边表示城市之间的道路,权重表示道路的距离。
3.1 示例
A----3---->B| |4 2| |v vC<----1----D| 5 |v E
对应的邻接表为:
A -> B(3) -> C(4)
B -> D(2)
C -> E(5)
D -> C(1)
E ->
3.2 C++实现
#include <iostream>
#include <vector>class Graph {
public:Graph(int vertices): vertices_(vertices) {adj_list_.resize(vertices_);}void AddEdge(int u, int v, int weight) {adj_list_[u].emplace_back(v, weight);}/*// 对比无向图的, 向图中添加边:void AddEdge(int u, int v, int weight) {adj_list_[u].emplace_back(v, weight);adj_list_[v].emplace_back(u, weight); // 对于无向图,需要双向添加边}*/void PrintAdjList() const {for (int v = 0; v < vertices_; ++v) {std::cout << static_cast<char>('A' + v) << ": ";for (const auto& edge : adj_list_[v]) {std::cout << static_cast<char>('A' + edge.first) << " (权重 " << edge.second << ") ";}std::cout << std::endl;}}private:int vertices_;std::vector<std::vector<std::pair<int, int>>> adj_list_;
};int main() {Graph g(5);g.AddEdge(0, 1, 3); // A -> Bg.AddEdge(0, 2, 4); // A -> Cg.AddEdge(1, 3, 2); // B -> Dg.AddEdge(3, 2, 1); // D -> Cg.AddEdge(2, 4, 5); // C -> Eg.PrintAdjList();return 0;
}
3.3 总结
有向图表示的邻接表结构和无向图类似,只是边的方向性需要注意。
4 邻接图的遍历
// **图的遍历(DFS 和 BFS)**
#include <iostream>
#include <vector>
#include <stack>
#include <queue>class Graph {
public:Graph(int vertices): vertices_(vertices) {adj_list_.resize(vertices_);}void AddEdge(int u, int v, int weight) {adj_list_[u].emplace_back(v, weight);}void DFS(int start) {std::vector<bool> visited(vertices_, false);std::stack<int> stack;stack.push(start);while (!stack.empty()) {int v = stack.top();stack.pop();if (!visited[v]) {std::cout << static_cast<char>('A' + v) << " ";visited[v] = true;}for (const auto& edge : adj_list_[v]) {if (!visited[edge.first]) {stack.push(edge.first);}}}std::cout << std::endl;}void BFS(int start) {std::vector<bool> visited(vertices_, false);std::queue<int> queue;queue.push(start);visited[start] = true;while (!queue.empty()) {int v = queue.front();queue.pop();std::cout << static_cast<char>('A' + v) << " ";for (const auto& edge : adj_list_[v]) {if (!visited[edge.first]) {queue.push(edge.first);visited[edge.first] = true;}}}std::cout << std::endl;}void PrintAdjList() const {for (int v = 0; v < vertices_; ++v) {std::cout << static_cast<char>('A' + v) << ": ";for (const auto& edge : adj_list_[v]) {std::cout << static_cast<char>('A' + edge.first) << " (权重 " << edge.second << ") ";}std::cout << std::endl;}}private:int vertices_;std::vector<std::vector<std::pair<int, int>>> adj_list_;
};int main() {Graph g(5);g.AddEdge(0, 1, 3); // A -> Bg.AddEdge(0, 2, 4); // A -> Cg.AddEdge(1, 3, 2); // B -> Dg.AddEdge(3, 2, 1); // D -> Cg.AddEdge(2, 4, 5); // C -> Estd::cout << "邻接表:" << std::endl;g.PrintAdjList();std::cout << "DFS 从 A 开始:" << std::endl;g.DFS(0);std::cout << "BFS 从 A 开始:" << std::endl;g.BFS(0);return 0;
}
5 拓展补充
- 时间复杂度分析:
- 添加边:O(1) - 在邻接表中添加一条边的时间复杂度为常数时间,因为只需将新边添加到链表头部。
- 删除边:O(E) - 删除一条边可能需要遍历整个链表,时间复杂度为 O(E),其中 E 是链表的长度。
- 查找邻接点:O(V) - 查找某个顶点的所有邻接点的时间复杂度为 O(V),其中 V 是顶点的数量。
- 查找某条边:O(E) - 查找某条边是否存在的时间复杂度为 O(E),其中 E 是链表的长度。
- 图的遍历:
- **深度优先搜索(DFS)和广度优先搜索(BFS)**都可以在邻接表上高效实现。时间复杂度均为 O(V + E),其中 V 是顶点的数量,E 是边的数量。
- 存储结构:
- 邻接表可以使用数组、链表、向量(
std::vector
)、哈希表(std::unordered_map
)等数据结构来实现,具体选择取决于需求和编程语言。
- 邻接表可以使用数组、链表、向量(
- 内存消耗:
- 相比邻接矩阵(Adjacency Matrix),邻接表在稀疏图(Sparse Graph)上更加节省内存。对于具有 V 个顶点和 E 条边的图,邻接矩阵需要 O(V^2) 的空间,而邻接表只需要 O(V + E) 的空间。
- 变种:
- 加权图:每条边都有权重(已在示例中展示)。
- 有向图和无向图:有向图的每条边有方向,反映在邻接表中只在一个方向上添加边;无向图在两个顶点之间添加双向边。
- 多重图(Multigraph):允许在两个顶点之间存在多条边。邻接表可以通过链表或向量来支持多重图。
- 图的表示法转换:
- 邻接表可以轻松转换为邻接矩阵,反之亦然,但在稀疏图上邻接表更有效。
- 动态图:
- 对于动态变化的图(例如频繁添加或删除边),邻接表比邻接矩阵更具优势,因为添加和删除操作更高效。