【C++】 set/multiset底层原理与逻辑详解

【C++】 set/multiset底层原理与逻辑详解
- 摘要
- 目录
- 一、`set`
- 1. 类模板认识
- 2. 构造函数认识
- 3. 迭代器和范围for的使用
- 4.insert 的使用
- 5. empty 和 size 的使用
- 6. erase 的使用
- 7. swap 的使用
- 8. clear的使用
- 10.find 的使用
- 10. count 的使用
- 11. lower_bound 和upper_bound 的使用
- 12. equal_range 的使用
- 二、`multiset`
- 1. 类模板和构造函数的认识
- 2. inser的使用
- 3. erase的使用
- 4. find的使用
- 5. count的使用
- 6. equal_range的使用
- 总结
摘要
本文详细介绍了C++ STL中的两种关联式容器:set 和 multiset。这两种容器都基于平衡二叉搜索树(通常是红黑树)实现,能够自动对元素进行排序。set 要求元素唯一,而 multiset 允许重复元素。文章全面讲解了它们的构造函数、迭代器、插入删除操作、查找功能以及各种实用成员函数的使用方法。
键对值详解请点击<----------
目录
一、set
1. 类模板认识

这张图定义了
std::set类模板的模板参数。其中,T表示集合中存储元素的类型;Compare决定元素的排序方式,默认是升序;Alloc表示用于分配内存的分配器,默认是标准分配器std::allocator<T>。set是一个类模板,用于表示有序且元素唯一的集合。注意:
std::set是按照一定顺序存储元素的容器,其底层实现为平衡二叉搜索树(通常是红黑树)。在set中,元素的值就是键(key),类型都是T,并且每个 key 的值是唯一且不可修改的。因为修改 key 会破坏二叉搜索树的结构,所以 key 只能删除或插入。插入时,如果 key 已经存在,由于唯一性,默认不会插入新的元素。与
map或multimap不同,set中只存储 key(即 value),在底层实际存储的是<value, value>的形式,每个数据既是键也是值。由于 key 的唯一性,set可以用于数据去重。用户也可以传入自定义仿函数,实现自己的比较逻辑以得到预期排序结果。
2. 构造函数认识

默认构造(空集合构造)
explicit set(const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());
创建一个空的 set 对象。其中comp:用于指定元素排序规则的比较函数对象,默认使用 key_compare(通常是 less)。alloc:用于内存分配的分配器,默认使用 allocator_type(通常是 std::allocator)。特点:explicit 修饰,防止隐式类型转换
区间构造
template <class InputIterator>
set(InputIterator first, InputIterator last,const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());
通过给定的 [first, last) 范围内的元素来初始化 set。first, last:输入迭代器(左闭右开),指定要插入的元素范围。comp:元素排序规则。alloc:内存分配器。特点:可以一次性用一段已有数据初始化 set。
拷贝构造
set(const set& x);
作用:通过拷贝另一个 set 对象 x 来创建新的 set。复制所有元素及排序规则。新对象与原对象独立,修改其中一个不会影响另一个。
//测试代码
int main()
{//默认构造,创建一个空的set容器set<int> s;//迭代器区间构造,数组初始化setint arr[7] = { 1,3,1,4,5,2,0 };set<int> s1(arr, arr + sizeof(arr) / sizeof(arr[0]));//拷贝构造,根据已有的set创建新的setset<int> s2(s1);return 0;
}

3. 迭代器和范围for的使用

int main()
{int arr[7] = { 1,3,1,4,5,2,0 };set<int> s1(arr, arr + sizeof(arr) / sizeof(arr[0]));//正向迭代set<int>::iterator it = s1.begin();while (it != s1.end()){cout << *it << ' ';++it;}cout << endl;//反向迭代set<int>::reverse_iterator rit = s1.rbegin();while (rit != s1.rend()){cout << *rit << " ";rit++;}cout << endl; //44 33 22 11//范围forfor (auto e : s){cout << e << ' ';}cout << endl;return 0;
}

4.insert 的使用

