STL中关于map和set的四个问题?

本文深入探讨了STL中的map和set容器的工作原理,包括它们为什么在插入和删除操作上比其他容器更高效,迭代器在插入后为什么不会失效等问题。揭示了这些容器背后所采用的红黑树数据结构及其带来的性能优势。

 STL map和set的使用虽不复杂,但也有一些不易理解的地方,如:

# 为何map和set的插入删除效率比用其他序列容器高?

# 为何每次insert之后,以前保存的iterator不会失效?

# 为何map和set不能像vector一样有个reserve函数来预分配数据?

# 当数据元素增多时(10000到20000个比较),map和set的插入和搜索速度变化如何?


或许有得人能回答出来大概原因,但要彻底明白,还需要了解STL的底层数据结构。


C++ STL 之所以得到广泛的赞誉,也被很多人使用,不只是提供了像vector, string, list等方便的容器,更重要的是STL封


装了许多复杂的数据结构算法和大量常用数据结构操作。vector封装数组,list封装了链表,map和 set封装了二叉树等,在


封装这些数据结构的时候,STL按照程序员的使用习惯,以成员函数方式提供的常用操作,如:插入、排序、删除、查找等。


让用户在 STL使用过程中,并不会感到陌生。


C++ STL中标准关联容器set, multiset, map, multimap内部采用的就是一种非常高效的平衡检索二叉树:红黑树,也成为RB


树(Red-Black Tree)。RB树的统计性能要好于一般的平衡二叉树(有些书籍根据作者姓名,Adelson-Velskii和Landis,将其称


为AVL-树),所以被STL选择作为了关联容器的内部结构。本文并不会介绍详细AVL树和RB树的实现以及他们的优劣,关于RB树


的详细实现参看红黑树: 理论与实现(理论篇)。本文针对开始提出的几个问题的回答,来向大家简单介绍map和set的底层数据


结构。


为何map和set的插入删除效率比用其他序列容器高?


大部分人说,很简单,因为对于关联容器来说,不需要做内存拷贝和内存移动。说对了,确实如此。map和set容器内所有元素


都是以节点的方式来存储,其节点结构和链表差不多,指向父节点和子节点。结构图可能如下:


A

/ \

B    C

/ \ / \

D E F G


因此插入的时候只需要稍做变换,把节点的指针指向新的节点就可以了。删除的时候类似,稍做变换后把指向删除节点的指针


指向其他节点就OK了。这里的一切操作就是指针换来换去,和内存移动没有关系。


为何每次insert之后,以前保存的iterator不会失效?


看见了上面答案的解释,你应该已经可以很容易解释这个问题。iterator这里就相当于指向节点的指针,内存没有变,指向内


存的指针怎么会失效呢(当然被删除的那个元素本身已经失效了)。相对于vector来说,每一次删除和插入,指针都有可能失效


,调用push_back在尾部插入也是如此。因为为了保证内部数据的连续存放,iterator指向的那块内存在删除和插入过程中可


能已经被其他内存覆盖或者内存已经被释放了。即使时push_back的时候,容器内部空间可能不够,需要一块新的更大的内存


,只有把以前的内存释放,申请新的更大的内存,复制已有的数据元素到新的内存,最后把需要插入的元素放到最后,那么以


前的内存指针自然就不可用了。特别时在和find等算法在一起使用的时候,牢记这个原则:不要使用过期的iterator。


为何map和set不能像vector一样有个reserve函数来预分配数据?


我以前也这么问,究其原理来说时,引起它的原因在于在map和set内部存储的已经不是元素本身了,而是包含元素的节点。也


就是说map内部使用的Alloc并不是map<Key, Data, Compare, Alloc>声明的时候从参数中传入的Alloc。例如:


map<int, int, less<int>, Alloc<int> > intmap;


这时候在intmap中使用的allocator并不是Alloc<int>, 而是通过了转换的Alloc,具体转换的方法时在内部通过


Alloc<int>::rebind重新定义了新的节点分配器,详细的实现参看彻底学习STL中的Allocator。其实你就记住一点,在map和


set内面的分配器已经发生了变化,reserve方法你就不要奢望了。


当数据元素增多时(10000和20000个比较),map和set的插入和搜索速度变化如何?


如果你知道log2的关系你应该就彻底了解这个答案。在map和set中查找是使用二分查找,也就是说,如果有16个元素,最多需


要比较4次就能找到结果,有32个元素,最多比较5次。那么有10000个呢?最多比较的次数为log10000,最多为14次,如果是


20000个元素呢?最多不过15次。看见了吧,当数据量增大一倍的时候,搜索次数只不过多了1次,多了1/14的搜索时间而已。


你明白这个道理后,就可以安心往里面放入元素了。


最后,对于map和set Winter还要提的就是它们和一个c语言包装库的效率比较。在许多unix和linux平台下,都有一个库叫isc


,里面就提供类似于以下声明的函数:


void tree_init(void **tree);

void *tree_srch(void **tree, int (*compare)(), void *data);

void tree_add(void **tree, int (*compare)(), void *data, void (*del_uar)());

int tree_delete(void **tree, int (*compare)(), void *data,void (*del_uar)());

