news 2026/8/18 20:50:48

set、map、unordered_map、unordered_set在应用上详细对比与底层实现逻辑分析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
set、map、unordered_map、unordered_set在应用上详细对比与底层实现逻辑分析

1.set是什么

set的底层是红黑树(一种平衡搜索二叉树),树形结构,而且只有key没有value,也就是说set只能做key在不在这种场景,比如说在机场对乘客是不是黑名单中的一员进行查找的时候就可以用到set,进行查找对应的身份证号是否存在,不需要知道这个身份证号对应的姓名是什么,也就是说不需要value,这也是set和map的区别;

2.set和红黑树的关系

set是容器,而红黑树是set的底层逻辑,是一种数据结构,set对红黑树的底层逻辑做了封装,提供了我们使用的上层接口

3.set的特点

① 红黑树天生自动维护顺序,所以 set 里的元素永远是排好序的。

② set可以进行去重,也就是说set中没有重复的元素,这是因为set这个容器在进行插入数据的时候会进行判断,将重复的元素进行去除,std::set的设计目标是集合set,数学上集合的定义就是"不重复元素的无序聚集"。这也是set和list的区别之一

4.unordered_set是什么

unordered的意思是无序的,所以unordered_set的意思就是无序集合,底层使用的是哈希表,为什么unordered底层使用的是hash表而不是红黑树呢?因为业务需求不同:set需要"有序",红黑树天然有序;unordered_set只需要"快速",哈希表天然最快。这是根据"要什么"选"用什么"的经典案例。

5.hash是怎么将元素放到对应的桶中的

