【高阶数据结构】第三弹---图的存储与遍历详解:邻接表构建与邻接矩阵的BFS/DFS实现

【高阶数据结构】第三弹---图的存储与遍历详解:邻接表构建与邻接矩阵的BFS/DFS实现

✨个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【高阶数据结构】

目录

1、图的存储结构

1.1、邻接表

1.1.1、边的结构

1.1.2、图的基本结构

1.1.3、图的创建

1.1.4、获取顶点下标

1.1.5、添加边

1.1.6、打印

1.1.7、测试一

1.1.8、测试二

2、图的遍历

2.1、图的广度优先遍历

2.2、图的深度优先遍历 


1、图的存储结构

1.1、邻接表

邻接表使用数组表示顶点的集合,使用链表表示边的关系

1. 无向图邻接表存储 

注意无向图中同一条边在邻接表中出现了两次。如果想知道顶点vi的度,只需要知道顶点vi边链表集合中结点的数目即可。 

2. 有向图邻接表存储

注意:有向图中每条边在邻接表中只出现一次,与顶点vi对应的邻接表所含结点的个数,就是该顶点的出度,也称出度表,要得到vi顶点的入度,必须检测其他所有顶点对应的边链表,看有多少边顶点的dst取值是i。 

1.1.1、边的结构

1、邻接表使用链表来存储边之间的关系,因此成员需要next指针,除此之外还需要起始点(此处可以省略,因为使用目标点即可实现图)目标点权值



2、此处还需要实现一个有参构造函数参数包括目标点下标和权值

