C++ STL | set、multiset

目录

关联式存储

键值对

树形结构的关联式存储

set

介绍

member_types:

set的使用

set的模板参数列表

set的构造

赋值运算符重载

set迭代器

set的容量

set修改操作

find(查找元素)

swap(交换内容)

clear(清除内容)

insert(插入元素)

erase(删除元素)

emplace系列(直接构造元素并插入)

[关键概念:参数包Args&&... args](#关键概念:参数包Args&&... args)

multiset

介绍

差异

使用

核心差异的代码示例验证

需要额外注意的是:

[那么,为什么 multiset 能存重复值?](#那么,为什么 multiset 能存重复值?)

使用场景选择


关联式存储

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

关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是**<key, value>结构的键值对** ,在数据检索时比序列式容器效率更高。关联式容器逻辑结构通常是非线性结构, 两个位置有紧密的关联关系,交换一下,其存储结构就被破坏了。顺序容器中的元素是按关键字来保存和访问的。关联式容器有 map / set 系列和 unordered_map / unordered_set 系列。


键值对

键值对是 用来表示具有一一对应关系 的一种结构,该结构中一般只包含两个成员变量****key和value ,key代表键值,value表示与key****对应的信息。比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。

SGI-STL中关于键值对的定义:

cpp 复制代码
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)
	{}
};

树形结构的关联式存储

根据应用场景的不桶, STL 总共实现了两种 不同结构的管理式容器:树型结构与哈希结构树型结
构的关联式容器主要有四种: map set multimap multiset 。这四种容器的共同点是:使
平衡搜索树 (即红黑树) 作为其底层结果,容器中的元素是一个有序的序列。下面一依次介绍每一
个容器。


set

介绍

cplusplus中关于set的介绍:cplusplus.com/reference/set/set/?kw=set

翻译过来就是:

  1. set是按照一定次序存储元素的容器
  2. 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。 set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
  3. 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行 排序。
  4. set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对 子集进行直接迭代。
  5. set在底层是用二叉搜索树(红黑树)实现的。

注意:

  1. 与 map/multimap 不同, map/multimap 中存储的是真正的键值对 <key, value> , set 中只放
    value ,但在底层实际存放的是由 <value, value> 构成的键值对。
  2. set 中插入元素时,只需要插入 value 即可,不需要构造键值对。
  3. set 中的元素不可以重复 ( 因此可以使用 set 进行去重 ) 。
  4. 使用 set 的迭代器遍历 set 中的元素,可以得到有序序列
  5. set 中的元素默认按照小于来比较
  6. set中查找某个元素,时间复杂度为:O( log₂N)
  7. set 中的元素不允许修改
  8. set 中的底层使用二叉搜索树 ( 红黑树 ) 来实现。

member_types:

set的使用

set的模板参数列表

  • T: set中存放元素的类型,实际在底层存储<value, value>的键值对。
  • Compare:set中元素默认按照小于来比较
  • Alloc:set中元素空间的管理方式,使用STL提供的空间配置器管理

set的构造

复制代码
explicit set (const key_compare& comp = key_compare(),
              const allocator_type& alloc = allocator_type());
  • 这是默认构造函数,创建一个空的std::set容器。
  • 参数comp用于指定比较函数,它用于确定元素的顺序。默认情况下,使用key_compare(),即容器类型中指定的比较函数。
  • 参数alloc用于指定分配器,用于管理内存。默认情况下,使用allocator_type(),即容器类型中指定的分配器。
cpp 复制代码
#include <iostream>
#include <set>

int main() {
    // 使用默认构造函数创建一个空的 set
    std::set<int> mySet;
    
    // 向 set 中插入元素
    mySet.insert(5);
    mySet.insert(2);
    mySet.insert(8);

    // 输出 set 的内容
    for (const int& value : mySet) {
        std::cout << value << " ";
    }
    std::cout << std::endl;

    return 0;
}
复制代码
template <class InputIterator>
set (InputIterator first, InputIterator last,
     const key_compare& comp = key_compare(),
     const allocator_type& = allocator_type());
  • 这是范围构造函数,创建一个std::set容器,并初始化其内容,从范围 [first, last) 中的元素。
  • 参数firstlast是迭代器,用于指定范围。容器将包含该范围内的元素。
  • 参数compalloc与默认构造函数中的含义相同,用于指定比较函数和分配器。
