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


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

  • 摘要
  • 目录
    • 一、`set`
      • [1. 类模板认识](#1. 类模板认识)
      • [2. 构造函数认识](#2. 构造函数认识)
      • [3. 迭代器和范围for的使用](#3. 迭代器和范围for的使用)
      • [4.insert 的使用](#4.insert 的使用)
      • [5. empty 和 size 的使用](#5. empty 和 size 的使用)
      • [6. erase 的使用](#6. erase 的使用)
      • [7. swap 的使用](#7. swap 的使用)
      • [8. clear的使用](#8. clear的使用)
      • [10.find 的使用](#10.find 的使用)
      • [10. count 的使用](#10. count 的使用)
      • [11. lower_bound 和upper_bound 的使用](#11. lower_bound 和upper_bound 的使用)
      • [12. equal_range 的使用](#12. equal_range 的使用)
    • 二、`multiset`
      • [1. 类模板和构造函数的认识](#1. 类模板和构造函数的认识)
      • [2. inser的使用](#2. inser的使用)
      • [3. erase的使用](#3. erase的使用)
      • [4. find的使用](#4. find的使用)
      • [5. count的使用](#5. count的使用)
      • [6. equal_range的使用](#6. equal_range的使用)
  • 总结

摘要

本文详细介绍了C++ STL中的两种关联式容器:setmultiset。这两种容器都基于平衡二叉搜索树(通常是红黑树)实现,能够自动对元素进行排序。set 要求元素唯一,而 multiset 允许重复元素。文章全面讲解了它们的构造函数、迭代器、插入删除操作、查找功能以及各种实用成员函数的使用方法。

键对值详解请点击<----------


目录

一、set

1. 类模板认识

这张图定义了 std::set 类模板的模板参数。其中,T 表示集合中存储元素的类型;Compare 决定元素的排序方式,默认是升序;Alloc 表示用于分配内存的分配器,默认是标准分配器 std::allocator<T>set 是一个类模板,用于表示有序且元素唯一的集合。

注意:std::set 是按照一定顺序存储元素的容器,其底层实现为平衡二叉搜索树(通常是红黑树)。在 set 中,元素的值就是键(key) ,类型都是 T,并且每个 key 的值是唯一且不可修改的。因为修改 key 会破坏二叉搜索树的结构,所以 key 只能删除或插入。插入时,如果 key 已经存在,由于唯一性,默认不会插入新的元素。

mapmultimap 不同,set 中只存储 key(即 value),在底层实际存储的是 <value, value> 的形式,每个数据既是键也是值。由于 key 的唯一性,set 可以用于数据去重。用户也可以传入自定义仿函数,实现自己的比较逻辑以得到预期排序结果。


2. 构造函数认识

默认构造(空集合构造)

cpp 复制代码
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 修饰,防止隐式类型转换

区间构造

cpp 复制代码
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。

拷贝构造

cpp 复制代码
set(const set& x);

作用:通过拷贝另一个 set 对象 x 来创建新的 set。复制所有元素及排序规则。新对象与原对象独立,修改其中一个不会影响另一个。

cpp 复制代码
//测试代码
int main()
{
	//默认构造,创建一个空的set容器
	set<int> s;
	//迭代器区间构造,数组初始化set
	int arr[7] = { 1,3,1,4,5,2,0 };
	set<int> s1(arr, arr + sizeof(arr) / sizeof(arr[0]));
	//拷贝构造,根据已有的set创建新的set
	set<int> s2(s1);

	return 0;
}

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

cpp 复制代码
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

	//范围for
	for (auto e : s)
	{
		cout << e << ' ';
	}
	cout << endl;

	return 0;
}

4.insert 的使用

  1. 第一个是向set中插入一个单元素val,iterator指向插入的元素(如果这个元素已经存在就指向存在的元素),bool表示是否插入成功),这样set中就不会插入重复元素,并且返回值就能判断是否插入成功。
  2. 第二个是带位置提示的插入,尝试在position(对插入位置的提示)附近插入元素val,返回指向元素的迭代器(如果提示准确可以提高插入效率,反之set仍会按照内部规则寻找正确的位置去插入)。
  3. 第三个是区间插入,将[first,last)范围内的所有元素插入到set中,它将会自动去重和排序。
cpp 复制代码
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 需要传入 迭代器,而不是整数。所以使用auto
	s1.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 的使用

cpp 复制代码
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 的使用

  1. 删除由迭代器指向的position的位置,删除后position迭代器失效,其他的迭代器仍然存在。
  2. 按值删除元素,返回删除元素的数量。

2.1 对于set来说,相同的元素不可能存在,一个值的元素只可能存在一个。但是为什么不用bool?

erase(const value_type& val) 之所以返回 size_type 而不是 bool,是为了保持 STL 各个关联式容器接口的一致性。因为在 setmap 中元素是唯一的,删除操作最多删除 1 个元素,而在 multisetmultimap 中同一个键可能对应多个元素,删除时可能会移除多个。因此标准库统一返回删除的元素数量(size_type),既能表示删除是否成功,也能在多重容器中反映删除的个数。在 set 中它实际上等价于布尔判断:返回 1 表示删除成功,返回 0 表示未找到目标元素。

  1. 删除迭代器区间[first,last)区间的所有元素,删除后范围内的迭代器失效,其他范围的迭代器仍然有效。
cpp 复制代码
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对象的数据

cpp 复制代码
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对象的数据

cpp 复制代码
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)。

cpp 复制代码
int main()
{
	int arr[7] = { 9,5,34,245,56,88,99 };

	set<int> s(arr, arr + sizeof(arr) / sizeof(arr[0]));

	//查找88
	set<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 中元素唯一,因此返回值只有 01,但仍用 size_t 是为了与其他容器保持统一接口设计。


11. lower_bound 和upper_bound 的使用

都是用于在有序容器(如 setmap)中查找位置的函数:

  • lower_bound(val) :返回指向第一个不小于 val (即 >= val)的元素的迭代器。
  • upper_bound(val) :返回指向第一个大于 val (即 > val)的元素的迭代器。

两者的返回值都是 iterator 类型,表示在容器中对应位置的元素;如果没有符合条件的元素(即到达容器末尾),则返回 end()。在 set 中,它们常用于查找区间范围 [lower_bound(val), upper_bound(val))

cpp 复制代码
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 大的第一个元素是 2
    set<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 的元素,即 99
    set<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);

    // 再次打印删除后的 set
    cout << "删除 [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 的返回结果实际上等价于:

cpp 复制代码
{ lower_bound(key), upper_bound(key) }

例如:

  1. set 中的元素为 {1, 5, 7},传入 key = 5 时,equal_range(5) 返回 [5, 7),即起始迭代器指向 5,结束迭代器指向 7。若对该区间调用 erase,会删除值为 5 的元素。

  2. 如果 key 不存在,比如传入 key = 3,则会返回 [5, 5):即起始与结束迭代器都指向比 3 大的第一个元素(5 的位置),代表一个空区间 。再进一步,如果传入的 key 大于容器中所有元素,则返回 [end(), end()),同样代表不存在的区间

  3. 因为需要同时返回两个迭代器,equal_range 的返回类型被设计为 pair<iterator, iterator>

    • first 存储区间的起始迭代器;
    • second 存储区间的结束迭代器。
cpp 复制代码
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;   // 输出 5
    cout << "结束迭代器指向:" << *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;   // 输出 5
    cout << "结束迭代器指向:" << *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::multisetstd::set都是类模板,他们的基本使用和接口基本相同,但是set中不能存放相同的key值,但是multiset中可以。

其类模板和构造函数的认识均可参考上述set的类模板和构造函数的认识。


2. inser的使用

与set不同的是可以插入多个相同的值

cpp 复制代码
#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值有相同的将会全部删除。

cpp 复制代码
ms.erase(5);
for (auto e : ms)
{
	cout << e << ' ';
}
cout << endl;

4. find的使用

与set不同的是,当有多个相同的key值,我们使用find查找将会返回通过中序遍历中第一个key值的位置迭代器。


5. count的使用

与set不同的是,当有多个key值的时候,count将会返回总个数。

cpp 复制代码
size_t Count = ms.count(5);
cout << Count << endl;

6. equal_range的使用

equal_range 的含义是"相等范围",即用于查找与指定 key 值相等的一段连续区间的起始与结束位置迭代器。在 set 中,虽然每个 key 值都唯一,不存在真正意义上的"相等序列",但该函数仍然被保留,是为了与允许重复键值的容器(如 multiset)保持接口一致性。

cpp 复制代码
	//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; //指向第一个2
	cout << *(p.second) << endl;//指向第一个大于2的元素

总结

C++ STL中的setmultiset是基于红黑树实现的有序关联容器,它们核心区别在于set存储唯一元素而multiset允许重复。两者都支持自动排序、O(log n)的高效查找、插入和删除,并提供了丰富的操作接口,包括使用迭代器遍历、插入(insert)、按值或位置删除(erase)、以及findcount等查找功能。特别地,lower_boundupper_bound用于进行范围查找,而equal_range能直接获取某个键的完整范围,这在处理multiset中的重复元素时尤为实用。由于其有序和高效的特性,它们非常适合需要快速查找、自动排序或数据去重(特指set)的场景。


✨ 坚持用 清晰易懂的图解 + 代码语言, 让每个知识点都 简单直观 !

🚀 个人主页不呆头 · CSDN

🌱 代码仓库不呆头 · Gitee

📌 专栏系列

💬 座右铭 : "不患无位,患所以立。"

相关推荐
q***31892 小时前
Spring Boot--@PathVariable、@RequestParam、@RequestBody
java·spring boot·后端
Macbethad2 小时前
如何用WPF做工控设置界面
java·开发语言·wpf
玖笙&2 小时前
✨WPF编程进阶【7.2】:动画类型(附源码)
c++·c#·wpf·visual studio
CodeAmaz2 小时前
使用责任链模式设计电商下单流程(Java 实战)
java·后端·设计模式·责任链模式·下单
喝养乐多长不高2 小时前
Rabbit MQ:概述
java·rabbitmq·mq·amqp
大炮走火2 小时前
iOS在制作framework时,oc与swift混编的流程及坑点!
开发语言·ios·swift
她说彩礼65万2 小时前
C# 容器实例生命周期
开发语言·c#
San30.3 小时前
JavaScript 深度解析:从 map 陷阱到字符串奥秘
开发语言·javascript·ecmascript
拾忆,想起3 小时前
Dubbo异步调用实战指南:提升微服务并发性能
java·服务器·网络协议·微服务·云原生·架构·dubbo