【C++】set 与multiset

目录

    • [1. 引入:从序列式到关联式容器](#1. 引入:从序列式到关联式容器)
    • [2. set 的设计](#2. set 的设计)
    • [3. 核心接口](#3. 核心接口)
      • [3.1 构造函数 (Constructor)](#3.1 构造函数 (Constructor))
      • [3.2 迭代器操作 (Iterator)](#3.2 迭代器操作 (Iterator))
      • [3.3 容量操作 (Capacity)](#3.3 容量操作 (Capacity))
      • [3.4 修改与查询操作 (Modifiers & Operations)](#3.4 修改与查询操作 (Modifiers & Operations))
    • [4. 核心操作](#4. 核心操作)
      • [4.1 插入与遍历(自动去重排序)](#4.1 插入与遍历(自动去重排序))
      • [4.2 查找与区间操作](#4.2 查找与区间操作)
      • [4.3 multiset:不去重的变体](#4.3 multiset:不去重的变体)
    • [5. 深度剖析:底层引擎为什么是红黑树?](#5. 深度剖析:底层引擎为什么是红黑树?)
      • [5.1 放弃 AVL 树的原因](#5.1 放弃 AVL 树的原因)
      • [5.2 红黑树的妥协与优势](#5.2 红黑树的妥协与优势)
      • [5.3 迭代器底层的精妙设计](#5.3 迭代器底层的精妙设计)
    • [附录:基于红黑树封装自定义 set](#附录:基于红黑树封装自定义 set)

1. 引入:从序列式到关联式容器

在 C++ STL 中,vectorlistdeque 等被称为序列式容器 ,其底层是线性的数据结构,存储的是元素本身。而为了解决海量数据下的高效率检索问题,STL 引入了关联式容器

关联式容器的核心在于存储的是 <key, value> 结构的键值对 。在底层,键值对通过 std::pair 结构体实现,包含代表键值的 key (即 first) 和对应信息的 value (即 second)。

cpp 复制代码
// STL 中键值对的底层定义
template <class T1, class T2>
struct pair
{
    typedef T1 first_type;
    typedef T2 second_type;
    T1 first;
    T2 second;
    pair(): first(T1()), second(T2())
    {}
    pair(const T1& a, const T2& b): first(a), second(b)
    {}
};

2. set 的设计

根据底层结构的不同,关联式容器分为树型结构(如 setmap)和哈希结构。set 作为树形关联容器的代表,其设计具有以下核心特征:

  • 真正的存储结构 :表面上 set 只对外暴露 value,但其底层实际存放的是由 <value, value> 构成的键值对。

  • 元素天然有序且唯一set 内部根据特定的严格弱排序准则(默认按小于升序)对元素进行排序,且每个 value 必须唯一。

  • 元素绝对禁止修改(const)set 中的元素在容器中总是 const 的。深度思考:为什么不允许修改? 因为 set 的底层是二叉搜索树,如果允许修改结点的 key,会直接破坏树的有序性与严格弱排序准则,导致整棵树失效。

  • 时间复杂度 :依靠平衡树支撑,查找、插入和删除的时间复杂度均严格稳定在 O ( log ⁡ 2 N ) O(\log_2 N) O(log2N)。

3. 核心接口

3.1 构造函数 (Constructor)

函数声明 功能介绍
set (const Compare& comp = Compare(), const Allocator& = Allocator() ); 构造空的 set
set (InputIterator first, InputIterator last, ...); [first, last) 区间中的元素构造 set
set (const set<Key,Compare,Allocator>& x); set 的拷贝构造

3.2 迭代器操作 (Iterator)

函数声明 功能介绍
iterator begin() / iterator end() 返回正向迭代器(begin 指向首元素,end 指向尾元素下一个位置)
const_iterator cbegin() / cend() 返回 const 版本的正向迭代器
reverse_iterator rbegin() / rend() 返回反向迭代器(rbeginendrendbegin
const_reverse_iterator crbegin() / crend() 返回 const 版本的反向迭代器

3.3 容量操作 (Capacity)

函数声明 功能介绍
bool empty() const 检测 set 是否为空,空返回 true,否则返回 false
size_type size() const 返回 set 中有效元素的个数

3.4 修改与查询操作 (Modifiers & Operations)

函数声明 功能介绍
pair<iterator,bool> insert(const value_type& x) 在 set 中插入元素 x,返回 <该元素位置, 是否插入成功>(若已存在则返回 false)
iterator erase (const_iterator position) 删除 position 位置上的元素
size_type erase (const key_type& x) 删除 set 中值为 x 的元素,返回删除的元素个数
iterator erase (const_iterator first, const_iterator last) 删除 set 中 [first, last) 区间中的元素
void swap (set<Key, Allocator Compare,>& st) 交换两个 set 中的元素
void clear () 将 set 中的元素清空
iterator find (const key_type& x) const 返回 set 中值为 x 的元素的位置(迭代器),找不到则返回 end()
size_type count (const key_type& x) const 返回 set 中值为 x 的元素的个数(对于 set 只能是 0 或 1)

4. 核心操作

4.1 插入与遍历(自动去重排序)

set 中插入元素时,无需显式构造键值对,直接传入 value 即可。

cpp 复制代码
#include <iostream>
#include <set>
using namespace std;

int main()
{
	// 去重 + 排序
	set<int> s;
	s.insert(5);
	s.insert(2);
	s.insert(7);
	s.insert(4);
	s.insert(9);
	s.insert(9); // 重复插入无效
	s.insert(9);
	s.insert(1);

	auto it = s.begin();
	while (it != s.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	// 范围 for 遍历
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

4.2 查找与区间操作

必须认清算法库中的 std::findset::find 的本质区别:

cpp 复制代码
// 1. 算法库的 find,底层暴力遍历,时间复杂度 O(N)
auto pos1 = find(s.begin(), s.end(), x);  

// 2. set 成员函数 find,利用红黑树查找,时间复杂度 O(log_2 N)
auto pos2 = s.find(x);                    

对于区间操作,set 提供了 lower_bound(返回 ≥ \ge ≥ 目标的迭代器)和 upper_bound(返回 > > > 目标的迭代器),极大地简化了左闭右开 [first, last) 区间的删除操作。

cpp 复制代码
int main()
{
    set<int> myset;
    set<int>::iterator itlow, itup;

    for (int i = 1; i < 10; i++) myset.insert(i * 10); // 10 20 30 40 50 60 70 80 90

    itlow = myset.lower_bound(30);                // >= 30
    itup = myset.upper_bound(60);                 // > 60               
    
    // 删除 [30, 60]
    myset.erase(itlow, itup);                     // 剩余: 10 20 70 80 90

    for (set<int>::iterator it = myset.begin(); it != myset.end(); ++it)
        cout << ' ' << *it;
    cout << endl;
    return 0;
}

4.3 multiset:不去重的变体

cpp 复制代码
int main()
{
	multiset<int> s;
	s.insert(1);
	s.insert(10);
	s.insert(15);
	s.insert(14);
	s.insert(14);
	s.insert(14);

	for (auto w : s)
	{
		cout << w << " ";
	}

	cout << endl << s.count(14) ;
	return 0;
}

如果业务场景仅需排序而不需要去重,可以使用 multiset。其接口与 set 基本一致,底层同样存放 <value, value>,但允许元素重复。此时调用 count(x) 能够返回元素出现的实际次数,而不再局限于 0 或 1。

5. 深度剖析:底层引擎为什么是红黑树?

setmap 的底层均为红黑树 (Red-Black Tree),而不是 AVL 树。

5.1 放弃 AVL 树的原因

AVL 树是绝对平衡的二叉搜索树,要求每个结点的左右子树高度差绝对值不超过 1。这保证了极高的查询效率 O ( log ⁡ 2 N ) O(\log_2 N) O(log2N)。但是,在频繁增删结点的场景下,AVL 树为了维持这种绝对平衡,需要进行大量的旋转操作,甚至在删除时旋转可能持续到根结点,性能开销极大。

5.2 红黑树的妥协与优势

红黑树通过颜色约束牺牲了部分平衡性,来换取更少旋转次数:

  • 每个结点不是红色就是黑色。

  • 根结点必须是黑色。

  • 不能有连在一起的红色结点。

  • 每条路径上的黑色结点数目必须相同。

这些性质确保了红黑树的最长路径不会超过最短路径的两倍 ,达成了一种"近似平衡"。它的增删改查时间复杂度依然是 O ( log ⁡ 2 N ) O(\log_2 N) O(log2N),但由于旋转次数远少于 AVL 树,在实际应用(如 C++ STL、Linux 内核)中具有更高的综合性能。

5.3 迭代器底层的精妙设计

STL 规定 begin()end() 构成前闭后开的区间。在中序遍历红黑树时,begin() 应当是最小结点(最左侧结点),那 end()(最大结点的下一个位置)应该指向哪里?不能简单设为 nullptr,因为还要支持对 end() 迭代器进行 -- 操作找回最后一个元素。

STL 的红黑树实现中,巧妙地增加了一个黑色的头结点 (header)

  • header->_pParent 指向红黑树真实的 root

  • header->_pLeft 指向树中最小的结点(即 begin())。

  • header->_pRight 指向树中最大的结点。

  • end() 迭代器直接指向这个 header 结点。

这种设计完美闭环了整棵树的迭代逻辑。

附录:基于红黑树封装自定义 set

为了证明底层结构与表层 API 的关系,我们可以通过复用泛型红黑树(RBTree)来模拟实现一个完整的 set

cpp 复制代码
#include <functional> // 为了引入 std::less

namespace bit
{
    // 增加 Compare 模板参数,默认使用 less<K>
    template<class K, class Compare = std::less<K>>
    class set
    {
        typedef K ValueType;

        struct KeyOfValue
        {
            const K& operator()(const ValueType& key) const // 注意加 const
            { 
                return key; 
            }
        };

        // 将 Compare 也传给底层的红黑树
        typedef RBTree<K, ValueType, KeyOfValue, Compare> RBTree_t;

    public:
        // 关键:set 的迭代器统一使用红黑树的 const 迭代器
        typedef typename RBTree_t::ConstIterator iterator;
        typedef typename RBTree_t::ConstIterator const_iterator;

    public:
        set() {}

        // 提供 const 版本的迭代器接口
        iterator begin() const { return _t.Begin(); }
        iterator end()   const { return _t.End(); }

        size_t size() const { return _t.Size(); }
        bool empty() const  { return _t.Empty(); }

        // 注意:底层 Insert 如果返回 pair<RBTree_t::Iterator, bool>
        // 这里可能需要做一个隐式或显式的转换,转成 pair<iterator, bool>
        pair<iterator, bool> insert(const ValueType& data)
        {
            return _t.Insert(data);
        }

        void clear() { _t.Clear(); }

        iterator find(const K& key) const // 提供 const 版本的 find
        {
            return _t.Find(key);
        }

    private:
        RBTree_t _t; 
    };
}
相关推荐
Wang's Blog1 小时前
Go-Zero项目开发34: 微服务超时控制与重试机制实践
开发语言·微服务·golang·go-zero
Mortalbreeze2 小时前
深入 Linux Socket 编程:端口号、网络字节序与 struct sockaddr 详解
linux·服务器·网络·c++
2zcode2 小时前
项目文档:基于MATLAB神经网络的心力衰竭预测与临床辅助决策系统研究
开发语言·神经网络·matlab·心力衰竭预测\
减瓦3 小时前
深入 Quarkus:云原生时代 Java 的重生之路
java·开发语言·云原生
杨运交3 小时前
[053][核心模块]Java枚举缓存与ORM集成实践
java·开发语言·缓存
繁星蓝雨3 小时前
C++设计原理——异常处理
java·c++·异常处理·noexcept·throw·try catch
Kurisu_红莉栖3 小时前
关于相关问题的自我回答
c++
hansang_IR3 小时前
【题解】[AGC020E] Encoding Subsets
c++·算法·dp
2401_894915533 小时前
Geo优化系统源码部署搭建技术——PHP程序开发部署指南
开发语言·php