cpp 复制代码
#include <iostream>
#include <set>
#include <string>
#include <vector>

// 自定义仿函数:按字符串长度降序排序
struct StrLenCompare {
    bool operator()(const std::string& a, const std::string& b) const {
        return a.length() > b.length();
    }
};

int main() {
    std::vector<std::string> str_vec = {"apple", "banana", "pear", "orange", "grape"};
    
    // 使用自定义比较函数构造set
    std::set<std::string, StrLenCompare> s(str_vec.begin(), str_vec.end(), StrLenCompare());
    
    // 遍历输出:按字符串长度降序(banana/orange(6) → apple/grape(5) → pear(4))
    std::cout << "按字符串长度降序的set:";
    for (const std::string& str : s) {
        std::cout << str << " ";  // 输出:banana orange apple grape pear
    }
    std::cout << std::endl;
    
    return 0;
}
复制代码
set (const set& x)
  • 这是拷贝构造函数,创建一个新的std::set容器,并使用另一个set容器x的内容初始化它。
  • 这个构造函数用于创建一个副本,将x中的所有元素复制到新容器中。
cpp 复制代码
#include <iostream>
#include <set>

int main() {
    // 创建一个 set 并初始化
    std::set<int> set1 = {1, 2, 3};

    // 使用拷贝构造函数创建一个新的 set,复制 set1 的内容
    std::set<int> set2(set1);

    // 输出 set2 的内容
    for (const int& value : set2) {
        std::cout << value << " ";
    }
    std::cout << std::endl;

    return 0;
}

赋值运算符重载

复制代码
set& operator= (const set& x);
  • 拷贝另一个set的所有元素(深拷贝),替换当前set的内容,原set内容不受影响
  • 赋值前会先清空当前set的所有元素,释放原有内存,再替换为新内容
cpp 复制代码
#include <iostream>
#include <set>
using namespace std;

int main()
{
    std::set<int> set1 = {1, 2, 3};
    std::set<int> set2;
    
    // 使用拷贝复制将 set1 的内容复制到 set2
    set2 = set1;
    std::cout << "拷贝赋值后s2:";
    for (int num : s2) std::cout << num << " ";  // 输出:1 2 3

    return 0;
}
复制代码
set& operator= (set&& x);
  • 接管另一个set的资源(浅拷贝 + 资源转移),效率更高,源set会变成空,但是仍然可用,无内存拷贝开销
  • 赋值前会先清空当前set的所有元素,释放原有内存,再替换为新内容
cpp 复制代码
#include <iostream>
#include <set>

int main() {
    // 准备基础数据
    std::set<int> s1 = {1, 3, 5, 7};
    std::set<int> s2;

    // 2. 移动赋值(operator=(set&&))
    // std::move将s1转为右值引用,触发移动赋值
    s1= std::move(s1);
    std::cout << "移动赋值后s1:";
    for (int num : s1) std::cout << num << " ";  // 输出:1 3 5 7
    std::cout << "\n移动后s1是否为空:" << s1.empty() << "\n";  // true(s1的资源被转移)


    return 0;
}

这里提供模拟实现赋值拷贝与移动赋值的代码帮助理解

cpp 复制代码
    BSTNode<T>* copy_tree(BSTNode<T>* node) const {
        if (node == nullptr) return nullptr;
        // 先拷贝当前节点,再递归拷贝左右子树
        BSTNode<T>* new_node = new BSTNode<T>(node->val);
        new_node->left = copy_tree(node->left);
        new_node->right = copy_tree(node->right);
        return new_node;
    }

    // 辅助函数:递归销毁二叉树
    void destroy_tree(BSTNode<T>* node) {
        if (node == nullptr) return;
        destroy_tree(node->left);
        destroy_tree(node->right);
        delete node;
    }
    

    // 版本1:拷贝赋值运算符(深拷贝)
    set& operator=(const set& other) {
        // 【重要】自赋值检查:防止s = s导致内存错误
        if (this == &other) {
            return *this;
        }

        // 步骤1:销毁当前对象的所有资源(清空原有数据)
        destroy_tree(root);
        root = nullptr;
        size_ = 0;

        // 步骤2:深拷贝源对象的二叉树
        root = copy_tree(other.root);
        size_ = other.size_;

        // 步骤3:返回自身引用,支持链式赋值
        return *this;
    }

    // 版本2:移动赋值运算符(资源转移,noexcept保证不抛异常)
    set& operator=(set&& other) noexcept {
        // 自赋值检查
        if (this == &other) {
            return *this;
        }

        // 步骤1:销毁当前对象的资源
        destroy_tree(root);

        // 步骤2:转移源对象的资源(浅拷贝指针)
        root = other.root;
        size_ = other.size_;

        // 步骤3:源对象置空,避免析构时重复释放
        other.root = nullptr;
        other.size_ = 0;

        return *this;
    }

