【C++进阶】万字长文详解STL set:从基本用法到红黑树底层原理
摘要/前言:
在C++开发中,当我们面临“去重”和“排序”的双重需求时,std::set无疑是首选利器。但你是否想过,为什么它的查找效率是 O(logN)?为什么迭代器遍历是有序的?底层红黑树到底是如何维持平衡的?本文将从基础用法出发,深入剖析set的底层红黑树机制,并对比unordered_set,助你在面试和实战中游刃有余。
目录
- 什么是 std::set?
- set 的全解
- 进阶:自定义类型的排序(仿函数)
- 核心:底层数据结构——红黑树解析
- 性能对比:set vs unordered_set
- 面试高频问答
1. 什么是 std::set?
std::set 是 C++ 标准模板库(STL)中的一种关联式容器。
它的核心特性可以用三个词概括:
- 有序:元素默认按照升序排列。
- 唯一:容器中不允许存在重复元素(Key 就是 Value)。
- 高效:插入、删除、查找的时间复杂度均为 O(logN)O(\log N)O(logN)。
头文件:
#include<set>2. set 的全解
2.1 构造与插入
insert 函数的返回值是一个 pair,包含一个迭代器和一个布尔值,用于判断插入是否成功。
#include<iostream>#include<set>usingnamespace std;intmain(){ set<int> s;// 1. 插入元素 s.insert(10); s.insert(20); s.insert(30); s.insert(10);// 重复插入,无效// 2. 检测插入结果 pair<set<int>::iterator,bool> ret = s.insert(20);if(ret.second){ cout <<"插入成功"<< endl;}else{ cout <<"插入失败,元素已存在"<< endl;// 输出这个}return0;}2.2 查找与统计
千万不要用全局的 std::find 去查找 set,效率会退化为 O(N)O(N)O(N)。一定要用 set 自带的 find 成员函数。
// 查找auto it = s.find(20);if(it != s.end()){ cout <<"找到了: "<<*it << endl;}// 统计(对于set,结果只能是0或1)if(s.count(30)){ cout <<"30存在"<< endl;}2.3 删除
s.erase(20);// 删除指定值的元素// s.erase(it); // 删除迭代器指向的元素3. 进阶:自定义类型的排序
这是新手最容易卡壳的地方。如果 set 中存放的是结构体或类,必须告诉 set 如何比较大小。
方式一:重载 operator<
structStudent{ string name;int score;// const 修饰非常重要booloperator<(const Student& other)const{// 按分数降序排列returnthis->score > other.score;}};方式二:仿函数(Functor)
这种方式更灵活,不需要修改结构体源码。
structComp{booloperator()(const Student& a,const Student& b)const{return a.score > b.score;}};// 使用时指定比较器 set<Student, Comp> st;4. 核心:底层数据结构——红黑树解析
面试官问 set,其实就是在问红黑树(Red-Black Tree)。
4.1 为什么是红黑树?
std::set 底层通常实现为红黑树。它是一种自平衡的二叉搜索树(BST)。
- 为什么不用普通二叉搜索树?
如果插入的数据是有序的(如 1, 2, 3, 4, 5),普通 BST 会退化成链表,查找效率从 O(logN)O(\log N)O(logN) 降为 O(N)O(N)O(N)。 - 为什么不用 AVL 树?
AVL 树是“绝对平衡”的,查询效率极高,但为了维持绝对平衡,每次插入删除都需要大量的旋转操作。红黑树是“弱平衡”的,在插入删除的性能和查询性能之间取得了更好的折中。
4.2 红黑树的 5 条规则
红黑树通过以下规则保证最长路径不会超过最短路径的 2 倍:
- 每个节点不是红色就是黑色。
- 根节点必须是黑色。
- 如果一个节点是红色的,则它的两个子节点必须是黑色(不能有连续的红节点)。
- 对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。
- 每个叶子节点(NIL节点)都是黑色的。
4.3 迭代器稳定性
set 的迭代器本质上是红黑树节点的指针。
- 重要特性: 除了被删除的那个元素的迭代器外,
set的插入和删除操作不会导致其他迭代器失效。这与vector扩容导致所有迭代器失效完全不同。
5. 性能对比:set vs unordered_set
这是面试中的必考题,一张表讲清楚:
| 特性 | std::set | std::unordered_set |
|---|---|---|
| 底层实现 | 红黑树 (RB-Tree) | 哈希表 (Hash Table) |
| 是否有序 | 有序 | 无序 |
| 查找/插入时间 | O(logN)O(\log N)O(logN) | 平均 O(1)O(1)O(1),最坏 O(N)O(N)O(N) |
| 内存占用 | 较低(存节点指针) | 较高(需维护Bucket数组) |
| 适用场景 | 需要数据有序、范围查找 | 纯粹的快速查找、去重 |
选择建议:
- 如果你需要遍历输出有序数据,或者进行
lower_bound/upper_bound范围查找,必选set。 - 如果你只是为了单纯的快速去重,不在乎顺序,选
unordered_set。
6. 面试高频问答 (Q&A)
Q1: set 中可以通过迭代器修改元素的值吗?
A:不可以。set中的元素是 const 的。因为修改元素值可能会破坏红黑树的有序性。如果必须修改,正确的做法是:先erase旧元素,再insert新元素。
Q2: 红黑树查找的时间复杂度是多少?
A:O(logN)O(\log N)O(logN)。因为红黑树的高度近似为 2logN2\log N2logN。
Q3: 为什么 map/set 的插入效率比 vector 高?
A: 虽然 vector 尾插很快,但如果在中间插入,vector 需要移动后续所有数据。而 map/set 只需要修改节点指针和进行少量的旋转变色,不需要内存拷贝。
总结
std::set 是 C++ 程序员必须掌握的容器。理解其背后的红黑树原理,不仅能帮你写出更高效的代码,更是通往高阶开发的敲门砖。
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、关注!如有疑问,请在评论区留言。