实现一个 LRU 风格的缓存类
实现一个缓存类
- 需求描述
- 豆包
- 解决思路:
- 实现代码:
- 优化1
- 1. `std::list::remove` 的时间复杂度问题
- 2. 代码复用
- 优化后的代码
- 优化说明
- 优化2
- 1. 边界条件检查
- 2. 异常处理
- 3. 代码封装性
- 4. 线程安全
- 优化后的代码示例
- 优化说明
- DeepSeek(深度思考R1):
- 代码实现
- 实现说明:
- 优化方案一:基础互斥锁版本
- 优化方案二:读写锁版本(C++17+)
- 优化分析说明:
需求描述
类似于Windows系统的 win 菜单最近打开功能模块,使用最近最少使用(LRU)的缓存淘汰策略,缓存可以设置最大容量。
get:获取缓存数据,不存在时返回 -1
put:新增或者更新缓存,当容量已满时,删除最早的数据,再插入
class UpdateCaching {public:UpdateCaching(int capacity){}int get(int key){}void put(int key ,int value){}
}
豆包
解决思路:
-
使用
std::unordered_map存储键值对,以便快速查找和更新数据。(为啥使用 unordered_map 而不是 map) -
使用
std::list存储键的访问顺序,以便在容量满时删除最早的数据。 -
在
put操作中:- 检查缓存是否已满,如果已满,删除 list 中的第一个元素,并从
unordered_map中移除对应的键值对。 - 将新元素添加到
unordered_map中,并将键添加到list的末尾。
- 检查缓存是否已满,如果已满,删除 list 中的第一个元素,并从
-
在
get操作中:- 使用
unordered_map查找键是否存在,如果存在,将键移到list的末尾,表示最近访问过。
- 使用
实现代码:
#include <iostream>
#include <unordered_map>
#include <list>class UpdateCaching {
private:std::unordered_map<int, int> cache;std::list<int> keyOrder;int capacity;public:UpdateCaching(int cap) : capacity(cap) {}int get(int key) {if (cache.count(key) == 0) {return -1;}// 将访问的键移到列表末尾,表示最近使用过keyOrder.remove(key);keyOrder.push_back(key);return cache[key];}void put(int key, int value) {if (cache.count(key) > 0) {// 键已存在,更新值并将其移到列表末尾cache[key] = value;keyOrder.remove(key);keyOrder.push_back(key);return;}if (cache.size() >= capacity) {// 缓存已满,移除最久未使用的元素int oldestKey = keyOrder.front();keyOrder.pop_front();cache.erase(oldestKey);}// 插入新元素cache[key] = value;keyOrder.push_back(key);}
};int main() {UpdateCaching cache(3);cache.put(1, 10);cache.put(2, 20);cache.put(3, 30);std::cout << cache.get(2) << std::endl; // 输出 20cache.put(4, 40); // 缓存已满,删除最早的元素 (1, 10)std::cout << cache.get(1) << std::endl; // 输出 -1return 0;
}
优化1
1. std::list::remove 的时间复杂度问题
在 get 和 put 方法中,使用 keyOrder.remove(key) 来移动元素到列表末尾。std::list::remove 的时间复杂度是 O ( n ) O(n) O(n),因为它需要遍历整个列表来找到要删除的元素。可以通过维护一个指向每个键在列表中位置的映射,将这一操作的时间复杂度优化到 O ( 1 ) O(1) O(1)。
2. 代码复用
get 和 put 方法中都有将键移到列表末尾的操作,可以将这一操作提取成一个单独的方法,提高代码的复用性。
优化后的代码
#include <iostream>
#include <unordered_map>
#include <list>class UpdateCaching {
private:std::unordered_map<int, int> cache;std::unordered_map<int, std::list<int>::iterator> keyIterators;std::list<int> keyOrder;int capacity;// 将键移到列表末尾void moveToBack(int key) {auto it = keyIterators[key];keyOrder.erase(it);keyOrder.push_back(key);keyIterators[key] = --keyOrder.end();//更新映射容器中key对应的迭代器}public:UpdateCaching(int cap) : capacity(cap) {}int get(int key) {if (cache.count(key) == 0) {return -1;}// 将访问的键移到列表末尾,表示最近使用过moveToBack(key);return cache[key];}void put(int key, int value) {if (cache.count(key) > 0) {// 键已存在,更新值并将其移到列表末尾cache[key] = value;moveToBack(key);return;}if (cache.size() >= capacity) {// 缓存已满,移除最久未使用的元素int oldestKey = keyOrder.front();keyOrder.pop_front();cache.erase(oldestKey);keyIterators.erase(oldestKey);}// 插入新元素cache[key] = value;keyOrder.push_back(key);keyIterators[key] = --keyOrder.end();}
};int main() {UpdateCaching cache(3);cache.put(1, 10);cache.put(2, 20);cache.put(3, 30);std::cout << cache.get(2) << std::endl; // 输出 20cache.put(4, 40); // 缓存已满,删除最早的元素 (1, 10)std::cout << cache.get(1) << std::endl; // 输出 -1return 0;
}
优化说明
- 引入
keyIterators映射:用于存储每个键在keyOrder列表中的迭代器,这样在移动元素到列表末尾时,就可以直接通过迭代器删除元素,时间复杂度为 O ( 1 ) O(1) O(1)。 - 提取
moveToBack方法:将将键移到列表末尾的操作提取成一个单独的方法,提高了代码的复用性和可读性。
通过这些优化,get 和 put 方法的时间复杂度都可以优化到 O ( 1 ) O(1) O(1)。
优化2
1. 边界条件检查
在构造函数中可以添加对 capacity 参数的检查,避免传入不合法的值(如负数),增强代码的健壮性。
2. 异常处理
在实际应用中,std::unordered_map 和 std::list 的操作可能会抛出异常(如内存分配失败),可以考虑添加异常处理机制,让代码在异常情况下能有更好的表现。
3. 代码封装性
可以提供一些额外的接口方法,如获取缓存当前大小、检查缓存是否为空等,方便调用者使用。
4. 线程安全
如果该缓存类要在多线程环境下使用,需要添加同步机制,保证线程安全。
优化后的代码示例
#include <iostream>
#include <unordered_map>
#include <list>
#include <mutex>class UpdateCaching {
private:std::unordered_map<int, int> cache;std::unordered_map<int, std::list<int>::iterator> keyIterators;std::list<int> keyOrder;int capacity;mutable std::mutex mtx; // 用于线程安全的互斥锁// 将键移到列表末尾void moveToBack(int key) {std::lock_guard<std::mutex> lock(mtx); // 加锁,在get或put中被调用时已经加锁了,且是同一个锁 mtx,不会再次进行真正的加锁操作auto it = keyIterators[key];keyOrder.erase(it);keyOrder.push_back(key);keyIterators[key] = --keyOrder.end();}public:// 构造函数添加边界检查UpdateCaching(int cap) : capacity(cap) {if (cap <= 0) {throw std::invalid_argument("Capacity must be a positive integer.");}}int get(int key) {std::lock_guard<std::mutex> lock(mtx); // 加锁if (cache.count(key) == 0) {return -1;}// 将访问的键移到列表末尾,表示最近使用过moveToBack(key);return cache[key];}void put(int key, int value) {std::lock_guard<std::mutex> lock(mtx); // 加锁if (cache.count(key) > 0) {// 键已存在,更新值并将其移到列表末尾cache[key] = value;moveToBack(key);return;}if (cache.size() >= capacity) {// 缓存已满,移除最久未使用的元素int oldestKey = keyOrder.front();keyOrder.pop_front();cache.erase(oldestKey);keyIterators.erase(oldestKey);}// 插入新元素cache[key] = value;keyOrder.push_back(key);keyIterators[key] = --keyOrder.end();}// 获取缓存当前大小size_t size() const {std::lock_guard<std::mutex> lock(mtx); // 加锁return cache.size();}// 检查缓存是否为空bool empty() const {std::lock_guard<std::mutex> lock(mtx); // 加锁return cache.empty();}
};int main() {try {UpdateCaching cache(3);cache.put(1, 10);cache.put(2, 20);cache.put(3, 30);std::cout << cache.get(2) << std::endl; // 输出 20cache.put(4, 40); // 缓存已满,删除最早的元素 (1, 10)std::cout << cache.get(1) << std::endl; // 输出 -1std::cout << "Cache size: " << cache.size() << std::endl;std::cout << "Is cache empty? " << (cache.empty() ? "Yes" : "No") << std::endl;} catch (const std::exception& e) {std::cerr << "Exception: " << e.what() << std::endl;}return 0;
}
在多线程环境下使用:
// 线程函数,用于向缓存中插入数据
void insertData(UpdateCaching& cache, int start, int end) {for (int i = start; i < end; ++i) {cache.put(i, i * 10);}
}// 线程函数,用于从缓存中获取数据
void getData(UpdateCaching& cache, int start, int end) {for (int i = start; i < end; ++i) {int value = cache.get(i);if (value != -1) {std::cout << "Key: " << i << ", Value: " << value << std::endl;}}
}int main() {try {UpdateCaching cache(10);// 创建插入数据的线程std::thread insertThread1(insertData, std::ref(cache), 0, 5);std::thread insertThread2(insertData, std::ref(cache), 5, 10);// 创建获取数据的线程std::thread getThread1(getData, std::ref(cache), 0, 5);std::thread getThread2(getData, std::ref(cache), 5, 10);// 等待所有线程完成insertThread1.join();insertThread2.join();getThread1.join();getThread2.join();std::cout << "Cache size: " << cache.size() << std::endl;} catch (const std::exception& e) {std::cerr << "Exception: " << e.what() << std::endl;}return 0;
}
优化说明
- 边界条件检查:在构造函数中添加了对
capacity参数的检查,如果传入的值不合法,会抛出std::invalid_argument异常。 - 异常处理:通过
try-catch块捕获可能抛出的异常,并进行相应的处理,提高了代码的健壮性。 - 代码封装性:添加了
size()和empty()方法,方便调用者获取缓存的当前大小和检查缓存是否为空。 - 线程安全:使用
std::mutex实现了简单的线程安全机制,在关键操作(如get、put、size、empty等)前加锁,确保多线程环境下的操作安全。
DeepSeek(深度思考R1):
代码实现
#include <list>
#include <unordered_map>
#include <algorithm>using namespace std;class UpdateCaching {
private:int max_capacity;list<pair<int, int>> cache_list;unordered_map<int, list<pair<int, int>>::iterator> cache_map;public:UpdateCaching(int capacity) {max_capacity = max(0, capacity); // 处理非法容量值}int get(int key) {auto it = cache_map.find(key);if (it == cache_map.end()) {return -1;}// 将访问的节点移动到链表头部cache_list.splice(cache_list.begin(), cache_list, it->second);return it->second->second;}void put(int key, int value) {if (max_capacity <= 0) return; // 容量为0时不操作auto it = cache_map.find(key);if (it != cache_map.end()) {// 更新已有值并移到头部it->second->second = value;cache_list.splice(cache_list.begin(), cache_list, it->second);return;}// 插入新数据if (cache_map.size() >= max_capacity) {// 删除最久未使用的数据int last_key = cache_list.back().first;cache_map.erase(last_key);cache_list.pop_back();}// 插入新数据到头部cache_list.emplace_front(key, value);cache_map[key] = cache_list.begin();}
};
实现说明:
-
数据结构选择:
- 使用双向链表
list维护访问顺序,最近访问的节点保持在链表头部 - 使用哈希表
unordered_map实现O(1)时间复杂度的查找操作
- 使用双向链表
-
核心操作:
- get操作:
- 查找存在时,将节点移动到链表头部并返回值
- 不存在时返回-1
- put操作:
- 已存在时更新值并移动到头部
- 不存在时先检查容量,满时删除链表尾部元素
- 插入新元素到链表头部并更新哈希表
- get操作:
-
边界处理:
- 构造函数处理非法容量值(负数自动设为0)
- 容量为0时缓存不可用
- 正确处理链表空时的删除操作
-
时间复杂度:
- get和put操作的时间复杂度均为O(1)
- 链表操作使用splice实现节点移动,保证O(1)时间复杂度
在原有LRU缓存实现基础上,可以通过以下方案优化线程安全问题:
优化方案一:基础互斥锁版本
#include <list>
#include <unordered_map>
#include <mutex>class UpdateCaching {
private:int max_capacity;list<pair<int, int>> cache_list;unordered_map<int, list<pair<int, int>>::iterator> cache_map;mutable mutex cache_mutex; // 可变的互斥锁public:UpdateCaching(int capacity) : max_capacity(max(0, capacity)) {}int get(int key) {lock_guard<mutex> lock(cache_mutex); // 自动加锁if (auto it = cache_map.find(key); it != cache_map.end()) {cache_list.splice(cache_list.begin(), cache_list, it->second);return it->second->second;}return -1;}void put(int key, int value) {lock_guard<mutex> lock(cache_mutex);if (max_capacity <= 0) return;if (auto it = cache_map.find(key); it != cache_map.end()) {it->second->second = value;cache_list.splice(cache_list.begin(), cache_list, it->second);return;}if (cache_map.size() >= max_capacity) {cache_map.erase(cache_list.back().first);cache_list.pop_back();}cache_list.emplace_front(key, value);cache_map[key] = cache_list.begin();}
};
优化方案二:读写锁版本(C++17+)
#include <shared_mutex>class UpdateCaching {// ...其他成员同基础版...mutable shared_mutex rw_mutex; // 读写锁public:int get(int key) {shared_lock<shared_mutex> lock(rw_mutex); // 共享锁// ...原有逻辑...}void put(int key, int value) {unique_lock<shared_mutex> lock(rw_mutex); // 独占锁// ...原有逻辑...}
};
优化分析说明:
-
线程安全实现:
- 使用
std::mutex保护所有缓存操作 - 通过
lock_guard自动管理锁的生命周期 - 读写分离版本采用
shared_mutex提升读多写少场景性能
- 使用
-
性能影响评估:
方案 读并发 写并发 适用场景 普通互斥锁 串行 串行 低并发通用场景 读写锁 并行读 串行写 读多写少的高并发场景 -
异常安全:
- 所有容器操作保证基本异常安全
- 使用RAII风格的锁管理确保不会死锁
-
扩展优化建议:
// 示例:分段锁优化(适用于超高并发场景) class SegmentedLRUCache {constexpr static int SEGMENTS = 16;array<mutex, SEGMENTS> segment_mutexes;array<UpdateCaching, SEGMENTS> segments;size_t get_segment(int key) {return hash<int>{}(key) % SEGMENTS;} public:int get(int key) {auto seg = get_segment(key);lock_guard<mutex> lock(segment_mutexes[seg]);return segments[seg].get(key);}// 类似实现put方法... };
建议根据实际使用场景选择优化方案:
- 常规服务:基础互斥锁方案即可满足需求
- 高频读取服务:升级到读写锁版本
- 超大规模并发:考虑分段锁或无锁队列方案
需要特别注意:线程安全实现会带来约10%-30%的性能损耗,在单线程场景中可以通过编译选项禁用锁机制(通过条件编译或模板策略)。
相关文章:
实现一个 LRU 风格的缓存类
实现一个缓存类 需求描述豆包解决思路:实现代码:优化11. std::list::remove 的时间复杂度问题2. 代码复用优化后的代码优化说明 优化21. 边界条件检查2. 异常处理3. 代码封装性4. 线程安全优化后的代码示例优化说明 DeepSeek(深度思考R1&…...
DS图(中)(19)
文章目录 前言一、图的遍历广度优先遍历深度优先遍历 二、最小生成树Kruskal算法Prim算法两种方法对比 总结 前言 承上启下,我们来学习下图的中篇!!! 一、图的遍历 图的遍历指的是遍历图中的顶点,主要有 广度优先遍历 …...
YK人工智能(六)——万字长文学会基于Torch模型网络可视化
1. 可视化网络结构 随着深度神经网络做的的发展,网络的结构越来越复杂,我们也很难确定每一层的输入结构,输出结构以及参数等信息,这样导致我们很难在短时间内完成debug。因此掌握一个可以用来可视化网络结构的工具是十分有必要的…...
使用 Swift 完成FFmpeg音频录制、播放和视频格式转换应用
使用 Swift 构建音频录制、播放和视频格式转换应用 在这篇博客中,我们介绍如何用ffmpeg在swift上实现音频录制、音频播放、通过ffmpeg命令实现视频格式转换 音频录制:通过 AVAudioRecorder 实现音频录制功能。音频播放:通过 AVAudioPlayer …...
Gitea+Gridea 创建个人博客
历史文档存档,该方法目前已经无法使用,部署方法可供参考 Gitea部分 1.关于Gitea Gitea 是一个面向开源及私有软件项目的托管平台,是全球最大的代码托管平台之一。它采用 Git 分布式版本控制系统,为开发者提供了代码托管、版本控…...
【Linux】一文带你入门了解线程和虚拟地址空间中页表映射的秘密(内附手绘底层逻辑图 通俗易懂)
绪论 每日激励:“努力去做自己该做的,但是不要期待回报,不是付出了就会有回报的,做了就不要后悔,不做才后悔。—Jack” 绪论: 本章是LInux中非常重要的线程部分,通过了解线程的基本概念&am…...
js面试some和every的区别
1.基础使用 some和every 都是数组的一个方法let num [1,2,3,4,5,6] let flag1 num.some((item,index,array)> item > 2)let flag2 num.every((item,index, array)> item > 2)1.some 遍历判断中是符合条件的值 一旦找到则不会继续迭代下去 直接返回 2.every 遍历…...
缓存类为啥使用 unordered_map 而不是 map
性能考虑: std::unordered_map 是基于哈希表实现的,而 std::map 是基于红黑树实现的。对于查找操作,std::unordered_map 的平均查找时间复杂度是 O ( 1 ) O(1) O(1),而 std::map 的查找时间复杂度是 O ( l o g n ) O(log n) O(l…...
ollama linux下载
实验室服务器(A6000)执行curl -fsSL https://ollama.com/install.sh | sh太慢了。 而sudo snap install ollama,容易爆cudalibrt.so12无法正常使用的bug。 发现 https://www.modelscope.cn/models/modelscope/ollama-linux 使用modelscope进…...
k8s服务发现有哪些方式?
在 Kubernetes 中,服务发现是指如何让应用程序在集群内互相找到并通信。Kubernetes 提供了多种服务发现的方式,适应不同的使用场景。以下是 Kubernetes 中常见的服务发现方式: 1. 环境变量(Environment Variables) 概…...
Flash Attention与Attention
原始Attention是: Flash Attention: 伪代码:4d(分别代表Q\K\V\O) Flash Attention2优化了...
vue 使用fetch-event-source 处理sse,实现ChatGpt逐字输出效果
1. 安装 npm install microsoft/fetch-event-source 2. 引用 import { fetchEventSource } from "microsoft/fetch-event-source"; 3. 使用 fetchEventSource(/api/chat, { method: POST,headers: {Content-Type: application/json,Accept: */*,Token: this.toke…...
JAVA进阶之线程
为神马有线程?这玩意儿在干嘛??? 回答这个问题,就先要知道一点点计算机的工作方式。 总所周知,计算机有五部分:输入输出、计算器、存储器、控制器。而在计算机内,CPU、内存、I/O之…...
机器学习专业毕设选题推荐合集 人工智能
目录 前言 毕设选题 开题指导建议 更多精选选题 选题帮助 最后 前言 大家好,这里是海浪学长毕设专题! 大四是整个大学期间最忙碌的时光,一边要忙着准备考研、考公、考教资或者实习为毕业后面临的升学就业做准备,一边要为毕业设计耗费大量精力。学长给大家整理…...
C++ 中的 `string` 类型:全面解析与高效操作
C 中的 string 类型:全面解析与高效操作 在 C 中,string 类型是对字符数组的高级封装,它提供了大量内置函数,使得字符串的处理变得更为简便和高效。与 C 风格的字符数组不同,string 类型不仅自动管理内存,…...
go语言中的Stringer的使用
Go 语言中的 Stringer 是一个非常有用的接口,它在标准库的 fmt 包中定义。Stringer 接口允许类型定义它们的字符串表示方式,这在格式化输出时特别有用。让我们深入了解一下: Stringer 接口定义: type Stringer interface {Strin…...
Java入门进阶
文章目录 1、常用API 1.1、Math1.2、System1.3、Object1.4、Arrays1.5、基本类型包装类 1.5.1、基本类型包装类概述1.5.2、Integer1.5.3、int和String相互转换1.5.4、自动装箱和拆箱 1.6、日期类 1.6.1、Date类1.6.2、SimpleDateFormat类 1.6.2.1、格式化(从Date到…...
【大数据技术】搭建完全分布式高可用大数据集群(Scala+Spark)
搭建完全分布式高可用大数据集群(Scala+Spark) scala-2.13.16.tgzspark-3.5.4-bin-without-hadoop.tgz注:请在阅读本篇文章前,将以上资源下载下来。 写在前面 本文主要介绍搭建完全分布式高可用集群Spark的详细步骤。 注意: 统一约定将软件安装包存放于虚拟机的/softwa…...
使用vLLM部署Qwen2.5-VL-7B-Instruct模型的详细指南
使用vLLM部署Qwen2.5-VL-7B-Instruct模型的详细指南 引言环境搭建安装vLLM安装依赖库下载模型启动vLLM服务器总结参考 引言 近年来,随着大规模语言模型(LLM)的快速发展,如何高效地进行模型推理成为了一个热门话题。vLLM作为一个专…...
AWS门店人流量数据分析项目的设计与实现
这是一个AWS的数据分析项目,关于快消公司门店手机各个门店进店人流量和各个产品柜台前逗留时间(利用IoT设备采集)和销售数据之间的统计分析,必须用到但不限于Amazon Kensis Data Stream,Spark Streaming,Sp…...
C#结合html2canvas生成切割图片并导出到PDF
目录 需求 开发运行环境 实现 生成HTML范例片断 HTML元素转BASE64 BASE64转图片 切割长图片 生成PDF文件 小结 需求 html2canvas 是一个 JavaScript 库,它可以把任意一个网页中的元素(包括整个网页)绘制到指定的 canvas 中…...
485网关数据收发测试
目录 1.UDP SERVER数据收发测试 使用产品: || ZQWL-GW1600NM 产品||【智嵌物联】智能网关型串口服务器 1.UDP SERVER数据收发测试 A(TX)连接RX B(RX)连接TX 打开1个网络调试助手,模拟用户的UDP客户端设…...
InnoDB和MyISAM的比较、水平切分和垂直切分、主从复制中涉及的三个线程、主从同步的延迟产生和解决
InnoDB和MyISAM的比较 事务支持: InnoDB支持:支持事务 (ACID 属性)。支持 Commit、Rollback 和 Savepoint 操作。适合需要事务处理的应用,例如银行系统。MyISAM:不支持事务。每次操作都是自动提交,不能回滚或中止。适合对事务要求…...
JDK9新特性
文章目录 新特性:1.模块化系统使用模块化module-info.java:exports:opens:requires:provides:uses: 2.JShell启动Jshell执行计算定义变量定义方法定义类帮助命令查看定义的变量:/var…...
基于Ubuntu2404搭建Zabbix7.2
Zabbix 搭建zabbix zabbix7.2已推出:官网 增加的新功能如下: 1.使用新的热门商品小部件全面概览指标 数据概览小部件已转换为热门项目小部件使用项目模式可以实现细粒度的项目选择利用条形图、指标和迷你图来可视化您的数据定义价值阈值以动态地可视化…...
Math Reference Notes: 符号函数
1. 符号函数的定义 符号函数(Sign Function) sgn ( x ) \text{sgn}(x) sgn(x) 是一个将实数 ( x ) 映射为其 符号值(即正数、负数或零)的函数。 它的定义如下: sgn ( x ) { 1 如果 x > 0 0 如果 x 0 − 1 如…...
【数据结构】链表应用-链表重新排序
重新排序 反转链表预期实现思路解题过程code力扣代码核心代码完整代码 总结 删除链表中间节点代码解惑 链表重新排序题目描述解题思路解题过程复杂度代码力扣代码完整代码 反转链表 预期实现 思路 你选用何种方法解题? 我选用了迭代法来反转链表。这是一种经典且高…...
学习threejs,pvr格式图片文件贴图
👨⚕️ 主页: gis分享者 👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨⚕️ 收录于专栏:threejs gis工程师 文章目录 一、🍀前言1.1 ☘️PVR贴图1.2 ☘️THREE.Mesh…...
数据库开发常识(10.6)——SQL性能判断标准及索引误区(1)
10.6. 数据库开发常识 作为一名专业数据库开发人员,不但需要掌握数据库开发相关的语法和功能实现,还要掌握专业数据库开发的常识。这样,才能在保量完成工作任务的同时,也保质的完成工作任务,避免了为应用的日后维护埋…...
2022年全国职业院校技能大赛网络系统管理赛项模块A:网络构建(样题2)-网络部分解析-附详细代码
目录 附录1:拓扑图编辑 附录2:地址规划表 1.SW1 2.SW2 3.SW3 4.SW4 5.SW5 6.SW6 7.SW7 8.R1 9.R2 10.R3 11.AC1 12.AC2 13.EG1 14.EG2 15.AP2 16.AP3 附录1:拓扑图 附录2:地址规划表...