set迭代器

std::set的迭代器是双向迭代器 (Bidirectional Iterator),不支持随机访问(不能用it + 3it -= 2这类操作),核心特性如下:

特性 说明
迭代器类型 std::set<T>::iterator(可读写,但set元素不可修改)、std::set<T>::const_iterator(只读)
遍历方向 支持++it(向后遍历,升序)、--it(向前遍历,降序)
元素特性 迭代器指向的元素是const的(set的键不可修改,否则破坏排序规则)
底层关联 绑定到红黑树的节点,遍历本质是红黑树的中序遍历(保证升序输出)
失效规则 插入元素:所有迭代器仍有效;删除元素:仅指向被删元素的迭代器失效,其余仍有效

以下是常见关于set迭代器使用的函数:

  • begin:返回一个迭代器,指向std::set容器中第一个元素的位置。
  • end:返回一个迭代器,指向std::set容器中超出最后一个元素的位置。
  • rbegin:返回一个反向迭代器,指向std::set容器中最后一个元素的位置。使用反向迭代器可以逆序遍历容器。
  • rend:返回一个反向迭代器,指向std::set容器中超出第一个元素的位置。它通常与rbegin一起使用,以定义逆序遍历的结束点。
  • cbegin:返回一个常量迭代器,指向std::set容器中第一个元素的位置。常量迭代器用于遍历容器并防止修改容器中的元素。
  • cend:返回一个常量迭代器,指向std::set容器中超出最后一个元素的位置。
  • crbegin:返回一个常量反向迭代器,指向std::set容器中最后一个元素的位置。常量反向迭代器用于逆序遍历容器,并防止修改容器中的元素。
  • crend:返回一个常量反向迭代器,指向std::set容器中超出第一个元素的位置。通常与crbegin一起使用,以定义逆序遍历的结束点。
cpp 复制代码
#include <iostream>
#include <set>

