C++ set 容器

55 阅读7分钟

set 是 C++ STL 中的关联式容器,核心特性是「有序、唯一」—— 存储的元素自动按升序排列,且不允许重复值。底层基于红黑树(平衡二叉搜索树)实现,保证插入、删除、查找操作的时间复杂度均为 O(logn),是处理「有序唯一集合」的首选容器。

一、核心特性

特性说明
有序性默认按元素值升序排列,可自定义排序规则(如降序、结构体自定义排序)
唯一性容器内无重复元素,插入重复值会被忽略(insert 返回值可判断是否插入成功)
底层实现红黑树(平衡二叉搜索树),避免 BST 退化,保证操作效率
迭代器类型双向迭代器(bidirectional iterator),支持 ++/--,不支持随机访问(如 it+3
内存与迭代器插入 / 删除元素时,除被删除元素的迭代器外,其他迭代器不失效
无下标访问没有 [] 运算符和 at() 方法,只能通过迭代器遍历 / 访问元素

二、基础使用(必掌握)

1. 头文件与命名空间

使用 set 必须包含头文件 <set>,且依赖 std 命名空间:

#include <iostream>
#include <set> // 核心头文件
using namespace std;

2. 初始化(常见方式)

// 方式1:空set(默认升序)
set<int> s1;

// 方式2:初始化列表(C++11及以上)
set<int> s2 = {3, 1, 4, 2}; // 自动排序为 {1,2,3,4}

// 方式3:迭代器范围初始化
int arr[] = {5, 3, 7};
set<int> s3(arr, arr + sizeof(arr)/sizeof(int)); // {3,5,7}

// 方式4:自定义排序(降序)
set<int, greater<int>> s4 = {3,1,4,2}; // {4,3,2,1}

3. 核心成员函数(高频)

(1)插入元素:insert()

  • 插入成功:返回 pair<iterator, bool>bool 为 trueiterator 指向插入位置;
  • 插入失败(重复):bool 为 falseiterator 指向已存在的重复元素。
set<int> s;
// 插入单个元素
auto res1 = s.insert(3); // res1.second = true,s={3}
auto res2 = s.insert(3); // res2.second = false,res2.first 指向已有的3

// 批量插入(C++11)
s.insert({1, 2, 4}); // s={1,2,3,4}

(2)查找元素:find()

  • 找到:返回指向该元素的迭代器;
  • 未找到:返回 s.end()(尾后迭代器,不指向任何元素)。
set<int> s = {1,2,3,4};
auto it = s.find(3);
if (it != s.end()) {
    cout << "找到元素:" << *it << endl; // 输出:找到元素:3
}

it = s.find(5);
if (it == s.end()) {
    cout << "未找到元素5" << endl;
}

(3)删除元素:erase()

支持三种删除方式,需注意迭代器失效问题:

set<int> s = {1,2,3,4,5};

// 方式1:按值删除(返回删除的元素个数,set中只能是0或1)
int cnt = s.erase(3); // cnt=1,s={1,2,4,5}

// 方式2:按迭代器删除(删除单个元素)
auto it = s.find(4);
if (it != s.end()) {
    s.erase(it); // s={1,2,5}
}

// 方式3:按迭代器范围删除(删除[first, last)区间)
s.erase(s.begin(), s.find(5)); // 删除1、2,s={5}

(4)遍历元素

因无下标访问,只能通过迭代器范围 for(C++11)  遍历:

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

// 方式1:普通迭代器(升序)
for (set<int>::iterator it = s.begin(); it != s.end(); ++it) {
    cout << *it << " "; // 输出:1 2 3 4
}
cout << endl;

// 方式2:范围for(简化版)
for (int val : s) {
    cout << val << " "; // 输出:1 2 3 4
}
cout << endl;

// 方式3:反向迭代器(降序)
for (set<int>::reverse_iterator it = s.rbegin(); it != s.rend(); ++it) {
    cout << *it << " "; // 输出:4 3 2 1
}
cout << endl;

(5)其他常用函数

set<int> s = {1,2,3,4};
s.size();       // 返回元素个数:4
s.empty();      // 判断是否为空:false
s.clear();      // 清空所有元素,s变为空
s.count(2);     // 统计元素个数(set中只能是0或1):1
s.lower_bound(2); // 返回第一个≥2的元素迭代器(指向2)
s.upper_bound(2); // 返回第一个>2的元素迭代器(指向3)

三、进阶用法

1. 自定义排序规则

默认 set 按 < 升序排列,可通过「函数对象」或「lambda 表达式(C++11)」修改排序规则:

(1)降序排列

// 方式1:使用STL自带的greater
set<int, greater<int>> s = {3,1,4,2};
for (int val : s) {
    cout << val << " "; // 输出:4 3 2 1
}

// 方式2:自定义函数对象
struct MyCompare {
    bool operator()(int a, int b) const {
        return a > b; // 降序:a>b时,a排在b前面
    }
};
set<int, MyCompare> s2 = {3,1,4,2}; // 同样输出4 3 2 1

(2)自定义结构体排序

存储结构体时,必须显式定义排序规则(否则编译器无法比较):

// 定义结构体:存储学生姓名和分数
struct Student {
    string name;
    int score;
    // 构造函数
    Student(string n, int s) : name(n), score(s) {}
};

// 自定义排序规则:按分数降序,分数相同按姓名升序
struct CompStudent {
    bool operator()(const Student& a, const Student& b) const {
        if (a.score != b.score) {
            return a.score > b.score;
        }
        return a.name < b.name;
    }
};

int main() {
    set<Student, CompStudent> s;
    s.insert(Student("Alice", 90));
    s.insert(Student("Bob", 85));
    s.insert(Student("Charlie", 90));

    // 遍历:Alice(90) → Charlie(90) → Bob(85)
    for (const auto& stu : s) {
        cout << stu.name << " : " << stu.score << endl;
    }
    return 0;
}

2. set 的变体容器

(1)multiset:允许重复元素

multiset 与 set 几乎一致,唯一区别是允许存储重复元素

  • insert 始终成功(无返回 bool 的重载);
  • count(val) 可返回重复元素的个数;
  • erase(val) 会删除所有值为 val 的元素;
  • 底层仍为红黑树,操作复杂度 O(logn)

示例:

multiset<int> ms = {1,2,2,3,3,3};
cout << ms.count(2) << endl; // 输出:2
ms.erase(2); // 删除所有2,ms={1,3,3,3}
auto it = ms.find(3);
if (it != ms.end()) {
    ms.erase(it); // 仅删除第一个3,ms={1,3,3}
}

(2)unordered_set:无序唯一集合

unordered_set 是「哈希表」实现的无序容器,核心特点:

  • 无序性(不排序,存储顺序随机);
  • 唯一性(无重复元素);
  • 查找 / 插入 / 删除平均 O(1) 复杂度(哈希冲突时退化为 O(n));
  • 不支持自定义排序(因无序),仅支持默认哈希规则(可自定义哈希函数)。

对比 set 和 unordered_set

特性setunordered_set
底层实现红黑树哈希表
有序性升序(可自定义)无序
查找复杂度O(logn)平均 O (1),最坏 O (n)
迭代器双向迭代器正向迭代器
适用场景需要有序集合高频查找,无需排序

示例:

#include <unordered_set>
unordered_set<int> us = {3,1,4,2};
for (int val : us) {
    cout << val << " "; // 输出顺序随机,如:1 3 2 4
}
cout << us.find(2) != us.end() << endl; // 输出:1(找到)

四、使用注意事项

  1. 迭代器限制

    • set 的迭代器是「const 迭代器」(逻辑上):即使声明为非 const 迭代器,也不能修改元素值(因为修改会破坏红黑树的有序性);
    • 不支持随机访问,如 it + 2 会编译报错,只能通过 ++/-- 移动。
  2. 插入重复元素

    • set.insert(val) 返回 pair<iterator, bool>,可通过 second 判断是否插入成功;
    • 若需存储重复元素,直接用 multiset,而非手动判断后插入。
  3. 效率对比

    • 若仅需「去重」且无需有序,优先用 unordered_set(查找更快);
    • 若需「有序 + 去重」,用 set
    • 若需「有序 + 允许重复」,用 multiset
  4. 自定义类型的要求

    • 存储自定义结构体 / 类时,必须提供排序规则(set/multiset)或哈希规则(unordered_set);
    • 排序规则的函数对象必须是「可调用的」且返回 bool,参数为 const 引用(避免拷贝)。

五、典型使用场景

  1. 去重 + 有序:如统计数组中不重复的元素,并按升序输出;
  2. 有序集合查询:如查找某个值是否存在、找大于 / 小于某个值的第一个元素;
  3. 范围查询:结合 lower_bound()/upper_bound() 实现区间查询(如找 [2,5) 范围内的元素);
  4. 替代手动实现红黑树:无需自己维护平衡二叉树,直接用 set 完成有序集合操作。

六、总结

容器类型核心特点适用场景
set有序、唯一、O (logn)有序去重、范围查询
multiset有序、允许重复、O (logn)有序可重复集合
unordered_set无序、唯一、O (1)高频查找、仅需去重无需有序

set 是 C++ 中处理「有序唯一集合」的核心容器,掌握其特性和变体的区别,能大幅简化有序集合的开发工作,避免重复造轮子(如手动实现平衡二叉树)。