- 第一个是向set中插入一个单元素val,iterator指向插入的元素(如果这个元素已经存在就指向存在的元素),bool表示是否插入成功),这样set中就不会插入重复元素,并且返回值就能判断是否插入成功。
- 第二个是带位置提示的插入,尝试在position(对插入位置的提示)附近插入元素val,返回指向元素的迭代器(如果提示准确可以提高插入效率,反之set仍会按照内部规则寻找正确的位置去插入)。
- 第三个是区间插入,将[first,last)范围内的所有元素插入到set中,它将会自动去重和排序。
int main()
{//插入单个数据set<int> s;s.insert(5);s.insert(2);s.insert(0);s.insert(1);s.insert(3);s.insert(1);s.insert(4);//带提示插入set<int> s1(s);auto position = s1.find(5);//找到5的位置,作为插入提示//s1.find(5) 返回的是 set<int>::iterator//带提示的 insert 需要传入 迭代器,而不是整数。所以使用autos1.insert(position, 6);//尝试在position的附近插入6//区间插入int arr[7] = { 1,3,1,4,5,2,0 };set<int> s2(arr, arr + sizeof(arr) / sizeof(arr[0]));return 0;
}

5. empty 和 size 的使用

int main()
{set<int> s;cout << "s.size: " << s.size() << endl;cout << "s.empty: " << s.empty() << endl;int arr[7] = { 1,3,1,4,5,2,0 };set<int> s1(arr, arr + sizeof(arr) / sizeof(arr[0]));cout << "s1.size: " << s1.size() << endl;cout << "s1.empty: " << s1.empty() << endl;return 0;
}

6. erase 的使用

- 删除由迭代器指向的position的位置,删除后position迭代器失效,其他的迭代器仍然存在。
- 按值删除元素,返回删除元素的数量。
2.1 对于set来说,相同的元素不可能存在,一个值的元素只可能存在一个。但是为什么不用bool?
erase(const value_type& val)之所以返回size_type而不是bool,是为了保持 STL 各个关联式容器接口的一致性。因为在set和map中元素是唯一的,删除操作最多删除 1 个元素,而在multiset和multimap中同一个键可能对应多个元素,删除时可能会移除多个。因此标准库统一返回删除的元素数量(size_type),既能表示删除是否成功,也能在多重容器中反映删除的个数。在set中它实际上等价于布尔判断:返回1表示删除成功,返回0表示未找到目标元素。
- 删除迭代器区间[first,last)区间的所有元素,删除后范围内的迭代器失效,其他范围的迭代器仍然有效。
int main()
{int arr[7] = { 1,3,1,4,5,2,0 };set<int> s(arr, arr + sizeof(arr) / sizeof(arr[0]));//删除迭代器指向的位置s.erase(s.begin());for (auto e : s){cout << e << ' ';}cout << endl;//按值删除元素s.erase(5);for (auto e : s){cout << e << ' ';}cout << endl;//删除迭代器范围内的元素s.erase(s.begin(), s.end());for (auto e : s){cout << e << ' ';}cout << endl;return 0;
}

7. swap 的使用

交换两个set对象的数据
int main()
{set<int> s;s.insert(1);s.insert(3);s.insert(6);s.insert(8);int arr[7] = { 1,3,1,4,5,2,0 };set<int> s1(arr, arr + sizeof(arr) / sizeof(arr[0]));s.swap(s1);return 0;
}

8. clear的使用

清除set对象的数据
int main()
{set<int> s;s.insert(1);s.insert(3);s.insert(6);s.insert(8);int arr[7] = { 1,3,1,4,5,2,0 };set<int> s1(arr, arr + sizeof(arr) / sizeof(arr[0]));s.swap(s1);s1.clear();return 0;
}

10.find 的使用

在set容器中查找值为val的元素,如果找到返回指向该元素的迭代器,如果没有找到返回end()。注意:这里的find和标准库的find并不一样。标准库的find是区间遍历查找没时间复杂度为O(N),这里是在二叉搜索数基础上进行了平衡,效率为O(logN)。
int main()
{int arr[7] = { 9,5,34,245,56,88,99 };set<int> s(arr, arr + sizeof(arr) / sizeof(arr[0]));//查找88set<int>::iterator it = s.find(88);if (it != s.end()) //如果不是最后一个元素{cout << *it << endl;s.erase(it);//删除它}else{s.insert(999);//如果是最后一个元素就插入999}for (auto e : s){cout << e << ' ';}cout << endl;return 0;
}