步骤操作具体做了什么?
第 1 步计算哈希值调用std::hash<Key>()(x),得到一个size_t类型的整数(比如123456789
第 2 步计算桶号(索引)用哈希值对桶数量取模:bucket_index = hash_value % bucket_count(),得到要去的桶的编号(比如5号桶)
第 3 步查重(遍历桶内链表)去对应的桶,遍历桶里的链表,用operator==逐一比较所有元素。如果有相等的,插入失败,直接返回
第 4 步检查是否需要扩容如果没找到重复,检查当前元素个数是否超过bucket_count * max_load_factor(默认 1.0)。如果超过了,触发 Rehash(扩容),重新分配更大的桶数组,所有旧元素重新计算桶号并搬过去
第 5 步插入元素把新元素挂在对应桶的链表头部(或尾部),元素个数size++
void unordered_set::insert(int x) { // 第 1 步:算哈希 size_t hash_val = hash_function(x); // 第 2 步:算桶号 size_t bucket_index = hash_val % bucket_count; // 第 3 步:查重 Node* cur = buckets[bucket_index]; while (cur != nullptr) { if (cur->value == x) { // 用 operator== 比较 return; // 重复了,拒绝插入 } cur = cur->next; } // 第 4 步:检查负载因子 if (size > bucket_count * max_load_factor) { rehash(bucket_count * 2); // 扩容翻倍,全部重新哈希 bucket_index = hash_val % bucket_count; // 重新计算桶号 } // 第 5 步:插入 Node* new_node = new Node(x); new_node->next = buckets[bucket_index]; buckets[bucket_index] = new_node; size++; }

① rehash的什么时候进行扩容?

rehash的时间复杂度是O(n),而因为负载因子是1,意思就是当元素个数和桶的数量相同的时候需要进行扩容;

② 为什么要重新计算hash值?

因为并不是所有数据都是整数,能让你直接取模!哈希值存在的核心目的,就是把"乱七八糟的任何东西"统一转化成"整数",这样计算机才能处理。

场景 1:如果原始值是字符串

std::unordered_set<std::string> mySet; mySet.insert("hello"); mySet.insert("world");

问题来了:你没法对字符串取模!

"hello" % 13; // ❌ 编译报错!C++ 不支持对字符串取模

你必须先把字符串转成一个整数,然后才能取模。这个"转成整数的过程",就是哈希函数做的事。

场景 2:如果原始值是自定义对象

struct Person { std::string name; int age; }; std::unordered_set<Person> mySet; // 编译报错!没有哈希函数

问题:Person不是整数,不能直接取模。你必须告诉 C++ 怎么把Person转成整数,这就是自定义哈希函数:

struct PersonHash { size_t operator()(const Person& p) const { return std::hash<string>()(p.name) ^ std::hash<int>()(p.age); } };

6.set和unordered_set的区别是什么

值得注意的是set和unordered_set都是去重的;

对比维度std::setstd::unordered_set
底层数据结构红黑树(自平衡二叉搜索树)哈希表(数组 + 链表)
元素顺序自动升序排序完全无序(不保证任何顺序)
查找时间复杂度O(log n)O(1) 平均,最坏 O(n)
插入时间复杂度O(log n)O(1) 平均,最坏 O(n)
删除时间复杂度O(log n)O(1) 平均,最坏 O(n)
迭代器稳定性✅ 插入/删除时已有迭代器不失效❌ 扩容(Rehash)时所有迭代器失效
是否支持范围查询✅ 支持(lower_bound/upper_bound❌ 不支持
内存开销每个节点额外存 3 个指针(父、左、右)+ 颜色位(约 32 字节)桶数组预分配(空桶也占内存)+ 链表节点指针
自定义类型要求必须重载operator<必须特化std::hash+ 重载operator==
适用场景需要排序、范围查询、迭代器长期持有只关心快速增删查改,不关心顺序

实际开发使用频率

较少(特定场景)极高(绝大多数场景首选)

为什么set的查找、插入、删除的时间复杂度是Ologn,而unordered_set的时间复杂度是O(1)?

这是因为set是需要排序的,而unordered_set是不需要排序,所以查找效率最高;

为什么unordered_set的时间复杂度最坏情况是O(N)?

因为unordered_set是hash,而哈希函数可能把所有元素都映射到同一个桶(Bucket)里,导致查找时退化成遍历链表,复杂度从 O(1) 变成 O(n)。

7.什么是map

map 是一种容器,存的是"键值对(Key-Value)",通过 Key 快速查找对应的 Value。就像"通讯录":输入名字(Key),查到手机号(Value)。set的名字叫做集合,map叫做映射。map和set一样,map的底层使用也是红黑树,也是有序的,map和set的区别是map从set的单个元素变成了键值对,map是按照key排序。

8.map在面试中常有的面试问题

①map和set的区别是什么

set只存key,只关心元素在不在,而map存了key和value的键值对,可以通过key查找value;

②map和unordered_map的区别

对比维度std::mapstd::unordered_map
底层红黑树哈希表
顺序按 Key 自动排序完全无序
查找O(log n)O(1) 平均,最坏 O(n)
迭代器稳定性插入不失效扩容时失效
适用场景需要排序、范围查询只关心快速查找

③map 的 [] 操作符和 insert 有什么区别?

操作行为返回值
map[key] = valueKey 不存在则插入;Key 存在则覆盖Value返回 Value 的引用
map.insert({key, value})Key 不存在则插入;Key 存在则什么都不做返回pair<iterator, bool>bool表示是否插入成功

[]操作符有个陷阱:即使你只是读取map["不存在"],它也会自动插入一个默认构造的 Value这可能不是你想要的。所以查找时建议用find()而不是[];

// 错误方式 if (map["key"] == value) { ... } // 如果 "key" 不存在,会插入一个空值! // 正确方式 auto it = map.find("key"); if (it != map.end() && it->second == value) { ... }

④map 的迭代器会失效吗?什么时候失效?

容器插入操作删除操作
std::map不失效(红黑树链式结构)只失效被删除的那个迭代器
std::unordered_map可能失效(触发 Rehash 时全部失效)只失效被删除的那个迭代器

正确的处理方法

// 正确:先获取下一个迭代器,再删除 for (auto it = myMap.begin(); it != myMap.end(); ) { if (it->second < 0) { it = myMap.erase(it); // C++11 后 erase 返回下一个迭代器 } else { ++it; } }

⑤map 的 Value 可以是任意类型吗

可以!Value 可以是任意类型:intstringvector、甚至另一个map或自定义类。

因为 Map 的排序和查找只依赖Key,Value 只是跟着 Key 走的附加数据,不参与任何比较操作,所以没有任何限制

⑥map 和 unordered_map 怎么选

场景推荐原因
需要 Key 有序(如排行榜、范围查询)std::map红黑树天然有序
数据量小(< 100 个)都可以,差别不大-
数据量大且只做查找std::unordered_map快得多
需要迭代器长期持有std::map插入不失效
担心哈希攻击std::map红黑树复杂度稳定 O(log n)

⑦为什么std::map的 Key 不能修改?

因为修改 Key 会破坏红黑树的有序结构。如果允许修改,树可能不再满足"左 < 根 < 右"的规则,导致查找和遍历结果错误。

正确做法是:先删除,修改完,再插入。

// 错误(禁止) auto it = myMap.find("张三"); it->first = "张四"; // ❌ 编译报错!Key 是 const // 正确 auto it = myMap.find("张三"); int age = it->second; myMap.erase(it); myMap["张四"] = age; // 重新插入

⑧multimap 和 map 的区别?

multimap允许重复 Key,即一个 Key 可以对应多个 Value。使用时注意:multimap不支持[]操作符,因为不知道你要访问的是哪一个 Value。

std::multimap<std::string, int> scores; scores.insert({"张三", 90}); scores.insert({"张三", 95}); // 允许!现在 "张三" 有两个成绩 // 遍历所有 "张三" 的成绩 auto range = scores.equal_range("张三"); for (auto it = range.first; it != range.second; ++it) { cout << it->second << " "; // 输出:90 95 }

9.map和set的常用接口

操作Set(std::set<int>Map(std::map<string, int>
插入insert(x)insert({key, value})map[key] = value
查找find(x)find(key)
删除erase(x)erase(it)erase(key)erase(it)
获取大小size()size()
判空empty()empty()
清空clear()clear()
遍历for (auto& x : s)for (auto& p : m)p.first是 Key,p.second是 Value)
检查存在count(x)(返回 0 或 1)count(key)(返回 0 或 1)
获取元素❌ 没有map[key]map.at(key)
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/18 20:47:02

【YOLO26创新改进】SCI 2024Top | Neck特征融合创新篇 | 使用HS-FPN高阶筛选特征融合金字塔,适合小目标检、医学图像目标检测、图像分割任务,即插即用涨点改进

一、本文介绍 ⭐本文 使用HS-FPN高阶筛选特征融合金字塔 改进YOLO26网络模型,利用高层特征中丰富的语义信息对低层特征进行选择性筛选,再将保留下来的有效低层细节与高层语义信息进行融合,从而避免传统特征金字塔直接叠加不同层特征时带来的冗余信息和背景干扰。 这种改进能…

作者头像 李华
网站建设 2026/8/18 20:44:42

标签制作软件导入Excel批量打印数据源

LabelNova 今天要分享一款标签打印工具——LabelNova。这款软件的开发者有段挺有意思的故事&#xff1a;十年前他在仓库做管理员&#xff0c;每天都要打印各种标签&#xff0c;但当时的打印软件特别难用&#xff0c;每次都要重新编辑&#xff0c;繁琐得不行。为了解决这个问题…

作者头像 李华
网站建设 2026/8/18 20:43:13

江南程序设计竞赛联盟暑期多校训练·第六场(个人补题A,E)

题目A. Bob 的私房钱知识点&#xff1a;分解质因数思路&#xff1a;又是分解质因数(●—●)&#xff0c;最后公式确实退不出来&#xff0c;还是直接附上题解吧代码&#xff1a;#include <bits/stdc.h> using namespace std; #define int long long #define endl \n int t…

作者头像 李华
网站建设 2026/8/18 20:42:07

Agent Memory:如何将上下文怎么变成团队知识资产?

导读&#xff1a;AI失忆不是模型笨&#xff0c;而是无状态推理的架构特性。本文从腾讯云开源的TencentDB Agent Memory切入&#xff0c;拆解L0到L3四层记忆蒸馏的机制&#xff0c;再对比Agent Memory和阿里云ContextDB的差异&#xff0c;阐述Mem0、Zep、MemGPT三条业界路线。 预…

作者头像 李华
网站建设 2026/8/18 20:40:00

高性能在线判题系统(OJ)架构设计与优化实践

1. 项目背景与核心价值 "3.25 OJ"这个命名乍看有些神秘&#xff0c;但在算法竞赛圈子里其实是个经典梗。这个数字组合源自圆周率π的近似值3.1415926...取前三位后四舍五入的结果&#xff0c;暗指这是一个与算法、数学密切相关的在线判题系统&#xff08;Online Judg…

作者头像 李华