C++ set和multiset的使用

文章目录

  • [1. 序列式容器和关联式容器(了解)](#1. 序列式容器和关联式容器(了解))
  • [2. set系列的使用](#2. set系列的使用)
    • [2.1 set类的介绍](#2.1 set类的介绍)
    • [2.2 set的构造和迭代器](#2.2 set的构造和迭代器)
      • 0.构造:
      • [1. 空构造(empty (1))](#1. 空构造(empty (1)))
      • [2. 范围构造(range (2))](#2. 范围构造(range (2)))
      • [3. 拷贝构造(copy (3))](#3. 拷贝构造(copy (3)))
      • [4. 初始化列表(C++11)](#4. 初始化列表(C++11))
    • [2.3 修改器(Modifiers)的成员函数](#2.3 修改器(Modifiers)的成员函数)
      • [0. 迭代器](#0. 迭代器)
      • [1. insert:插入元素](#1. insert:插入元素)
      • [2. erase:删除元素](#2. erase:删除元素)
      • [3. swap:交换两个set的内容(与算法库swap对比)](#3. swap:交换两个set的内容(与算法库swap对比))
      • [4. clear:清空所有元素](#4. clear:清空所有元素)
      • [5. emplace:构造并插入元素(C++11+)](#5. emplace:构造并插入元素(C++11+))
      • [6. emplace_hint:带位置提示的构造插入(C++11+)](#6. emplace_hint:带位置提示的构造插入(C++11+))
    • [2.4 find(与算法库find的对比)](#2.4 find(与算法库find的对比))
    • [2.5 key_comp && value_comp](#2.5 key_comp && value_comp)
    • [2.6 count(与find比较)](#2.6 count(与find比较))
    • [2.7 lower_bound && upper_bound](#2.7 lower_bound && upper_bound)
  • [3. multiset](#3. multiset)
    • [3.1 核心特性差异](#3.1 核心特性差异)
    • [3.2 接口行为差异](#3.2 接口行为差异)
    • [3.3 使用场景差异](#3.3 使用场景差异)
  • [4. 例题部分](#4. 例题部分)
    • [4.1 环形链表 II](#4.1 环形链表 II)
    • [4.2 两个数组的交集](#4.2 两个数组的交集)

1. 序列式容器和关联式容器(了解)

前面我们已经接触过STL中的部分容器如:string、vector、list、deque、array、forward_list等,这些容器统称为序列式容器,因为逻辑结构为线性序列的数据结构,两个位置存储的值之间一般没有紧密的关联关系,比如交换一下,他依旧是序列式容器。顺序容器中的元素是按他们在容器中的存储位置来顺序保存和访问的。

关联式容器也是用来存储数据的,与序列式容器不同的是,关联式容器逻辑结构通常是非线性结构,两个位置有紧密的关联关系,交换一下,他的存储结构就被破坏了。顺序容器中的元素是按关键字来保存和访问的。关联式容器有map/set系列和unordered_map/unordered_set系列。

mapset底层是红黑树,红黑树是一颗平衡二叉搜索树。setkey搜索场景的结构,mapkey/value搜索场景的结构。
说人话 就是map set的值不能改 改了结构会被破坏。

2. set系列的使用

2.1 set类的介绍

  • set的声明如下,T就是set底层关键字的类型
  • set默认要求T支持小于比较,如果不支持或者想按自己的需求走可以自行实现仿函数传给第二个模版参数。
  • set底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给第三个参数。
  • 一般情况下,我们都不需要传后两个模版参数。
  • set底层是用红黑树实现,增删查效率是 O ( l o g N ) O(logN) O(logN),迭代器遍历是走的搜索树的中序,所以是有序的。
  • vector/list等容器的使用,STL容器接口设计,高度相似,所以这里我们就不再一个接口一个接口的介绍,挑比较重要的接口进行介绍。

2.2 set的构造和迭代器

0.构造:

set 的支持正向和反向迭代遍历,遍历默认按升序顺序,因为底层是二叉搜索树,迭代器遍历走的中序;支持迭代器就意味着支持范围 for,setiteratorconst_iterator 都不支持迭代器修改数据 ,修改关键字数据,防止破坏底层搜索树的结构。

1. 空构造(empty (1))

cpp 复制代码
explicit set (const key_compare& comp = key_compare(),
               const allocator_type& alloc = allocator_type());
  • 作用 :创建空的set容器。
  • 参数
    • comp:可选,自定义的键比较规则(默认使用key_compare,即<比较);
    • alloc:可选,内存分配器(默认使用allocator_type)。

2. 范围构造(range (2))

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:同空构造的可选参数。

3. 拷贝构造(copy (3))

cpp 复制代码
set (const set& x);
  • 作用 :创建一个与已有set对象x内容完全相同的新set

4. 初始化列表(C++11)

cpp 复制代码
void test_set1()
{
    set<int> s = { 5,1,5,3,4,2,6,83,9,10,22 };
    // 中序,排序+去重
    set<int>::iterator it = s.begin();
    while (it != s.end())
    {
        // 普通迭代器也不支持修改
        // *it = 1;
        
        cout << *it << " ";
        ++it;
    }
    cout << endl;
}

2.3 修改器(Modifiers)的成员函数

这是C++ std::set修改器(Modifiers)成员函数 ,负责对set的元素进行增删等操作。以下结合代码示例逐一讲解:

0. 迭代器

这个太基础了 我个人感觉实在没什么可以说的 唯一要注意的就是 不能通过迭代器修改里面的值。

1. insert:插入元素

功能 :向set中插入键值(自动去重、按规则排序)。
代码示例

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

int main() {
    set<int> s;
    // 插入单个元素
    s.insert(3);
    s.insert(1);
    s.insert(2);
    s.insert(2); // 重复元素,插入失败(set自动去重)

    // 遍历输出:1 2 3(默认升序)
    for (int val : s) cout << val << " ";
    return 0;
}

2. erase:删除元素

功能 :删除set中的元素(支持按键值、迭代器、范围删除)。
代码示例

cpp 复制代码
int main() {
    set<int> s = {1,2,3,4,5};
    // 1. 按键值删除
    s.erase(3); 
    // 2. 按迭代器删除
    auto it = s.find(4);
    if (it != s.end()) s.erase(it);
    // 3. 按范围删除(删除[begin, end))
    s.erase(s.begin(), s.end()); //左闭右开

    cout << s.size(); // 输出0
    return 0;
}

3. swap:交换两个set的内容(与算法库swap对比)

功能 :交换当前set与另一个set的所有元素(底层仅交换内部指针,效率高,而算法库swap则涉及深层拷贝等)。
代码示例

cpp 复制代码
int main() {
    set<int> s1 = {1,2,3};
    set<int> s2 = {4,5,6};
    s1.swap(s2);

    // s1变为{4,5,6},s2变为{1,2,3}
    for (int val : s1) cout << val << " "; // 输出4 5 6
    return 0;
}

4. clear:清空所有元素

功能 :删除set中的所有元素,使其变为空容器。
代码示例

cpp 复制代码
int main() {
    set<int> s = {1,2,3};
    s.clear();
    cout << s.empty(); // 输出1(表示容器为空)
    return 0;
}

5. emplace:构造并插入元素(C++11+)

功能 :直接在set中构造元素(避免临时对象拷贝,比insert更高效)。
代码示例

cpp 复制代码
int main() {
    set<pair<int, string>> s;
    // emplace直接构造pair(无需手动创建临时pair)
    s.emplace(1, "apple"); 
    // 等价于insert,但emplace更高效
    s.insert(pair<int, string>(2, "banana"));

    return 0;
}

6. emplace_hint:带位置提示的构造插入(C++11+)

功能 :在指定迭代器位置附近构造并插入元素(若位置合理,可提升插入效率)。
代码示例

cpp 复制代码
int main() {
    set<int> s = {1,3,5};
    // 提示在3的位置附近插入2(实际插入到1和3之间)
    auto it = s.find(3);
    s.emplace_hint(it, 2);

    for (int val : s) cout << val << " "; // 输出1 2 3 5
    return 0;
}

2.4 find(与算法库find的对比)

这是C++标准库中std::set::find成员函数的声明(支持C++98及以上版本),其核心信息与使用说明如下:

cpp 复制代码
iterator find (const value_type& val) const;
  • 返回值iteratorset的迭代器),指向找到的键值val;若val不存在,返回set::end()(尾后迭代器)。
  • 参数const value_type& val,待查找的键值(value_typeset的关键字类型)。
  • 特性const修饰表示该函数不会修改set本身。

findset查找接口 ,基于底层红黑树的特性,能以 O ( log ⁡ N ) O(\log N) O(logN)的时间复杂度快速定位键值,常用于判断元素是否存在、获取元素迭代器。

  • 效率:由于set底层是有序的红黑树,find通过二分查找逻辑实现,效率远高于算法库的find( O ( N ) O(N) O(N))。
  • 迭代器特性:set的迭代器是双向迭代器,且不可修改(因为修改键值会破坏set的有序性)。
cpp 复制代码
#include <set>
#include <iostream>
using namespace std;

int main() {
    set<int> s = {1, 2, 3, 4, 5};
    
    // 查找键值3
    auto it = s.find(3);
    if (it != s.end()) {
        cout << "找到元素:" << *it << endl; // 输出"找到元素:3"
    }

    // 查找不存在的键值6
    it = s.find(6);
    if (it == s.end()) {
        cout << "未找到元素" << endl; // 输出"未找到元素"
    }

    return 0;
}

2.5 key_comp && value_comp

函数名 功能描述
key_comp 返回set用于比较**键(key)**的函数对象,是set模板参数中指定的比较类型(默认是less<key_type>)。
value_comp 功能与key_comp一致(因为set的键和值是同一类型),返回的比较对象逻辑等同于key_comp

set的底层红黑树依赖比较规则维持有序性,这两个函数可以获取当前set使用的比较逻辑,常用于:

  1. 自定义比较规则时,验证或复用set的排序逻辑;
  2. set的元素进行外部排序(保持与set内部一致的规则)。

代码示例

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

int main() {
    // 定义一个按降序排序的set
    set<int, greater<int>> s = {3, 1, 2};

    // 获取key_comp比较对象
    auto comp = s.key_comp();

    // 使用comp判断两个键的大小关系(符合set的降序规则)
    bool res = comp(1, 2); // 等价于greater<int>()(1,2),结果为false
    cout << "1 > 2 ? " << boolalpha << res << endl; // 输出"1 > 2 ? false"

    return 0;
}

2.6 count(与find比较)

  • 声明size_type count (const value_type& val) const;
  • 功能 :统计set中值为val的元素个数(由于set不允许重复元素,返回值只能是01)。
  • 参数const value_type& val,待统计的目标值;
  • 返回值size_type(无符号整数类型),表示valset中的出现次数。

由于set的"唯一性"特性,count的实际作用是判断元素是否存在 (返回1表示存在,0表示不存在),效果等价于find(val) != end(),但语义更偏向"计数"。

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

int main() {
    set<int> s = {1, 2, 3, 4};

    // 统计存在的元素
    size_t cnt1 = s.count(3);
    cout << "元素3的出现次数:" << cnt1 << endl; // 输出1

    // 统计不存在的元素
    size_t cnt2 = s.count(5);
    cout << "元素5的出现次数:" << cnt2 << endl; // 输出0

    return 0;
}

与find的区别

函数 功能 返回值类型 适用场景
find 查找元素并返回迭代器 迭代器 需要获取元素的位置时
count 统计元素出现次数 无符号整数 仅需判断元素是否存在时

综合对比 在判断元素是否存在时 count 更加方便!

2.7 lower_bound && upper_bound

函数名 功能描述
lower_bound 返回指向**第一个不小于(≥)目标值val**的元素的迭代器;若所有元素都小于val,返回end()
upper_bound 返回指向**第一个大于(>)目标值val**的元素的迭代器;若所有元素都不大于val,返回end()

由于set是有序容器(默认升序),这两个函数通过二分查找(时间复杂度 O ( log ⁡ N ) O(\log N) O(logN))快速定位边界,常用于获取"等于val的元素区间"([lower_bound, upper_bound))。

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

int main() {
    set<int> s = {1, 3, 5, 7, 9};
    int val = 5;

    // 获取lower_bound:第一个≥5的元素(即5)
    auto lb = s.lower_bound(val);
    cout << "lower_bound(" << val << "): " << *lb << endl; // 输出5

    // 获取upper_bound:第一个>5的元素(即7)
    auto ub = s.upper_bound(val);
    cout << "upper_bound(" << val << "): " << *ub << endl; // 输出7

    // 若val不存在(如val=4)
    val = 4;
    lb = s.lower_bound(val); // 第一个≥4的元素是5
    ub = s.upper_bound(val); // 第一个>4的元素是5
    cout << "val=4时,[lb, ub)区间长度:" << distance(lb, ub) << endl; // 输出0(无元素)

    return 0;
}

用处: 删除或者遍历 某一区间的值:

3. multiset

multisetset,核心差异集中在元素唯一性、接口行为、使用场景 三个维度,但是multiset不用单独包含头文件,二者基本完全类似。

3.1 核心特性差异

维度 set multiset
元素唯一性 不允许重复元素(键唯一) 允许重复元素(键可重复)
底层实现 红黑树(平衡二叉搜索树) 红黑树(平衡二叉搜索树)
有序性 元素按键有序排列 元素按键有序排列

3.2 接口行为差异

以常用成员函数为例,两者行为因"唯一性"产生区别:

接口 set的行为 multiset的行为
insert 插入重复元素时返回失败(仅插入一次) 插入重复元素时成功(可插入多次)
find 返回第一个匹配键的迭代器 返回第一个匹配键的迭代器(切记是中序遍历的第一个)
count 返回0或1(仅表示存在性) 返回键的实际出现次数
lower_bound/upper_bound 区间[lb, ub)长度最多为1 区间[lb, ub)包含所有匹配键的元素
erase 按键删除时,删除所有匹配的元素(仅1个) 按键删除时,删除所有匹配的元素(可能多个)

3.3 使用场景差异

  • set:适用于需要唯一键的场景(如存储不重复的ID、去重后的数据集);
  • multiset:适用于需要统计键出现次数的场景(如统计单词频率、存储可重复的有序数据)。
cpp 复制代码
#include <set>
#include <iostream>
using namespace std;

int main() {
    // set:键唯一
    set<int> s = {1, 2, 2, 3};
    cout << "set大小:" << s.size() << endl; // 输出3(自动去重)

    // multiset:键可重复
    multiset<int> ms = {1, 2, 2, 3};
    cout << "multiset大小:" << ms.size() << endl; // 输出4(保留重复)

    // count接口差异
    cout << "set中2的数量:" << s.count(2) << endl; // 输出1
    cout << "multiset中2的数量:" << ms.count(2) << endl; // 输出2

    return 0;
}

4. 例题部分

4.1 环形链表 II

题目链接: 点此跳转


我们之前C语言阶段是使用快慢指针完成的 其实我们可以用ste<Node*> s来做 遍历链表,每个节点是否在s中,不在就插入,在的第一个点就是入口点 , 要说这道题唯一的缺陷 就是有 O ( N ) O(N) O(N)的空间复杂度。

cpp 复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode* head) 
    {
        set<ListNode*> s;
        ListNode* tmp=head;
        while(tmp!=NULL)
        {
            if(s.count(tmp)==0)
            {
                s.insert(tmp);
                tmp=tmp->next;
            }
            else
            {
                return tmp;
            }
        }
        return NULL;
    }
};

4.2 两个数组的交集

题目链接: 点此转跳

这题也挺简单的 其实就是拿set去重就可以了 然后用一个set的count去遍历另一个set 把重复的插入vector即可。

cpp 复制代码
class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2)
    {
     //去重 
     set<int> s1(nums1.begin(),nums1.end());  
     set<int> s2(nums2.begin(),nums2.end());  
     vector<int> v;
     for(auto e : s1)
     {
        if(s2.count(e))
        {
            v.push_back(e);
        }
     }
     return v;
    }
};

补充

这么做 其实复杂度还是高的 因为count的特性 时间复杂度可能要 O ( N ∗ l o g N ) O(N*logN) O(N∗logN)

但是利用双指针特性 就可以把复杂度压缩到 O ( N ) O(N) O(N)。这个方法不光能找交集,也能找差集。

相关推荐
ss2732 小时前
阻塞队列:生产者-消费者模式
java·开发语言
八个程序员2 小时前
c++常见问题1——跳出代码
开发语言·c++
初晴や2 小时前
第一章:计算机基础知识
c++·计算机基础知识、
Kiri霧2 小时前
Go 字符串格式化
开发语言·后端·golang
暗然而日章2 小时前
C++基础:Stanford CS106L学习笔记 10 函数模板(Function Templates)
c++·笔记·学习
小年糕是糕手2 小时前
【C++同步练习】内存管理
开发语言·jvm·数据结构·c++·程序人生·算法·改行学it
Dev7z2 小时前
基于MATLAB的5G通信信号频谱分析与信道性能仿真研究
开发语言·5g·matlab
不会代码的小猴2 小时前
C++的第十五天笔记
数据结构·c++·笔记
愚润求学2 小时前
【C++11】并发库
c++