C++ STL multimap:一对多关联容器的原理、操作与实战应用
1. 从“键值对”到“一对多”为什么需要multimap在C的STL标准模板库里map和multimap这对兄弟常常让初学者感到困惑。我们都很熟悉std::map它就像一个高效的字典每个“键”key都独一无二指向一个特定的“值”value。你输入一个名字就能立刻找到对应的电话号码这种一对一的映射关系清晰、直接是处理大量关联数据的利器。但现实世界的数据关系远比一对一复杂。想象一下你要管理一个部门的员工信息以“部门名称”作为键员工对象作为值。在map的世界里一个部门只能对应一个员工这显然不合理。你需要的是一个能容纳多个值的“键”。再比如处理日志文件以“时间戳精确到秒”为键记录这一秒内发生的所有事件消息或者构建一个单词的倒排索引每个单词键对应它在文档中出现过的所有位置值列表。在这些场景下键与值是一对多的关系。这就是std::multimap存在的意义。它是关联容器的一种允许容器中存在多个具有相同键的键值对。你可以把它理解为一个自动排序的“多值字典”。它底层通常基于红黑树实现这意味着它的元素总是按键的升序排列默认情况下并且插入、删除、查找操作都能保持对数时间复杂度O(log n)在需要有序遍历或范围查询的场景下非常高效。很多从map转过来的朋友第一次用multimap时最容易踩的坑就是试图用operator[]去访问元素。在map里myMap[“key”]能轻松拿到或插入值但在multimap里这个操作符直接被禁用了。为什么因为对于一个键可能对应多个值的情况operator[]的语义变得模糊——它到底该返回哪一个值呢这个设计决策迫使我们必须换一种更精确的方式来与multimap交互这也是理解其用法的第一个关键点。2. multimap的核心操作插入、查找与遍历与map相比multimap的接口设计强调了“多值”的特性。我们不再能简单地通过键来获取单一值而是需要处理一个值的集合。2.1 元素的插入插入操作和map类似使用insert成员函数。但由于允许多个相同键插入总是会成功除非发生异常。#include iostream #include map #include string int main() { std::multimapstd::string, int scoreMap; // 插入多个相同键的值 scoreMap.insert({Alice, 85}); scoreMap.insert({Bob, 90}); scoreMap.insert({Alice, 92}); // 第二个Alice scoreMap.insert({Alice, 88}); // 第三个Alice // 也可以使用 make_pair (C11 前) 或 emplace (C11 后) scoreMap.emplace(Bob, 95); }这里“Alice”这个键关联了三个分数859288。需要注意的是multimap会保持所有元素的排序状态。对于相同键的多个值它们会按照插入的顺序依次排列吗实际上C标准只保证相同键的元素会相邻存储但并没有规定它们之间的相对顺序。不过在常见的实现中如GCC、Clang的libstdc MSVC的STL这些相同键的值通常会按照插入的先后顺序排列但这不是可移植的保证。如果你的业务逻辑依赖相同键值对的顺序更安全的做法是使用std::mapstd::string, std::vectorint将顺序的控制权完全掌握在自己手里。2.2 元素的查找与访问这是multimap与map用法差异最大的地方。由于一个键对应多个值我们通常使用一对迭代器来获取这个范围。find与count的组合拳find(key)会返回指向第一个匹配键的元素的迭代器。count(key)则返回该键对应的元素数量。// 查找键为Alice的所有记录 std::string keyToFind Alice; int num scoreMap.count(keyToFind); // 返回 3 if (num 0) { auto it scoreMap.find(keyToFind); // 找到第一个Alice for (int i 0; i num; i, it) { std::cout it-first : it-second std::endl; } }这种方法简单但需要注意在循环中我们手动递增迭代器num次这依赖于一个假设——所有相同键的元素都紧挨着。在multimap中这个假设是成立的。更优雅的方式equal_rangeequal_range(key)是处理multimap查找的“标准答案”。它返回一个pair其中first是指向第一个键等于key的元素的迭代器second是指向最后一个键等于key的元素之后位置的迭代器。这个区间就是所有该键对应的值用起来非常符合STL的区间习惯。auto range scoreMap.equal_range(Alice); for (auto it range.first; it ! range.second; it) { std::cout it-first - it-second std::endl; } // 输出 // Alice - 85 // Alice - 92 // Alice - 88使用equal_range的代码更清晰且不依赖于count直接利用迭代器区间是推荐的做法。lower_bound与upper_bound这两个函数也常用于查找范围。lower_bound(key)返回指向第一个不小于key的元素的迭代器即第一个键等于key或大于key的位置。upper_bound(key)返回指向第一个大于key的元素的迭代器。因此[lower_bound(key), upper_bound(key))这个左闭右开区间同样包含了所有键等于key的元素。auto low scoreMap.lower_bound(Alice); auto up scoreMap.upper_bound(Alice); for (auto it low; it ! up; it) { // 处理每一个Alice }在multimap中equal_range(key)的实现通常就等价于std::make_pair(lower_bound(key), upper_bound(key))。你可以根据代码语境选择更贴切的一个。2.3 元素的遍历与删除遍历整个multimap和遍历其他容器一样使用迭代器即可。因为元素是排序的所以遍历顺序就是键的升序。for (const auto kv : scoreMap) { std::cout kv.first : kv.second std::endl; } // 输出可能是 // Alice: 85 // Alice: 92 // Alice: 88 // Bob: 90 // Bob: 95删除操作有几种形式erase(iterator pos)删除迭代器pos指向的元素。erase(key_type key)删除所有键等于key的元素并返回被删除的元素数量。这是与map的erase(key)行为不同的地方map只删除一个如果存在而multimap会删除全部。erase(iterator first, iterator last)删除迭代器区间[first, last)内的所有元素。如果你想删除特定键的某一个特定值比如只删除Alice的92分你需要先定位到那个具体的元素迭代器然后删除。auto range scoreMap.equal_range(Alice); for (auto it range.first; it ! range.second; ) { if (it-second 92) { it scoreMap.erase(it); // erase 返回被删除元素之后位置的迭代器 } else { it; } }这里有一个关键细节在遍历中删除元素时erase(it)会使it失效。但幸运的是erase成员函数会返回下一个有效迭代器所以我们采用it container.erase(it)的模式来安全地继续循环。3. 底层实现与性能考量红黑树的利与弊std::multimap的默认实现底层是一棵红黑树一种自平衡的二叉查找树。这决定了它的特性有序性元素始终按照键的顺序默认std::less即升序排列。这使得范围查询如lower_bound/upper_bound和有序遍历非常高效。稳定的对数时间复杂度插入(insert)、删除(erase)、查找(find)操作的平均和最坏情况时间复杂度都是O(log n)其中n是容器中元素的数量。性能可预测不会像哈希表那样在极端情况下退化。内存开销每个元素都是一个独立的节点除了存储键值对还需要存储额外的指针指向父节点、左右子节点和颜色信息内存开销比std::vector或std::unordered_multimap要大。与std::map的唯一区别就在于multimap的树节点允许键重复。在插入一个新节点时如果键已存在树会将其插入到相同键的节点序列中的适当位置通常遵循特定的内部规则如维护插入顺序。与unordered_multimap的对比std::unordered_multimap是基于哈希表实现的关联容器。它的核心优势是平均情况下的O(1)时间复杂度访问但最坏情况可能退化到O(n)。它不保证元素的任何顺序。 如何选择选用multimap当你需要元素按键排序或者需要频繁进行范围查询“找出所有键在A和B之间的元素”。例如处理时间序列数据你需要按时间顺序处理事件。选用unordered_multimap当顺序无关紧要且你追求极致的平均访问速度同时哈希函数对你的键类型效果很好时。例如实现一个快速的单词计数器键是单词值是计数虽然这种情况用unordered_mapstring, int更常见。一个性能陷阱大量重复键下的查找假设一个multimap中有100万个元素其中键“X”对应了50万个值。当你调用equal_range(“X”)时虽然查找第一个“X”是O(log n)但遍历这50万个值的过程是O(k)k是重复数量。如果这是你的核心操作且k很大线性遍历可能会成为瓶颈。此时或许std::mapKey, std::vectorValue是更好的选择因为你可以直接通过map[key]拿到整个vector访问是O(1)并且你对vector内的顺序有完全的控制权。代价是失去了整个结构的全局有序性。4. 实战场景与设计模式何时该用multimap理解了基本操作和原理我们来看看multimap在哪些实际场景中能真正发光发热。场景一事件调度器Event Scheduler假设你在写一个游戏或模拟程序需要处理在特定游戏时间点触发的多个事件。std::multimapdouble, std::functionvoid() eventQueue; // 键: 触发时间, 值: 事件函数 // 订阅事件 eventQueue.emplace(10.5, [](){ std::cout Spawn enemy at 10.5s\n; }); eventQueue.emplace(5.0, [](){ std::cout Play sound at 5.0s\n; }); eventQueue.emplace(10.5, [](){ std::cout Update score at 10.5s\n; }); // 主循环中处理事件 double currentTime 0.0; while (!eventQueue.empty() eventQueue.begin()-first currentTime) { auto range eventQueue.equal_range(eventQueue.begin()-first); for (auto it range.first; it ! range.second; ) { it-second(); // 执行事件 it eventQueue.erase(it); // 执行后移除 } // 更新currentTime... }这里multimap保证了事件按时间顺序处理并且能优雅地处理同一时刻的多个事件。场景二多值配置读取从配置文件中读取设置一个配置项可能对应多个值例如服务器监听的多个端口用户拥有的多个角色。// 假设从文件解析后得到 std::multimapstd::string, std::string config; config.emplace(server.port, 8080); config.emplace(server.port, 8081); config.emplace(user.role, admin); config.emplace(user.role, editor); // 读取所有端口 auto ports config.equal_range(server.port); std::vectorint portList; for (auto it ports.first; it ! ports.second; it) { portList.push_back(std::stoi(it-second)); }场景三构建反向索引Inverted Index这是搜索引擎的核心数据结构之一。对于一组文档我们建立“单词”到“出现该单词的文档ID列表”的映射。std::multimapstd::string, int invertedIndex; // 键: 单词, 值: 文档ID // 假设文档1包含单词 apple, banana invertedIndex.emplace(apple, 1); invertedIndex.emplace(banana, 1); // 文档2包含单词 apple invertedIndex.emplace(apple, 2); // 搜索包含apple的文档 auto docs invertedIndex.equal_range(apple); std::setint uniqueDocIds; // 用set去重因为同一个单词在同一文档可能出现多次 for (auto it docs.first; it ! docs.second; it) { uniqueDocIds.insert(it-second); } // uniqueDocIds 包含 {1, 2}在这个场景中multimap自然地存储了单词和文档ID的对应关系。但需要注意的是对于大规模索引unordered_multimap或mapstring, vectorint可能因为更好的局部性而具有更高的效率。设计模式思考multimapvsmap of container这是使用multimap时最常遇到的设计抉择。是选择std::multimapKey, Value还是std::mapKey, std::vectorValuestd::multimapKey, Value优点结构简单STL直接支持范围查询和有序遍历内存分配是分散的每个节点独立插入删除单个元素可能更高效尤其是中间插入。缺点相同键的值遍历是线性时间且值存储分散缓存不友好。无法直接获取某个键的所有值的集合需要遍历。std::mapKey, std::vectorValue优点可以直接通过map[key]访问整个值列表O(log n)查找O(1)访问数据局部性好对值的批量操作方便例如对整个vector排序。对“获取某个键的所有值”这个操作更直观高效。缺点结构稍复杂。插入第一个值时需要构造vector。如果频繁在vector中间插入删除成本可能较高。选择的关键在于你的主要访问模式。如果你最常做的操作是“针对某个键处理它的所有值”那么map of vector通常更优。如果你的操作是“在整个集合中按序遍历或进行键的范围查询”并且需要频繁插入删除单个键值对那么multimap可能更合适。5. 进阶技巧与常见“坑点”掌握了基础我们来看看一些能提升代码质量和效率的进阶用法以及那些容易让人栽跟头的地方。自定义比较函数和map一样multimap的模板第三个参数是比较器Comparator。默认是std::lessKey即升序。你可以自定义排序规则。struct CaseInsensitiveCompare { bool operator()(const std::string a, const std::string b) const { return std::lexicographical_compare( a.begin(), a.end(), b.begin(), b.end(), [](char ca, char cb) { return std::tolower(ca) std::tolower(cb); } ); } }; std::multimapstd::string, int, CaseInsensitiveCompare myMap; myMap.emplace(Apple, 1); myMap.emplace(banana, 2); myMap.emplace(apple, 3); // 此时Apple和apple被视为相同的键注意自定义比较函数必须实现严格弱序否则会导致未定义行为。“坑点”一误用find进行存在性判断在map中我们常用if (myMap.find(key) ! myMap.end())来判断键是否存在。在multimap中这仍然有效但它只告诉你“至少存在一个”该键的元素。如果你需要知道“是否存在且唯一”则需要结合count。std::multimapint, int mm; mm.emplace(1, 100); mm.emplace(1, 200); if (mm.find(1) ! mm.end()) { // 会进入这里因为键1存在 } if (mm.count(1) 1) { // 不会进入这里因为键1有2个值 }“坑点”二迭代器失效的微妙之处multimap的迭代器在插入操作后通常不会失效除非重平衡导致节点移动但标准规定插入不会使任何迭代器失效。但是删除操作会使指向被删除元素的迭代器失效。正如之前删除示例中提到的必须使用erase的返回值来更新迭代器。// 错误示例遍历并删除满足条件的元素 for (auto it mm.begin(); it ! mm.end(); it) { if (someCondition(*it)) { mm.erase(it); // it 在此处失效 // 下一轮循环 it 行为未定义 } } // 正确示例 (C11前) for (auto it mm.begin(); it ! mm.end(); /* 不在这里递增 */) { if (someCondition(*it)) { it mm.erase(it); // erase 返回下一个有效迭代器 } else { it; } } // 正确示例 (C20 起更简洁) std::erase_if(mm, someCondition);“坑点”三性能误区——线性查找新手有时会写出这样的代码std::multimapstd::string, Data bigMap; // ... 填充大量数据 ... // 低效做法在循环内多次调用 equal_range 或 find for (const auto targetKey : keyList) { auto range bigMap.equal_range(targetKey); for (auto it range.first; it ! range.second; it) { process(*it); } } // 如果keyList很大且bigMap也很大这相当于多次O(log n)查找。 // 如果keyList本身无序且需要处理bigMap中大部分数据不如直接遍历bigMap一次。 for (const auto kv : bigMap) { if (shouldProcess(kv.first)) { // shouldProcess判断是否在keyList中 process(kv); } } // 或者如果keyList有序可以考虑使用双指针或同时遍历的方式优化。关键在于理解你的数据规模和访问模式选择最合适的算法。与std::pair和结构化绑定的配合multimap的元素类型是std::pairconst Key, Value。C17的结构化绑定让遍历代码更清晰。std::multimapint, std::string mm; // ... 插入数据 ... for (const auto [key, value] : mm) { // 结构化绑定 std::cout key : value std::endl; } // 在 equal_range 循环中同样适用 auto [begin, end] mm.equal_range(42); // C17 结构化绑定 for (auto it begin; it ! end; it) { const auto [k, v] *it; // 对迭代器解引用也可以绑定 // 使用 k 和 v }std::multimap是一个强大的工具但它不是万能的。它的价值在于对有序的、一对多关系的优雅表达。下次当你面临需要将多个值关联到一个键并且希望它们保持有序时不妨考虑一下它。但在按下CtrlI自动补全选择它之前先花几秒钟想想我真的需要全局有序吗我的主要操作是范围查询还是按键取值数据量有多大重复键多吗想清楚这些问题你就能在multimap、unordered_multimap和map of container之间做出最明智的选择。

