C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++ set和multiset

C++ set和multiset的使用小结

作者:Fcy648

本文介绍了C++中序列式容器和关联式容器的区别,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

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类的介绍

2.2 set的构造和迭代器

0.构造:

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

1. 空构造(empty (1))

explicit set (const key_compare& comp = key_compare(),
               const allocator_type& alloc = allocator_type());

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

template <class InputIterator>
set (InputIterator first, InputIterator last,
     const key_compare& comp = key_compare(),
     const allocator_type& alloc = allocator_type());

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

set (const set& x);

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

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中插入键值(自动去重、按规则排序)。
代码示例

#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中的元素(支持按键值、迭代器、范围删除)。
代码示例

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则涉及深层拷贝等)。
代码示例

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中的所有元素,使其变为空容器。
代码示例

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

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

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

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+)

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

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及以上版本),其核心信息与使用说明如下:

iterator find (const value_type& val) const;

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

#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内部一致的规则)。

代码示例

#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比较)

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

#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))。

#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 核心特性差异

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

3.2 接口行为差异

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

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

3.3 使用场景差异

#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)的空间复杂度。

/**
 * 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即可。

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

到此这篇关于C++ set和multiset的使用小结的文章就介绍到这了,更多相关C++ set和multiset内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文