【CS.DS】数据结构 —— 图:深入了解三种表示方法之邻接表(Adjacency List)

news2025/1/16 5:01:01

文章目录

    • 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 连接到 BD,顶点 B 连接到 ACD,以此类推。

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 图示例

表节点链表
顶点
3
3
4
2
5
4
2
1
5
1
^
C
^
D
^
E
D
^
C
^
B
A
A
B
A
C
B
D
C
E

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 -> B
    addEdge(vertices, 0, 2, 4); // A -> C
    
    addEdge(vertices, 1, 0, 3); // B -> A
    addEdge(vertices, 1, 3, 2); // B -> D

    addEdge(vertices, 2, 0, 4); // C -> A
    addEdge(vertices, 2, 3, 1); // C -> D
    addEdge(vertices, 2, 4, 5); // C -> E

    addEdge(vertices, 3, 1, 2); // D -> B
    addEdge(vertices, 3, 2, 1); // D -> C

    addEdge(vertices, 4, 2, 5); // E -> C

    printGraph(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 -> B
    g.AddEdge(0, 2, 4); // A -> C
    g.AddEdge(1, 3, 2); // B -> D
    g.AddEdge(1, 4, 2); // B -> E
    g.AddEdge(2, 3, 1); // C -> D
    g.AddEdge(3, 4, 5); // D -> E

    g.PrintAdjList();

    return 0;
}

2.4 总结

在邻接表表示法中,链表中顶点的顺序实际上是不重要的。邻接表的主要目的是表示每个顶点的邻接关系以及对应的边权重,因此,顶点的顺序并不会影响图的表示和算法的正确性。

总体来看,第二种封装方式更符合现代 C++ 编程规范,更加推荐。主要原因如下:

  1. 简洁性和可维护性:使用 STL 容器使代码更简洁,易于维护和扩展。
  2. 内存管理:STL 容器自动管理内存,减少内存泄漏的风险。
  3. 灵活性:STL 容器提供了丰富的操作接口,使用更加灵活。

当然, 如果你需要对图进行非常细粒度的控制,或者在非常严格的性能要求下,第一种封装方式可能更适合。

3 有向图的邻接表

假设有以下有向图,其中节点 A、B、C、D、E 表示城市,边表示城市之间的道路,权重表示道路的距离。

3.1 示例

    A----3---->B
    |         |
    4         2
    |         |
    v         v
    C<----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 -> B
    g.AddEdge(0, 2, 4); // A -> C
    g.AddEdge(1, 3, 2); // B -> D
    g.AddEdge(3, 2, 1); // D -> C
    g.AddEdge(2, 4, 5); // C -> E

    g.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 -> B
    g.AddEdge(0, 2, 4); // A -> C
    g.AddEdge(1, 3, 2); // B -> D
    g.AddEdge(3, 2, 1); // D -> C
    g.AddEdge(2, 4, 5); // C -> E

    std::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):允许在两个顶点之间存在多条边。邻接表可以通过链表或向量来支持多重图。
  • 图的表示法转换
    • 邻接表可以轻松转换为邻接矩阵,反之亦然,但在稀疏图上邻接表更有效。
  • 动态图
    • 对于动态变化的图(例如频繁添加或删除边),邻接表比邻接矩阵更具优势,因为添加和删除操作更高效。

References

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1853715.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C语言】解决C语言报错:Syntax Error

文章目录 简介什么是Syntax ErrorSyntax Error的常见原因如何检测和调试Syntax Error解决Syntax Error的最佳实践详细实例解析示例1&#xff1a;缺少分号示例2&#xff1a;括号不匹配示例3&#xff1a;变量未声明示例4&#xff1a;拼写错误示例5&#xff1a;数据类型不匹配 进一…

低代码平台实践:打造高效动态表单解决方案的探索与思考

&#x1f525;需求背景 我司业务同事在抓取到候选人的简历之后&#xff0c;经常会出现&#xff0c;很多意向候选人简历信息不完整&#xff0c;一个个打电话确认的情况&#xff0c;严重影响了HR的工作效率&#xff0c;于是提出我们可以通过发送邮件、短信、H5链接的方式来提醒候…

低成本STC32G8K64驱动控制BLDC开源入门学习方案

低成本STC32G8K64驱动控制BLDC开源入门学习方案 ✨采用STC32G8K64单片机&#xff0c;参考梁工的STC32G12K128-LQFP48驱动方案制作&#xff0c;梁工BLDC相关的资料&#xff1a;https://www.stcaimcu.com/forum.php?modviewthread&tid7472&extrapage%3D1&#xff0c;在此…

Node.js是什么(基础篇)

前言 Node.js是一个基于Chrome V8 JavaScript引擎的开源、跨平台JavaScript运行时环境&#xff0c;主要用于开发服务器端应用程序。它的特点是非阻塞I/O模型&#xff0c;使其在处理高并发请求时表现出色。 一、Node JS到底是什么 1、Node JS是什么 Node.js不是一种独立的编程…

算法训练与程序竞赛题目集合(L4)

目录 L4-103 就不告诉你 输入格式&#xff1a; 输出格式&#xff1a; 输入样例&#xff1a; 输出样例&#xff1a; L4-104 Wifi密码 输入格式&#xff1a; 输出格式&#xff1a; 输入样例&#xff1a; 输出样例&#xff1a; L4-105 冠军魔术 输入格式&#xff1a; …

Ocam:高效录屏,屏幕录制最佳?