相关新闻

信号简介...

信号简介...

#./XXX 前台进程 #./YYY& 后台进程 前台进程能从键盘获取标准输入,后台进程不能 但二者都可以向标准输出上打印jobs查看所有的后台任务fg任务号将特定的进程提到前台ctrlz将进程切换到后台bg任务号让后台进程恢复运行信号 1 kill 2 raisetask_struct内部维…

2026/10/14 22:51:52 阅读更多 →
PostgreSQL跨Schema/Tablespace数据迁移实战指南

PostgreSQL跨Schema/Tablespace数据迁移实战指南

1. PostgreSQL逻辑备份迁移实战:跨Schema/Tablespace的数据导入技巧 作为PostgreSQL数据库管理员,数据迁移是日常工作中最常见的任务之一。最近在将测试环境数据迁移到生产环境时,遇到了需要将数据导入到不同schema和tablespace的需求。经过多…

2026/10/11 6:13:37 阅读更多 →
【爱马仕】Hermes Agent 桌面端落地方案,Windows 可视化部署完整流程

【爱马仕】Hermes Agent 桌面端落地方案,Windows 可视化部署完整流程

Windows 本地部署 Hermes 流程简化,整合包五分钟快速搭建 不少使用者想要体验 Hermes Agent,但在原生部署阶段很容易卡在环境配置环节。 手动安装各类运行依赖、调试系统运行路径、处理命令行报错、应对系统安全拦截、补全缺失文件等一系列操作&#xf…