10. count 的使用

count函数用于统计指定值在容器中出现的次数,返回类型为size_t(即size_type),因为统计结果是一个非负整数,而size_t专门用于表示对象大小或数量,能够适配不同平台下的无符号整数范围,避免溢出或负数结果。在set中元素唯一,因此返回值只有0或1,但仍用size_t是为了与其他容器保持统一接口设计。
11. lower_bound 和upper_bound 的使用

都是用于在有序容器(如
set、map)中查找位置的函数:
lower_bound(val):返回指向第一个不小于val(即>= val)的元素的迭代器。upper_bound(val):返回指向第一个大于val(即> val)的元素的迭代器。两者的返回值都是
iterator类型,表示在容器中对应位置的元素;如果没有符合条件的元素(即到达容器末尾),则返回end()。在set中,它们常用于查找区间范围[lower_bound(val), upper_bound(val))。
int main()
{int arr[] = { 13,31,25,2,77,22,99 };set<int> s(arr, arr + sizeof(arr) / sizeof(arr[0]));for (auto e : s)cout << e << ' ';cout << endl;// lower_bound:返回第一个 >= val 的元素位置// 这里没有 0,但比 0 大的第一个元素是 2set<int>::iterator it_low = s.lower_bound(0);cout << "lower_bound(0) 指向的元素是:" << *it_low << endl;// lower_bound(22):找到第一个 >= 22 的元素,即 22 本身set<int>::iterator it1 = s.lower_bound(22);// upper_bound(77):找到第一个 > 77 的元素,即 99set<int>::iterator it2 = s.upper_bound(77);cout << "lower_bound(22) 指向:" << *it1 << endl;cout << "upper_bound(77) 指向:" << *it2 << endl;// erase(it1, it2):删除 [it1, it2) 区间的所有元素// 由于区间是前闭后开,所以删除的范围是 [22, 77]s.erase(it1, it2);// 再次打印删除后的 setcout << "删除 [22,77] 区间后剩余元素:" << endl;for (auto e : s)cout << e << ' ';cout << endl;return 0;
}

12. equal_range 的使用

equal_range的含义是“相等范围”,即用于查找与指定key值相等的一段连续区间的起始与结束位置迭代器。在set中,虽然每个key值都唯一,不存在真正意义上的“相等序列”,但该函数仍然被保留,是为了与允许重复键值的容器(如multiset)保持接口一致性。
在 set 中,equal_range 的返回结果实际上等价于:
{ lower_bound(key), upper_bound(key) }
例如:
-
当
set中的元素为{1, 5, 7},传入key = 5时,equal_range(5)返回[5, 7),即起始迭代器指向5,结束迭代器指向7。若对该区间调用erase,会删除值为5的元素。 -
如果
key不存在,比如传入key = 3,则会返回[5, 5):即起始与结束迭代器都指向比3大的第一个元素(5的位置),代表一个空区间。再进一步,如果传入的key大于容器中所有元素,则返回[end(), end()),同样代表不存在的区间。 -
因为需要同时返回两个迭代器,
equal_range的返回类型被设计为pair<iterator, iterator>:
-
first存储区间的起始迭代器;
-
second存储区间的结束迭代器。
int main()
{//初始化int arr[] = { 1, 5, 7 };set<int> s(arr, arr + sizeof(arr) / sizeof(arr[0]));//打印cout << "当前 set 中的元素为:";for (auto e : s)cout << e << ' ';cout << endl;// ---------- 示例1:key 存在的情况 ----------// equal_range(5):查找 key=5 对应的区间范围pair<set<int>::iterator, set<int>::iterator> range1 = s.equal_range(5);cout << "\n查找 key=5 的区间结果:" << endl;cout << "起始迭代器指向:" << *range1.first << endl; // 输出 5cout << "结束迭代器指向:" << *range1.second << endl; // 输出 7(>5 的第一个元素)// 使用区间删除 [5,7)s.erase(range1.first, range1.second);cout << "删除 key=5 后的 set 元素:";for (auto e : s)cout << e << ' ';cout << endl;// ---------- 示例2:key 不存在的情况 ----------// 重新插入数据s.insert(5);cout << "\n重新插入 5 后,set 元素:";for (auto e : s)cout << e << ' ';cout << endl;// equal_range(3):key=3 不存在,会返回 [5,5)pair<set<int>::iterator, set<int>::iterator> range2 = s.equal_range(3);cout << "\n查找 key=3 的区间结果:" << endl;cout << "起始迭代器指向:" << *range2.first << endl; // 输出 5cout << "结束迭代器指向:" << *range2.second << endl; // 同样输出 5(因为返回 [5,5))// ---------- 示例3:key 比最大元素还大 ----------pair<set<int>::iterator, set<int>::iterator> range3 = s.equal_range(100);cout << "\n查找 key=100 的区间结果:" << endl;if (range3.first == s.end() && range3.second == s.end())cout << "返回 [end(), end()) —— key 不存在且大于所有元素。" << endl;return 0;
}