int tree_trav(void **tree, int (*trav_uar)());

void tree_mung(void **tree, void (*del_uar)());


许多人认为直接使用这些函数会比STL map速度快,因为STL map中使用了许多模板什么的。其实不然,它们的区别并不在于算


法,而在于内存碎片。如果直接使用这些函数,你需要自ma己去new一些节点,当节点特别多,而且进行频繁的删除和插入的时


候,内存碎片就会存在,而STL采用自己的Allocator分配内存,以内存池的方式来管理这些内存,会大大减少内存碎片,从而


会提升系统的整体性能。Winter在自己的系统中做过测试,把以前所有直接用isc函数的代码替换成map,程序速度基本一致。


当时间运行很长时间后(例如后台服务程序),map的优势就会体现出来。从另外一个方面讲,使用map会大大降低你的编码难


度,同时增加程序的可读性。何乐而不为?

<think> 嗯,用户这次明确询问C++ STLmapset的区别。结合之前的系统指令,我需要组织一个专业且清晰的回答。 用户引用了两段资料,第一段提到两者底层都是红黑树实现,且操作都是转调红黑树接口。第二段补充了set采用K模型而map采用KV模型,并列出部分接口。这些信息基本正确,但需要更系统地整理。 我考虑从四个维度展开区别:首先是元素构成(set存key/map存键值对),其次是功能定位(纯集合vs映射关系),然后是迭代器特性(set只读/map部分可写),最后是操作接口差异(如map独有的operator[])。还要强调底层实现的一致性——都是红黑树,这解释了为何性能特征相似。 用户可能混淆两者的使用场景,所以需要举例说明:比如存储学号集合用set,存储学号对应姓名就用map。性能方面可以提都是O(log n),但map内存占用更大。 注意到用户上次关注快速排序的实现,这次转向容器区别,可能是在学习数据结构的实际应用。因此回答要突出树结构如何支撑效查找——毕竟红黑树的自平衡特性保障了最坏情况性能。 最后生成的问题要引导深入思考:为什么选择红黑树而不是哈希表?这能帮助用户理解设计哲学。接口对比常量迭代器的问题则瞄准实际编程痛点。 </think> 在C++ STL中,`map``set`都是基于**红黑树**(自平衡二叉搜索树)实现的关联容器,核心区别在于元素组织功能定位: --- ### 🔑 核心区别 | 特性 | `set` | `map` | |--------------|--------------------------------|--------------------------------| | **存储内容** | 仅存储唯一键(Key) | 存储键值对(Key-Value) | | **数据模型** | K模型(纯关键字集合) | KV模型(关键字到值的映射) | | **元素访问** | 迭代器仅访问键(`*it`为Key) | 迭代器访问键值对(`it->first`为Key, `it->second`为Value) | | **修改值** | ❌ Key不可直接修改 | ✅ Value可修改(Key仍不可变) | | **典型操作** | 检查成员是否存在(`find()`) | 按键检索/修改值(`operator[]`)| --- ### ️ 功能与接口差异 1. **元素插入** - `set`: `insert(key)` - `map`: `insert({key, value})` 或 `emplace(key, value)` 2. **元素访问** - `map` 独有 `operator[]`: ```cpp map<string, int> m; m["apple"] = 5; // 若key不存在则创建,并赋值 ``` - `set` 无直接访问接口,需用迭代器或`find()` 3. **迭代器行为** - `set::iterator` 指向 **const Key**,禁止修改键值 - `map::iterator` 指向 **pair<const Key, Value>**,可修改`second`(Value) --- ### ️ 底层实现共性 两者均基于红黑树(RB-Tree),因此共享以下特性: - ✅ **自动排序**:元素按Key严格弱序(默认`std::less`)排列 - ✅ **操作复杂度**:查找/插入/删除均为 **O(log n)** - ✅ **唯一Key约束**:不允许重复Key(若需重复可用`multiset`/`multimap`) --- ### 📌 使用场景 | 容器 | 典型场景 | |-------|---------------------------------------------| | `set` | 去重集合(如屏蔽词库)、存在性检查 | | `map` | 键值映射(如ID→对象)、计数器(如词频统计) | --- ### 示例对比 ```cpp // set 示例 set<int> s = {3, 1, 4}; s.insert(2); // 插入Key auto it = s.find(3); // 查找Key // map 示例 map<string, int> fruitCount; fruitCount["apple"] = 5; // 插入键值对 fruitCount["banana"]++; // 修改Value cout << fruitCount["apple"]; // 输出5 ``` > 💡 **关键结论**: > `set` = **纯Key集合**,`map` = **Key-Value映射**。两者共享红黑树的效特性,但语义接口针对不同场景设计[^1][^2]。 --- ### ⁉️ 相关问题 1. **为什么`map``set`选择红黑树而非哈希表?红黑树的哪些特性适合关联容器?** 2. **`unordered_map``map`在性能与应用场景上有何本质区别?** 3. **如何为自定义类型实现`map`/`set`所需的严格弱序(如重载`operator<`)?** 4. **`set`的迭代器为什么是常量(const)?这种设计有何优缺点?**
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值