2026/10/6 7:29:58 阅读更多 →

最新新闻

网站安全狗IIS枸杞2版v3.5.11497实战:IIS服务器防护配置与避坑指南

网站安全狗IIS枸杞2版v3.5.11497实战:IIS服务器防护配置与避坑指南

简介:网站安全狗IIS枸杞2版 v3.5.11497 是一款面向 Windows IIS 服务器运维人员与网站管理员的服务器安全防护工具,主要解决网站内容安全、资源保护与流量防护等问题。其功能覆盖网马与木马扫描、防 SQL 注入、防盗链、防黑链、防 CC 攻击、下载线程保护…

2026/10/14 22:51:28 阅读更多 →
AdderNet:用加法替代乘法,端侧AI的极简计算思路

AdderNet:用加法替代乘法,端侧AI的极简计算思路

端侧模型部署的工程师大概都有同感:模型体积可以通过剪枝量化压下来,但真正跑起来以后功耗和散热还是经常兜不住。深度学习里的卷积、全连接、注意力机制,底层都是海量的乘法和累加,而乘法器在硬件世界里是个“吃大户”的角色——…

2026/10/14 22:51:28 阅读更多 →
Unity网格绘制贝塞尔曲线:从数学原理到动态轨迹完整实现

Unity网格绘制贝塞尔曲线:从数学原理到动态轨迹完整实现