3、权值需要使用类型模板参数
template<class W> struct Edge { // int _srci; // 起始点,入边使用 int _dsti; // 目标点下标,出边使用 W _w; // 权值 Edge<W>* _next; Edge(size_t dsti, const W& w) :_dsti(dsti) ,_w(w) ,_next(nullptr) {} };

1.1.2、图的基本结构

1、使用邻接表存储图的基本结构与使用邻接矩阵存储图的基本结构类似,邻接表同样的使用模板实现,但是无需表示无穷大的非类型模板参数,因为此处为链表结构,添加边的时候才需要new新的结点

2、成员变量包括顶点集合,顶点与下标的映射关系和领接表
template<class V, class W, bool Direction = false> class Graph { typedef Edge<W> Edge; public: // ... private: vector<V> _vertexs; // 顶点集合 map<V, int> _vIndexMap; // 顶点映射下标 vector<Edge*> _tables; // 邻接表 };

1.1.3、图的创建

图的创建与邻接矩阵一样,使用手动添加边的方式创建,即实现一个有参构造(参数包含定带你集合以及顶点个数)
Graph(const V* a, size_t n) { _vertexs.reserve(n); for (size_t i = 0; i < n; i++) { _vertexs.push_back(a[i]); _vIndexMap[a[i]] = i; } _tables.resize(n, nullptr); }

注意:此处的邻接表只需开辟一层空间,因为当添加边的时候,我们会new新的结点。

1.1.4、获取顶点下标

顶点与下标的映射存储在map中,获取顶点对应的下标只需要在map中进行查找即可,找到则返回下标,没找到则抛异常(为了编译通过,返回-1)。
// 根据顶点获取下标 size_t GetVertexIndex(const V& v) { auto it = _vIndexMap.find(v); if (it != _vIndexMap.end()) { return it->second; } else { throw invalid_argument("顶点不存在"); return -1; } }

1.1.5、添加边

1、参数为起始顶点,结束顶点和权值,还需注意有向与无向图

2、添加边的思想先获取顶点对应的下标,然后将新的结点头插到邻接表中,如果是无向图则需双向存储
void AddEdge(const V& src, const V& dst, const W& w) { size_t srci = GetVertexIndex(src); size_t dsti = GetVertexIndex(dst); // 1 -> 2 Edge* eg = new Edge(dsti, w); // 将Edge头插到邻接表 eg->_next = _tables[srci]; _tables[srci] = eg; // 无向图 2 -> 1 if (Direction == false) { Edge* eg = new Edge(srci, w); // 将Edge头插到邻接表 eg->_next = _tables[dsti]; _tables[dsti] = eg; } }

1.1.6、打印

打印的方式可以根据自己需求打印,此处打印下标与顶点的映射关系和邻接表,为了让打印更美观,将邻接表打印成   顶点[下标]->[顶点,下标,权值]...  的形式
void Print() { // 顶点 下标 -> 顶点值 for (size_t i = 0; i < _vertexs.size(); i++) { cout << "[" << i << "]->" << _vertexs[i] << endl; } cout << endl; // 邻接表 for (size_t i = 0; i < _tables.size(); i++) { // 顶点[下标]->[顶点,下标,权值]... cout << _vertexs[i] << "[" << i << "]->"; Edge* cur = _tables[i]; while (cur) { cout << "[" << _vertexs[cur->_dsti] << "," << cur->_dsti << "," << cur->_w << "]->"; cur = cur->_next; } cout << "nullptr" << endl; } }

1.1.7、测试一

顶点值为char类型。
void TestGraph1() { Graph<char, int, true> g("0123", 4); g.AddEdge('0', '1', 1); g.AddEdge('0', '3', 4); g.AddEdge('1', '3', 2); g.AddEdge('1', '2', 9); g.AddEdge('2', '3', 8); g.AddEdge('2', '1', 5); g.AddEdge('2', '0', 3); g.AddEdge('3', '2', 6); g.Print(); }

构造函数 

添加边

打印结果

1.1.8、测试二

顶点值为string类型。

测试代码 

void TestGraph2() { string a[] = { "张三", "李四", "王五", "赵六" }; Graph<string, int, true> g1(a, 4); g1.AddEdge("张三", "李四", 100); g1.AddEdge("张三", "王五", 200); g1.AddEdge("王五", "赵六", 30); g1.Print(); }

打印结果 

2、图的遍历

注意:此处的遍历操作都是基于邻接矩阵进行遍历的。

给定一个图G和其中任意一个顶点v0,从v0出发,沿着图中各边访问图中的所有顶点,且每个顶点仅被遍历一次"遍历"即对结点进行某种操作的意思

2.1、图的广度优先遍历

如何防止节点被重复遍历?

我们可以创建一个标记容器成员类型为bool类型,访问过则将值设置为true,默认为false。 

广度优先遍历思想:

与二叉树的层序遍历类似,需要借助队列先将第一个值入队列,出队列的同时把邻接顶点入队,直到队列为空则遍历结束,具体步骤如下:1、获取顶点对应的下标2、创建队列和标记容器3、入队4、队列不为空则遍历4.1、打印队头4.2、将front的邻接顶点入队有效边且标志位为false则入队

遍历代码 

// 根据顶点进行广度优先遍历,使用队列+标记容器 void BFS(const V& src) { // 1.获取顶点对应的下标 size_t srci = GetVertexIndex(src); // 2.创建队列和标记容器 queue<int> q; vector<bool> visited(_vertexs.size(),false); // 默认false可以访问 // 3.入队 q.push(srci); visited[srci] = true; // 不能访问 size_t n = _vertexs.size(); // 4.队列不为空则遍历 while (!q.empty()) { // 打印队头 int front = q.front(); q.pop(); cout << front << ":" << _vertexs[front] << endl; // 将front的邻接顶点入队 for (size_t i = 0; i < n; i++) { // 有效边且标志位为false则入队 if (_matrix[front][i] != MAX_W && !visited[i]) { q.push(i); visited[i] = true; } } } cout << endl; }

测试代码

void TestBDFS() { string a[] = { "张三", "李四", "王五", "赵六", "周七" }; Graph<string, int> g1(a, sizeof(a) / sizeof(string)); g1.AddEdge("张三", "李四", 100); g1.AddEdge("张三", "王五", 200); g1.AddEdge("王五", "赵六", 30); g1.AddEdge("王五", "周七", 30); g1.Print(); g1.BFS("张三"); }

结构及打印结果

遍历结果

上面确实实现了广度优先遍历,但是能不能像二叉树的层序遍历一样,打印每一层呢?

可以的。

与二叉树打印每一层类型,我们可以创建一个记录队列元素个数的变量,一次打印队列元素个数个,打印完一层之后更新这个值

一层层打印 

// 广度优先遍历(一层层打印) void BFS(const V& src) { // 1.获取顶点对应的下标 size_t srci = GetVertexIndex(src); // 2.创建队列和标记容器 queue<int> q; vector<bool> visited(_vertexs.size(), false); // 默认false可以访问 // 3.入队 q.push(srci); visited[srci] = true; // 不能访问 int levelSize = 1; size_t n = _vertexs.size(); // 4.队列不为空则遍历 while (!q.empty()) { // 一层层打印,一次打印队列个数个 for (int i = 0; i < levelSize; i++) { // 打印队头 int front = q.front(); q.pop(); cout << front << ":" << _vertexs[front] << endl; // 将front的邻接顶点入队 for (size_t i = 0; i < n; i++) { // 有效边且标志位为false则入队 if (_matrix[front][i] != MAX_W && !visited[i]) { q.push(i); visited[i] = true; } } } cout << endl; levelSize = q.size(); } }

遍历结果 

2.2、图的深度优先遍历 

深度优先遍历思想: 

与二叉树的前序遍历类似,但是此处需要使用标记容器,因此需要实现一个子函数具体步骤如下:1、获取顶点对应的下标2、创建标记容器3、调用子函数

子函数思想:1、打印该位置的值并将该位置的标记位设置为true(不能再访问)2、找一个该位置相邻的且没有访问过的点深度遍历

代码实现 

void _DFS(size_t srci, vector<bool>& visited) { // 下标:顶点值 cout << srci << ":" << _vertexs[srci] << endl; visited[srci] = true; // 不能再访问 // 找一个scri相邻的且没有访问过的点,深度遍历 for (size_t i = 0; i < _vertexs.size(); i++) { if (_matrix[srci][i] != MAX_W && !visited[i]) { _DFS(i, visited); } } } void DFS(const V& src) { size_t srci = GetVertexIndex(src); vector<bool> visited(_vertexs.size(), false); _DFS(srci, visited); }

运行结果

Read more

Python实现 MCP 客户端调用(高德地图 MCP 服务)查询天气示例

Python实现 MCP 客户端调用(高德地图 MCP 服务)查询天气示例

文章目录 * MCP 官网 * MCP 官方文档中文版 * 官方 MCP 服务示例 * Github * MCP 市场 * 简介 * 架构 * 高德地图 MCP 客户端示例 * python-sdk 客户端 * java-sdk 客户端 MCP 官网 * https://modelcontextprotocol.io/introduction MCP 官方文档中文版 * https://app.apifox.com/project/5991953 官方 MCP 服务示例 * https://github.com/modelcontextprotocol/servers Github * python-sdk:https://github.com/modelcontextprotocol/python-sdk * java-sdk:

By Ne0inhk
43-dify案例分享-MCP-Server让工作流秒变第三方可调用服务

43-dify案例分享-MCP-Server让工作流秒变第三方可调用服务

1.前言 之前我们为大家介绍过MCP SSE插件,它能够支持MCP-server在Dify平台上的调用,从而帮助Dify与第三方平台提供的MCP-server进行无缝对接。有些小伙伴提出了疑问:既然Dify可以通过MCP SSE插件调用其他平台的MCP-server,那么Dify的工作流或Chatflow是否也能发布为MCP-server,供其他支持MCP client的工具使用呢?今天,我们将为大家介绍一款Dify插件——mcp-server,它能够实现这一功能,即将Dify的工作流或Chatflow发布为MCP-server,供其他第三方工具调用。 插件名字叫做MCP-server,我们在dify插件市场可以找到这个工具 Mcp-server 是一个由 Dify 社区贡献的 Extension 类型插件。安装后,你可以把任何 Dify 应用转变成符合 MCP 标准的 Server Endpoint,供外部 MCP 客户端直接访问。它的主要功能包括: * **暴露为 MCP 工具:**将 Dify 应用抽象为单一 MCP 工具,供外部 MCP 客户端(如

By Ne0inhk
【大模型系列篇】大模型基建工程:基于 FastAPI 自动构建 SSE MCP 服务器

【大模型系列篇】大模型基建工程:基于 FastAPI 自动构建 SSE MCP 服务器

今天我们将使用FastAPI来构建 MCP 服务器,Anthropic 推出的这个MCP 协议,目的是让 AI 代理和你的应用程序之间的对话变得更顺畅、更清晰。FastAPI 基于 Starlette 和 Uvicorn,采用异步编程模型,可轻松处理高并发请求,尤其适合 MCP 场景下大模型与外部系统的实时交互需求,其性能接近 Node.js 和 Go,在数据库查询、文件操作等 I/O 密集型任务中表现卓越。 开始今天的正题前,我们来回顾下相关的知识内容: 《高性能Python Web服务部署架构解析》、《使用Python开发MCP Server及Inspector工具调试》、《构建智能体MCP客户端:完成大模型与MCP服务端能力集成与最小闭环验证》   FastAPI基础知识 安装依赖 pip install uvicorn, fastapi FastAPI服务代码示例  from fastapi import FastAPI app

By Ne0inhk
【MCP】详细了解MCP协议:和function call的区别何在?如何使用MCP?

【MCP】详细了解MCP协议:和function call的区别何在?如何使用MCP?

本文介绍了MCP大模型上下文协议的的概念,并对比了MCP协议和function call的区别,同时用python sdk为例介绍了mcp的使用方式。 1. 什么是MCP? 官网:https://modelcontextprotocol.io/introduction 2025年,Anthropic提出了MCP协议。MCP全称为Model Context Protocol,翻译过来是大模型上下文协议。这个协议的主要为AI大模型和外部工具(比如让AI去查询信息,或者让AI操作本地文件)之间的交互提供了一个统一的处理协议。我们常用的USB TypeC接口(USB-C)统一了USB接口的样式,MCP协议就好比AI大模型中的USB-C,统一了大模型与工具的对接方式。 MCP协议采用了C/S架构,也就是服务端、客户端架构,能支持在客户端设备上调用远程Server提供的服务,同时也支持stdio流式传输模式,也就是在客户端本地启动mcp服务端。只需要在配置文件中新增MCP服务端,就能用上这个MCP服务器提供的各种工具,大大提高了大模型使用外部工具的便捷性。 MCP是开源协议,能让所有A

By Ne0inhk