二、multiset
注意:关于multiset的讲解我们主要聚焦于与set的差别部分
1. 类模板和构造函数的认识

std::multiset与std::set都是类模板,他们的基本使用和接口基本相同,但是set中不能存放相同的key值,但是multiset中可以。

其类模板和构造函数的认识均可参考上述set的类模板和构造函数的认识。
2. inser的使用
与set不同的是可以插入多个相同的值
#include<iostream>
#include<set>
using namespace std;int main()
{multiset<int> ms;ms.insert(2);ms.insert(0);ms.insert(2);ms.insert(5);ms.insert(2);ms.insert(0);ms.insert(2);ms.insert(5);for (auto e : ms){cout << e << ' ';}cout << endl;return 0;
}

3. erase的使用
与set不同的是,在删除key值的时候,如果key值有相同的将会全部删除。
ms.erase(5);
for (auto e : ms)
{cout << e << ' ';
}
cout << endl;

4. find的使用
与set不同的是,当有多个相同的key值,我们使用find查找将会返回通过中序遍历中第一个key值的位置迭代器。
5. count的使用
与set不同的是,当有多个key值的时候,count将会返回总个数。
size_t Count = ms.count(5);
cout << Count << endl;

6. equal_range的使用
equal_range的含义是“相等范围”,即用于查找与指定key值相等的一段连续区间的起始与结束位置迭代器。在set中,虽然每个key值都唯一,不存在真正意义上的“相等序列”,但该函数仍然被保留,是为了与允许重复键值的容器(如multiset)保持接口一致性。
//equal_range 返回的类型是一个 pair,两个成员分别是迭代器类型。//所以这里定义 p 的类型为 pair<multiset<int>::iterator, multiset<int>::iterator>,// 用来接收返回结果。pair<multiset<int>::iterator, multiset<int>::iterator> p = ms.equal_range(2);cout << *(p.first) << endl; //指向第一个2cout << *(p.second) << endl;//指向第一个大于2的元素

总结
C++ STL中的set和multiset是基于红黑树实现的有序关联容器,它们核心区别在于set存储唯一元素而multiset允许重复。两者都支持自动排序、O(log n)的高效查找、插入和删除,并提供了丰富的操作接口,包括使用迭代器遍历、插入(insert)、按值或位置删除(erase)、以及find、count等查找功能。特别地,lower_bound和upper_bound用于进行范围查找,而equal_range能直接获取某个键的完整范围,这在处理multiset中的重复元素时尤为实用。由于其有序和高效的特性,它们非常适合需要快速查找、自动排序或数据去重(特指set)的场景。
✨ 坚持用 清晰易懂的图解 + 代码语言, 让每个知识点都 简单直观 !
🚀 个人主页 :不呆头 · CSDN
🌱 代码仓库 :不呆头 · Gitee
📌 专栏系列 :
- 📖 《C语言》
- 🧩 《数据结构》
- 💡 《C++》
- 🐧 《Linux》
💬 座右铭 : “不患无位,患所以立。”