名人说&#xff1a;&#xff1a;一点浩然气&#xff0c;千里快哉风。 ——苏轼 创作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 目录 一、软件介绍1、Ocam2、核心特点 二、下载安装1、下载2、安装 三、使用方法 很高兴你…

BarTender中文版安装包下载及安装教程

​根据大数据结果显示可扩充的大容量卷标数据库&#xff1a;利用大量已设计好的标签库,从数以千计的现成标签尺寸中进行选择,也能够定义并加入自己的标签库尺寸。习惯上来说操作简单&#xff1a;BarTender条码打印软件是目前功能最强大、便捷的标签设计打印软件,在150 多个国家…

WMS项目测试点

这里写目录标题 最后附有图片 仓库系统 仓库 / 库区 仓库 新增仓库 编号 必填校验 字段长度校验 20为字符 数据类型校验 名称 必填校验 字段长度校验 20为字符 数据类型校验 备注 填写备注校验 字符长度限制 不填写备注校验 新增仓库之后是否可以通过查询仓库名称和仓库编号查询…

工业互联网的独特UI风格

工业互联网的独特UI风格

记录Nuxt3部署线上pm2启动项目修改端口

看官方文档&#xff1a; TNND&#xff0c;修改这个端口号顶个P用&#xff0c;毛用也没有 实际上应该是这样&#xff1a; 好了&#xff0c;误人子弟&#xff5e;

快速生成基于vue-element的后台管理框架,实现短时间二次开发

你是否遇到过当你想要独立开发一个项目时对反复造轮子的烦扰&#xff1f; 这种流水线的操作实在让人受不了 而vue-element-template很好的帮你解决了这个烦恼 只需克隆下来&#xff0c;改改图标&#xff0c;模块名&#xff0c;甚至样式&#xff0c;就会变成一个全新的自己的项目…

【Java】pcm 与 wav 格式互转工具类 (附测试用例)

文章目录 1. 前言1.1 背景1.2 目标1.3 亮点 2. 用例说明3. 补充验证4. 相关链接 1. 前言 git 仓库 https://github.com/ChenghanY/pcm-wav-converter 1.1 背景 系统新接入语音引擎。 语音引擎只认 pcm 格式数据。前端只认 wav 格式 。 需要后端对 pcm 和 wav 格式实现互转&a…

计算机组成原理 —— 存储系统(DRAM和SRAM)

计算机组成原理 —— 存储系统&#xff08;DRAM和SRAM&#xff09; DRAM和SRAMDRAM的刷新DRAM地址复用 我们今天来看DRAM和SRAM&#xff1a; DRAM和SRAM DRAM&#xff08;动态随机存取存储器&#xff09;和SRAM&#xff08;静态随机存取存储器&#xff09;都是半导体存储器&a…

中国真实婚恋相亲交友服务平台有哪些?全国靠谱恋爱脱单软件APP大全

终于成功脱单了&#xff01;在过去的这两年里&#xff0c;我动用了身边所有的资源&#xff0c;却始终未能找到理想的男朋友。无奈之下&#xff0c;只好将目光转向线上。经过长达半年的不懈坚持&#xff0c;终于寻觅到了心仪的对象&#xff01;接下来&#xff0c;我要把自己用过…

AtCoder Beginner Contest 359 A~C(D~F更新中...)

A.Count Takahashi 题意 给出 N N N个字符串&#xff0c;每个字符串为以下两种字符串之一&#xff1a; "Takahashi" "Aoki" 请你统计"Takahashi"出现了多少次。 分析 输入并统计即可。 代码 #include <bits/stdc.h>using namespa…

RNN循环卷积神经网络

1.定义 RNN &#xff08;Recurrent Neural Network&#xff0c;RNN&#xff09;循环卷积神经网络&#xff0c;用于处理序列数据。 序列数据&#xff1a;按照一定的顺序排列的数据&#xff0c;可以是时间顺序、空间顺序、逻辑顺序。 eg:电影、语言 2.特点 传统神经网络模型无法…

ping命令返回结果实例分析

测试在各相关情况下ping命令回复信息。 网络环境搭建如下图所示&#xff1a; 【1】R1、R2、PC1和PC2没有配置&#xff0c;测试ping命令回复 在路由器没有配置端口IP地址和路由&#xff0c;PC没有配置IP地址、子网掩码和网关的情况下&#xff0c;PC2 ping 192.168.1.1。 在PC没…

前后端经验分享:秋招春招赛道如何选择

前言&#xff1a;考研考公&#xff1f;国企互联网&#xff1f;老白小粉也曾对未来的方向选择产生迷茫&#xff0c;但最终老白小粉都选择了就业 →前端春招秋招经验分享 →后端春招秋招经验分享 因此今天这篇文章主要针对秋招春招的就业赛道给予学弟学妹们一些建议。 对于计算机…

38.MessageToMessageCodec线程安全可被共享Handler

handler被注解@Sharable修饰的。 这样的handler,创建一个实例就够了。例如: ByteToMessageCodec的子类不能被@Sharable修饰 如果自定义类是MessageToMessageCodec的子类就是线程共享的,可以被@Sharable修饰的 package com.xkj.protocol;import com.xkj.message.Message; i…

Python编程技巧:下划线的11种妙用,看看你知道几种?

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 文章内容 📒📝 用法一:Python控制台中的上次结果📝 用法二:命名变量的蛇形命名法(snake_case)📝 用法三:大数字的可读性📝 用法四:忽略不重要的值📝 用法五:用于吸收中间值📝 用法六:在for循环中忽略变量…