int main() {
    // 创建一个 std::set 容器并初始化
    std::set<int> mySet = {5, 2, 8, 1, 9};

    // 使用 begin 和 end 迭代器遍历容器
    std::cout << "正序遍历:" << std::endl;
    for (std::set<int>::iterator it = mySet.begin(); it != mySet.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    // 使用 rbegin 和 rend 反序遍历容器
    std::cout << "逆序遍历:" << std::endl;
    for (std::set<int>::reverse_iterator rit = mySet.rbegin(); rit != mySet.rend(); ++rit) {
        std::cout << *rit << " ";
    }
    std::cout << std::endl;

    // 使用 cbegin 和 cend 常量迭代器遍历容器
    std::cout << "使用常量迭代器:" << std::endl;
    for (std::set<int>::const_iterator cit = mySet.cbegin(); cit != mySet.cend(); ++cit) {
        std::cout << *cit << " ";
    }
    std::cout << std::endl;

    // 使用 crbegin 和 crend 常量反序遍历容器
    std::cout << "使用常量反序迭代器:" << std::endl;
    for (std::set<int>::const_reverse_iterator crit = mySet.crbegin(); crit != mySet.crend(); ++crit) {
        std::cout << *crit << " ";
    }
    std::cout << std::endl;

    return 0;
}


/*

正序遍历:
1 2 5 8 9 
逆序遍历:
9 8 5 2 1 
使用常量迭代器:
1 2 5 8 9 
使用常量反序迭代器:
9 8 5 2 1 

*/

set的容量

复制代码
bool empty() const noexcept;

empty函数用于检测set是否为空,若为空则返回true,否则false

cpp 复制代码
std::set<int> mySet;
if (mySet.empty()) {
    std::cout << "容器为空" << std::endl;
} else {
    std::cout << "容器不为空" << std::endl;
}
复制代码
size_type size() const noexcept;

size函数用于查看set的容器大小,即返回set中有效元素的个数

cpp 复制代码
std::set<int> mySet = {1, 2, 3, 4, 5};
std::cout << "容器的大小为: " << mySet.size() << std::endl;

set修改操作

find(查找元素)
复制代码
const_iterator find (const value_type& val) const;
iterator       find (const value_type& val);

返回一个迭代器,指向键等于给定键的元素。如果未找到元素,则返回指向容器末尾的迭代器 end()

swap(交换内容)
复制代码
void swap (set& x);

swap函数用于交换两个std::set容器的内容,使它们互相包含对方的元素。

clear(清除内容)
复制代码
void clear() noexcept;

clear函数用于清除容器中的所有元素,使容器变为空。

count(计算特定值的元素个数)

复制代码
size_type count (const value_type& val) const;

返回具有指定值的元素个数。通常情况下,由于set的键是唯一的,结果将是0或1

insert(插入元素)
  • std::pair<iterator, bool> insert(const value_type& val);
  • iterator insert(iterator position, const value_type& val);
  • template <class InputIterator>
    void insert(InputIterator first, InputIterator last);

set 中插入元素 x ,实际插入的是 <x, x> 构成的 键值对
如果插入成功,返回< 该元素在 set 中的 位置, true>
如果插入失败,说明 x set 中已经 存在,返回 <x set 中的位置, false>
注意,很多人忽视的一个点

insert(position, val)里的position不是 "强制插入位置",而是给set优化提示 ------ 你告诉set:"我觉得val应该插在position这个位置附近"。

set的底层是红黑树,正常插入一个值需要:

  1. 从根节点开始遍历,找到val该插入的正确位置(时间复杂度O(logN));

  2. 插入节点并调整红黑树平衡。

如果你的position提示准确 (比如要插 6,提示位置是 5 的迭代器),set会直接从position开始检查,不用从根节点遍历,插入效率直接降到O(1)(这才是提示的核心价值)。

cpp 复制代码
#include <iostream>
#include <set>
#include <vector>

int main() {
    std::set<int> s = {1, 3, 5};

    // ------------------- 版本1:插入单个值(返回pair) -------------------
    // 插入不存在的值:返回<指向4的迭代器, true>
    auto ret1 = s.insert(4);
    std::cout << "插入4是否成功:" << std::boolalpha << ret1.second << "\n";  // true
    std::cout << "插入4后指向的元素:" << *ret1.first << "\n";  // 4

    // 插入已存在的值:返回<指向3的迭代器, false>
    auto ret2 = s.insert(3);
    std::cout << "插入3是否成功:" << ret2.second << "\n";  // false
    std::cout << "返回的迭代器指向:" << *ret2.first << "\n";  // 3
    std::cout << "版本1插入后set:";
    for (int num : s) std::cout << num << " ";  // 1 3 4 5
    std::cout << "\n";

    // ------------------- 版本2:带位置提示插入 -------------------
    // 提示位置(s.find(4))准确,优化插入效率
    auto pos = s.find(4);  // 提示5的下一个位置是4的右侧
    auto it = s.insert(pos, 6);
    std::cout << "带提示插入6后,迭代器指向:" << *it << "\n";  // 6

    // 提示位置错误(仍能正确插入,仅失去优化效果)
    auto wrong_pos = s.begin();
    auto it2 = s.insert(wrong_pos, 2);
    std::cout << "错误提示插入2后,迭代器指向:" << *it2 << "\n";  // 2
    std::cout << "版本2插入后set:";
    for (int num : s) std::cout << num << " ";  // 1 2 3 4 5 6
    std::cout << "\n";

    // ------------------- 版本3:批量插入迭代器范围 -------------------
    std::vector<int> vec = {7, 8, 2, 9};  // 包含重复值2
    s.insert(vec.begin(), vec.end());  // 批量插入,自动去重
    std::cout << "版本3批量插入后set:";
    for (int num : s) std::cout << num << " ";  // 1 2 3 4 5 6 7 8 9
    std::cout << "\n";

    return 0;
}
erase(删除元素)
  • iterator erase(iterator position);
  • size_type erase(const key_type& key);
  • iterator erase(iterator first, iterator last);

这些成员函数用于从std::set容器中删除元素。您可以提供要删除的元素的位置或键(key)。

erase函数返回一个迭代器,指向被删除元素之后的位置。

语法 核心作用 返回值 迭代器失效规则
iterator erase(const_iterator position); 删除指定迭代器指向的单个元素 指向被删元素下一个元素的迭代器 仅被删元素的迭代器失效,其余有效
size_type erase(const value_type& val); 删除值为val的元素(若存在) 被删除的元素个数(set中只能是 0 或 1) 无(仅删除匹配值的元素,不涉及迭代器)
iterator erase(const_iterator first, const_iterator last); 删除迭代器范围[first, last)内的所有元素 指向last位置的迭代器(即被删范围的下一个元素) [first, last)范围内的迭代器失效,其余有效
cpp 复制代码
#include <iostream>
#include <set>

int main() {
    std::set<int> s = {1, 2, 3, 4, 5, 6, 7};

    // ------------------- 版本1:删除指定迭代器指向的元素 -------------------
    auto it = s.find(3);  // 找到值为3的迭代器
    if (it != s.end()) {
        auto next_it = s.erase(it);  // 删除3,返回指向4的迭代器
        std::cout << "删除3后,返回的迭代器指向:" << *next_it << "\n";  // 输出4
    }
    std::cout << "版本1删除后set:";
    for (int num : s) std::cout << num << " ";  // 1 2 4 5 6 7
    std::cout << "\n";

    // ------------------- 版本2:删除指定值的元素 -------------------
    size_t del_count = s.erase(5);  // 删除值为5的元素
    std::cout << "删除值5的个数:" << del_count << "\n";  // 输出1
    del_count = s.erase(10);        // 删除不存在的元素
    std::cout << "删除值10的个数:" << del_count << "\n";  // 输出0
    std::cout << "版本2删除后set:";
    for (int num : s) std::cout << num << " ";  // 1 2 4 6 7
    std::cout << "\n";

    // ------------------- 版本3:删除迭代器范围的元素 -------------------
    auto first = s.find(2);
    auto last = s.find(6);
    auto end_it = s.erase(first, last);  // 删除[2,6) → 2、4
    std::cout << "删除范围后返回的迭代器指向:" << *end_it << "\n";  // 输出6
    std::cout << "版本3删除后set:";
    for (int num : s) std::cout << num << " ";  // 1 6 7
    std::cout << "\n";

    return 0;
}
emplace系列(直接构造元素并插入)

std::setemplace是 C++11 新增的成员函数,核心作用是:set中直接构造元素并插入,避免临时对象的创建和拷贝 / 移动

emplace 重载版本 语法 核心作用 返回值
基础版 template <class... Args> pair<iterator, bool> emplace(Args&&... args); 用参数args直接在set的正确位置构造元素 insert(val)一致:pair<iterator, bool>first指向元素迭代器,second表示是否插入成功
带位置提示版 template <class... Args> iterator emplace_hint(const_iterator hint, Args&&... args); 带位置提示(优化效率),用args直接构造元素 指向插入 / 已存在元素的迭代器(无bool返回值)
关键概念:参数包Args&&... args

这是 C++11 的可变参数模板,意思是你可以传任意数量、任意类型的参数,这些参数会直接传递给元素的构造函数,比如:

  • 插入std::pair<int, std::string>时,可传1, "hello",而非先构造pair(1, "hello")
  • 插入自定义类对象时,可传类构造函数的参数,而非先创建临时对象。

emplace vs insert:核心区别

操作 insert 方式 emplace 方式 核心差异
插入简单类型(int) s.insert(5); s.emplace(5); 无差异(int 是内置类型,无构造开销)
插入复杂类型(如pair // 步骤1:创建临时pair对象std::pair<int, std::string> temp(1, "test"); // 步骤2:插入(拷贝/移动temp到set)s.insert(temp); // 直接在set里构造pair,无临时对象s.emplace(1, "test"); emplace避免了临时对象的创建和拷贝 / 移动,效率更高

emplace 基础使用示例

cpp 复制代码
#include <iostream>
#include <set>
#include <string>

// 自定义类:演示复杂类型的emplace
class Person {
public:
    std::string name;
    int age;

    // 构造函数(带参数)
    Person(std::string n, int a) : name(std::move(n)), age(a) {
        std::cout << "Person构造函数调用\n";
    }

    // 重载<运算符(set需要排序规则)
    bool operator<(const Person& other) const {
        return age < other.age;
    }
};

int main() {
    // ------------------- 示例1:插入简单类型(int) -------------------
    std::set<int> s1;
    // emplace直接传int值,效果和insert一致
    auto ret1 = s1.emplace(3);
    std::cout << "emplace(3)是否插入成功:" << ret1.second << "\n";  // true
    auto ret2 = s1.emplace(3);  // 重复插入
    std::cout << "重复emplace(3)是否成功:" << ret2.second << "\n";  // false

    // ------------------- 示例2:插入复杂类型(Person) -------------------
    std::set<Person> s2;
    // 方式1:用insert(会创建临时Person对象,触发构造函数)
    std::cout << "\n=== insert方式 ===\n";
    s2.insert(Person("Alice", 25));  // 先构造临时对象,再插入

    // 方式2:用emplace(直接在set里构造,无临时对象)
    std::cout << "\n=== emplace方式 ===\n";
    s2.emplace("Bob", 30);  // 直接传构造参数,触发一次构造

    // ------------------- 示例3:emplace_hint(带位置提示) -------------------
    std::cout << "\n=== emplace_hint方式 ===\n";
    auto hint = s2.find(Person("", 28));  // 提示位置(即使找不到,也不影响)
    auto it = s2.emplace_hint(hint, "Charlie", 28);
    std::cout << "emplace_hint插入的元素年龄:" << it->age << "\n";  // 28

    return 0;
}

multiset

介绍

cplusplus中关于multiset的介绍:cplusplus.com/reference/set/multiset/?kw=multiset

翻译过来就是:

  1. multiset是按照特定顺序存储元素的容器,其中元素是可以重复的。
  2. 在multiset中,元素的value也会识别它(因为multiset中本身存储的就是<value, value>组成
  3. 的键值对,因此value本身就是key,key就是value,类型为T). multiset元素的值不能在容器中进行修改(因为元素总是const的),但可以从容器中插入或删除。
  4. 在内部,multiset中的元素总是按照其内部比较规则(类型比较)所指示的特定严格弱排序准进行排序。
  5. multiset容器通过key访问单个元素的速度通常比unordered_multiset容器慢,但当使用迭代器遍历时会得到一个有序序列。
  6. multiset底层结构为二叉搜索树(红黑树)。

注意的是:

  1. multiset中再底层中存储的是<value, value>的键值对
  2. mtltiset的插入接口中只需要插入即可
  3. 与set的区别是,multiset中的元素可以重复,set是中value是唯一的
  4. 使用迭代器对multiset中的元素进行遍历,可以得到有序的序
  5. multiset中的元素不能修改
  6. 在multiset中找某个元素,时间复杂度为 O(log₂N)
  7. multiset的作用:可以对元素进行排序

差异

setmultiset都属于 STL 关联式容器,底层都是红黑树,核心区别仅在于「是否允许键重复」,其余特性高度一致。

特性 std::set std::multiset
键的唯一性 ✅ 不允许重复键(插入重复值会失败) ❌ 允许重复键(可插入多个相同值)
插入返回值 insert/emplace返回pair<iterator, bool>bool表示是否插入成功 insert/emplace仅返回iterator(无bool,因为总能插入)
erase版本 2 返回值 size_type erase(val)返回 0 或 1(最多删 1 个) size_type erase(val)返回删除的元素个数(可删多个)
查找接口 find(val)返回第一个匹配val的迭代器(无则返回end() find(val)返回第一个匹配val的迭代器(无则返回end()
范围查找 无专属接口(需手动遍历,虽然也有equal_range(val)) 提供equal_range(val),返回<first, last>迭代器对,指向所有val的范围
底层结构 红黑树(每个键唯一) 红黑树(键可重复,排序时相同键相邻)
时间复杂度 插入 / 删除 / 查找均为O(logN) 插入 / 删除 / 查找均为O(logN)

使用

参考set

核心差异的代码示例验证

cpp 复制代码
#include <iostream>
#include <set>
#include <multiset>

int main() {
    // ------------------- 1. 插入重复值的区别 -------------------
    std::set<int> s;
    std::multiset<int> ms;

    // set插入重复值:失败,返回false
    auto ret1 = s.insert(3);
    auto ret2 = s.insert(3);
    std::cout << "set插入第1个3是否成功:" << ret1.second << "\n";  // true
    std::cout << "set插入第2个3是否成功:" << ret2.second << "\n";  // false
    std::cout << "set大小:" << s.size() << "\n";  // 1

    // multiset插入重复值:成功,仅返回迭代器
    ms.insert(3);
    ms.insert(3);
    std::cout << "multiset大小:" << ms.size() << "\n";  // 2

    // ------------------- 2. erase删除指定值的区别 -------------------
    size_t del_cnt1 = s.erase(3);
    std::cout << "set删除3的个数:" << del_cnt1 << "\n";  // 1
    size_t del_cnt2 = ms.erase(3);
    std::cout << "multiset删除3的个数:" << del_cnt2 << "\n";  // 2
    ms.insert({3, 3, 3});  // 重新插入3个3

    // ------------------- 3. multiset的equal_range(专属接口) -------------------
    auto [first, last] = ms.equal_range(3);
    std::cout << "multiset中3的个数:";
    int cnt = 0;
    for (auto it = first; it != last; ++it) cnt++;
    std::cout << cnt << "\n";  // 3

    // ------------------- 4. 遍历对比(排序规则一致) -------------------
    s.insert({1, 2, 4});
    ms.insert({1, 2, 4});
    std::cout << "set遍历:";
    for (int num : s) std::cout << num << " ";  // 1 2 4
    std::cout << "\nmultiset遍历:";
    for (int num : ms) std::cout << num << " ";  // 1 2 3 3 3 4
    std::cout << "\n";

    return 0;
}

需要额外注意的是:

insert/emplace的返回值差异

  • setinsert(val):返回pair<iterator, bool>bool是判断 "是否真的插入" 的关键;
  • multisetinsert(val):仅返回iterator(指向新插入的元素),因为重复值也能插入,无需bool

erase的注意事项

  • 对于multiseterase(val)会删除所有 值为val的元素;若只想删除其中一个,需先find找到迭代器,再erase(it)
cpp 复制代码
// 仅删除multiset中第一个3
auto it = ms.find(3);
if (it != ms.end()) ms.erase(it);

③ 排序规则一致

两者都默认按<升序排序,也可自定义比较器(比如降序),且重复值在multiset中会相邻排列:

cpp 复制代码
// 降序的multiset
std::multiset<int, std::greater<int>> ms_desc = {3,1,4,3,2};
for (int num : ms_desc) std::cout << num << " ";  // 4 3 3 2 1

那么,为什么 multiset 能存重复值?

两者底层都是红黑树,但节点的比较规则不同:

  • set的比较规则:comp(a, b)comp(b, a)都为false时,认为a == b,拒绝插入;
  • multiset的比较规则:仅当comp(a, b)false时,就允许插入(不判断comp(b, a)),因此相同值可共存,且红黑树会将相同值的节点放在相邻位置。

使用场景选择

场景 set multiset
存储唯一值(如 ID、用户名) ✅ 首选 ❌ 不适用
存储可重复值(如成绩、分数、日志等级) ❌ 不适用 ✅ 首选
需要快速去重 + 排序 ✅ 首选 ❌ 去重需额外处理
需要统计相同值的个数 ❌ 需手动遍历统计 ✅ 用equal_range高效统计
插入时需要判断是否已存在 ✅ 用返回值bool判断 ❌ 需额外find检查
相关推荐
一晌小贪欢1 小时前
Python 健壮性进阶:精通 TCP/IP 网络编程与 requirements.txt 的最佳实践
开发语言·网络·python·网络协议·tcp/ip·python基础·python小白
enfpZZ小狗1 小时前
基于C++的反射机制探索
开发语言·c++·算法
曹牧1 小时前
C#:WebReference
开发语言·c#
炽烈小老头1 小时前
【每天学习一点算法 2026/01/22】杨辉三角
学习·算法
黎雁·泠崖1 小时前
Java static入门:概述+静态变量特点与基础实战
java·开发语言
玉梅小洋1 小时前
C盘爆满 修改VS Code缓存与插件目录指定方法
开发语言·windows·visualstudio
MicroTech20251 小时前
微算法科技(NASDAQ :MLGO)量子安全区块链:PQ-DPoL与Falcon签名的双重防御体系
科技·算法·安全
C#程序员一枚1 小时前
C#AsNoTracking()详解
开发语言·c#
努力也学不会java1 小时前
【Spring Cloud】 服务注册/服务发现
人工智能·后端·算法·spring·spring cloud·容器·服务发现