写这篇的起因其实很简单:一个项目里需要根据玩家操作实时画出一条技能轨迹线,要求线条能跟着角色移动变形,粗细可控,还不能穿模。当时我下意识先想到的是LineRenderer,结果一算顶点数量、一测贴图拉伸、一看需要动态变…

2026/10/14 22:51:28 阅读更多 →
大厂Java面试:从HashMap到微服务的底层逻辑与追问链

大厂Java面试:从HashMap到微服务的底层逻辑与追问链

前段时间有位候选人跟我复盘了一场面试,他说面试官让他讲 HashMap,他从 put 流程讲到红黑树,自以为答得已经很完整了,结果对方只追问了一句“为什么树化阈值是 8”,他就当场卡住。这其实是很多准备大厂 Java 面试的人共…

2026/10/14 22:51:28 阅读更多 →
一句话叫齐所有人:语音对讲/集群通信系统EasyDSS集群对讲让调度听得见、找得到

一句话叫齐所有人:语音对讲/集群通信系统EasyDSS集群对讲让调度听得见、找得到

应急处置最怕“叫不应、找不到”。电话逐个打太慢,群里所有人又容易漏。集群通信系统EasyDSS集群对讲把语音呼叫、频道组织与GIS地图揉进一个工作台,让调度指令既“喊得响”,也“看得清”,从前线到指挥中心在同一张图上协同&#…

2026/10/14 22:51:28 阅读更多 →
NI USB-6008/6501 C#采集实战:驱动适配、时序控制与边沿防丢

NI USB-6008/6501 C#采集实战:驱动适配、时序控制与边沿防丢

简介:本资源是一套面向C#初学者与工业数据采集开发者的NI USB系列采集卡通讯实战Demo,聚焦USB-6501与USB-6008两款经济型总线供电板卡的快速接入与基础控制。通过调用NI官方动态链接库(DLL),完整实现数字I/O读写、计数…

2026/10/14 22:50:27 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →