C++ set 全面解析与实战指南

C++ set 全面解析与实战指南

在C++标准模板库(STL)中,set是一种基于红黑树(Red-Black Tree)实现的有序关联容器,其核心特性是自动对元素进行排序且不允许重复元素。相比于vector的动态数组特性,set更擅长有序数据的存储、查找与去重场景。本文将从set的核心特性、常用操作、实现原理、性能分析及实战案例等方面,带你系统掌握set的使用逻辑与底层机制。

一、set 核心特性

set作为有序关联容器,其特性与底层红黑树的实现紧密相关,核心亮点如下,也是它区别于vector、list等序列容器的关键:

  • 自动有序性:set会根据元素的默认比较规则(通常是<运算符)自动对元素进行升序排序,插入元素时无需手动维护顺序。

  • 无重复元素:set不允许存储重复的元素,若尝试插入已存在的元素,插入操作会被忽略(返回的pair对象中second值为false)。

  • 基于红黑树实现:红黑树是一种平衡二叉搜索树,保证了插入、删除、查找等操作的时间复杂度均为O(log n),效率稳定。

  • 不支持随机访问:set的元素存储不连续,无法通过下标直接访问元素,只能通过迭代器进行顺序遍历。

  • 迭代器稳定性:与vector不同,set的迭代器在插入、删除元素时(除被删除元素的迭代器外)不会失效,因为红黑树的节点删除/插入不会导致内存块整体移动。

  • 模板化设计:支持存储任意可比较的类型(基本数据类型、自定义结构体/类,需重载比较运算符),通用性强。

补充:STL中还有multiset容器,其特性与set基本一致,唯一区别是multiset允许存储重复元素,适合需要保留重复有序数据的场景。

二、set 常用操作(C++实现)

使用set需先包含头文件 <set>,并使用std命名空间(或显式指定std::set)。以下是set最常用的操作及对应的代码实现,涵盖初始化、插入、删除、查找等核心场景。

2.1 set 初始化

set提供了多种初始化方式,可适配不同的数据导入场景,具体如下:

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

int main() {
    // 1. 空set,默认升序排序(基于less<int>比较器)
    set<int> s1;
    
    // 2. 用初始化列表初始化(C++11及以上)
    set<int> s2 = {5, 2, 8, 2, 1};  // 自动去重并排序,最终为{1,2,5,8}
    
    // 3. 用迭代器范围初始化(从其他容器导入数据)
    vector<int> vec = {3, 7, 1, 7, 9};
    set<int> s3(vec.begin(), vec.end());  // 去重排序后为{1,3,7,9}
    
    // 4. 自定义排序规则(降序),使用greater比较器
    set<int, greater<int>> s4 = {5, 2, 8, 1};  // 最终为{8,5,2,1}
    
    // 5. 拷贝构造(用另一个set初始化)
    set<int> s5(s2);  // s5是s2的副本,为{1,2,5,8}
    
    // 打印验证
    cout << "s2(默认升序):";
    for (auto it = s2.begin(); it != s2.end(); ++it) {
        cout << *it << " ";  // 输出:1 2 5 8
    }
    cout << endl;
    
    cout << "s4(自定义降序):";
    for (auto x : s4) {
        cout << x << " ";  // 输出:8 5 2 1
    }
    cout << endl;
    
    return 0;
}

2.2 元素插入

set的插入操作会自动完成排序与去重,主要通过insert()方法实现,支持多种插入形式,返回值为pair<iterator, bool>:其中iterator指向插入位置(或已存在元素的位置),bool表示插入是否成功(成功为true,重复插入为false)。

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

int main() {
    set<int> s;
    pair<set<int>::iterator, bool> ret;
    
    // 1. 插入单个元素
    ret = s.insert(5);
    cout << "插入5:" << (ret.second ? "成功" : "失败") << endl;  // 输出:成功
    
    ret = s.insert(5);  // 尝试插入重复元素
    cout << "再次插入5:" << (ret.second ? "成功" : "失败") << endl;  // 输出:失败
    
    // 2. 插入多个元素(初始化列表,C++11及以上)
    s.insert({2, 8, 1});  // 插入后s为{1,2,5,8}
    
    // 3. 插入迭代器范围的元素
    set<int> s2 = {3, 7};
    s.insert(s2.begin(), s2.end());  // 插入后s为{1,2,3,5,7,8}
    
    // 4. 插入到指定位置(hint,提示位置,若提示正确可提高插入效率)
    // 注意:hint仅为提示,最终插入位置仍由排序规则决定
    s.insert(s.begin(), 4);  // 提示在开头插入,但实际插入到3和5之间
    cout << "插入4后:";
    for (auto x : s) {
        cout << x << " ";  // 输出:1 2 3 4 5 7 8
    }
    cout << endl;
    
    return 0;
}

2.3 元素查找与统计

set的查找操作基于红黑树的二分查找特性,效率较高,常用方法有find()、count()、lower_bound()、upper_bound()等:

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

int main() {
    set<int> s = {1, 2, 3, 4, 5, 7, 8};
    
    // 1. find():查找指定元素,返回指向该元素的迭代器;若不存在,返回end()
    auto it = s.find(5);
    if (it != s.end()) {
        cout << "找到元素5:" << *it << endl;  // 输出:找到元素5:5
    } else {
        cout << "未找到元素5" << endl;
    }
    
    it = s.find(6);
    if (it == s.end()) {
        cout << "未找到元素6" << endl;  // 输出:未找到元素6
    }
    
    // 2. count():统计元素出现的次数(set中仅为0或1,multiset中可大于1)
    cout << "元素3出现次数:" << s.count(3) << endl;  // 输出:1
    cout << "元素6出现次数:" << s.count(6) << endl;  // 输出:0
    
    // 3. lower_bound(x):返回第一个大于等于x的元素的迭代器
    auto it_low = s.lower_bound(4);
    cout << "第一个大于等于4的元素:" << *it_low << endl;  // 输出:4
    
    // 4. upper_bound(x):返回第一个大于x的元素的迭代器
    auto it_up = s.upper_bound(4);
    cout << "第一个大于4的元素:" << *it_up << endl;  // 输出:5
    
    // 5. equal_range(x):返回pair<lower_bound(x), upper_bound(x)>,表示x的合法插入范围
    auto range = s.equal_range(4);
    cout << "equal_range(4) 左边界:" << *(range.first) << endl;  // 输出:4
    cout << "equal_range(4) 右边界:" << *(range.second) << endl;  // 输出:5
    
    return 0;
}

2.4 元素删除

set的删除操作支持通过迭代器、元素值、迭代器范围三种方式,删除后红黑树会自动重新平衡,保证结构稳定:

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

int main() {
    set<int> s = {1, 2, 3, 4, 5, 7, 8};
    
    // 1. 通过迭代器删除单个元素
    auto it = s.find(4);
    if (it != s.end()) {
        s.erase(it);  // 删除元素4
    }
    cout << "删除4后:";
    for (auto x : s) cout << x << " ";  // 输出:1 2 3 5 7 8
    cout << endl;
    
    // 2. 通过元素值删除(返回删除的元素个数,set中为0或1)
    size_t del_cnt = s.erase(5);
    cout << "删除5的个数:" << del_cnt << endl;  // 输出:1
    del_cnt = s.erase(6);
    cout << "删除6的个数:" << del_cnt << endl;  // 输出:0
    
    // 3. 通过迭代器范围删除(左闭右开)
    s.erase(s.begin(), s.find(3));  // 删除从开头到第一个小于3的元素(即删除1、2)
    cout << "删除1、2后:";
    for (auto x : s) cout << x << " ";  // 输出:3 7 8
    cout << endl;
    
    // 4. 清空所有元素(clear())
    s.clear();
    cout << "清空后元素个数:" << s.size() << endl;  // 输出:0
    
    return 0;
}

2.5 迭代器遍历

由于set不支持随机访问,遍历只能通过迭代器进行,支持正向、反向、常量三种遍历方式,遍历结果均遵循set的排序规则:

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

int main() {
    set<int> s = {1, 2, 3, 4, 5};
    
    // 1. 正向迭代器(begin()/end()):升序遍历
    cout << "正向升序遍历:";
    for (set<int>::iterator it = s.begin(); it != s.end(); ++it) {
        cout << *it << " ";  // 输出:1 2 3 4 5
    }
    cout << endl;
    
    // 2. 常量正向迭代器(cbegin()/cend()):不可修改元素
    cout << "常量正向遍历:";
    for (set<int>::const_iterator it = s.cbegin(); it != s.cend(); ++it) {
        // *it = 10;  // 错误:常量迭代器不可修改元素
        cout << *it << " ";  // 输出:1 2 3 4 5
    }
    cout << endl;
    
    // 3. 反向迭代器(rbegin()/rend()):降序遍历
    cout << "反向降序遍历:";
    for (set<int>::reverse_iterator it = s.rbegin(); it != s.rend(); ++it) {
        cout << *it << " ";  // 输出:5 4 3 2 1
    }
    cout << endl;
    
    // 4. 范围for循环(C++11及以上):简化正向遍历
    cout << "范围for遍历:";
    for (auto x : s) {
        cout << x << " ";  // 输出:1 2 3 4 5
    }
    cout << endl;
    
    return 0;
}

2.6 其他常用操作

除上述核心操作外,set还提供了获取大小、判断空、交换容器等辅助操作:

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

int main() {
    set<int> s = {1, 2, 3};
    set<int> s2 = {4, 5, 6};
    
    // 1. size():获取元素个数
    cout << "s的元素个数:" << s.size() << endl;  // 输出:3
    
    // 2. empty():判断容器是否为空(空返回true,否则返回false)
    cout << "s是否为空:" << (s.empty() ? "是" : "否") << endl;  // 输出:否
    
    // 3. max_size():获取容器可容纳的最大元素个数(受系统内存限制)
    cout << "s的最大容量:" << s.max_size() << endl;
    
    // 4. swap():交换两个set的内容(同类型、同排序规则)
    s.swap(s2);
    cout << "交换后s的元素:";
    for (auto x : s) cout << x << " ";  // 输出:4 5 6
    cout << endl;
    cout << "交换后s2的元素:";
    for (auto x : s2) cout << x << " ";  // 输出:1 2 3
    cout << endl;
    
    return 0;
}

三、set 实现原理简析

C++ STL中的set(以及multiset、map、multimap)均基于红黑树实现,红黑树是一种自平衡的二叉搜索树(Self-Balanced Binary Search Tree),其核心优势是通过特定的旋转和着色规则,保证树的高度始终维持在O(log n)级别,从而确保插入、删除、查找等操作的时间复杂度稳定为O(log n)。

3.1 红黑树的核心特性

红黑树的每个节点都有一个"颜色"属性(红色或黑色),且满足以下5条规则,正是这些规则保证了树的平衡性:

  1. 每个节点要么是红色,要么是黑色;

  2. 根节点必须是黑色;

  3. 所有叶子节点(NIL节点,空节点)都是黑色;

  4. 如果一个节点是红色,那么它的两个子节点必须是黑色(即不允许两个红色节点相邻);

  5. 从任意节点到其所有叶子节点的路径上,黑色节点的数量都相同(称为"黑高"相等)。

3.2 set 与红黑树的映射关系

set的每个元素对应红黑树的一个节点,元素值作为节点的键值(key),set的核心操作本质是红黑树的节点操作:

  • 插入操作:先按二叉搜索树的规则插入新节点(默认着色为红色),然后检查红黑树规则是否被破坏,若破坏则通过"旋转"(左旋转、右旋转)和"重新着色"修复平衡;

  • 删除操作:找到目标节点后,按二叉搜索树的规则删除,同样检查并修复红黑树的平衡规则;

  • 查找操作:利用二叉搜索树的特性,从根节点开始,若目标值小于当前节点键值则向左子树查找,大于则向右子树查找,直到找到目标节点或遍历到叶子节点;

  • 排序特性:二叉搜索树的中序遍历(左子树→根节点→右子树)结果就是有序的,set的正向迭代器本质就是红黑树的中序遍历迭代器。

3.3 set 不支持修改元素的原因

set的迭代器是"常量迭代器"(const_iterator),不允许直接修改元素值,这是因为元素值是红黑树节点的键值,修改键值会破坏二叉搜索树的有序性,导致红黑树的平衡规则失效,进而使set的后续操作(查找、插入等)出现错误。若必须修改set中的元素,需先删除旧元素,再插入新元素。

四、set 性能分析

set的性能表现由其底层红黑树的特性决定,优势和劣势都非常明确,选择时需结合具体场景判断:

4.1 优势场景

  • 有序数据存储与遍历:自动排序特性无需手动维护顺序,遍历效率稳定(O(n),本质是红黑树的中序遍历);

  • 高效查找与去重:查找、去重操作时间复杂度为O(log n),适合需要频繁判断元素是否存在的场景(如数据去重、字典查询);

  • 稳定的插入/删除效率:无论数据量大小,插入、删除操作的时间复杂度均为O(log n),优于vector(扩容时为O(n))和list(查找目标元素时为O(n));

  • 迭代器稳定:插入、删除元素时(除被删除元素的迭代器外),其他迭代器均有效,无需像vector那样重新获取迭代器。

4.2 劣势场景

  • 不支持随机访问:无法通过下标快速访问元素,只能顺序遍历,随机访问场景(如频繁按索引取元素)效率低下;

  • 内存开销较大:红黑树节点除了存储元素值,还需要存储颜色、左右子节点指针、父节点指针等额外信息,内存利用率低于vector;

  • 插入/删除效率低于vector尾部操作:vector尾部插入/删除(不扩容时)时间复杂度为O(1),而set始终为O(log n),小数据量高频尾部操作场景下vector更优;

  • 自定义类型需重载比较运算符:存储自定义结构体/类时,必须手动重载<运算符(或自定义比较器),否则无法编译通过,使用成本高于vector。

五、set 实战案例

以下通过两个典型实战案例,展示set在实际编程中的应用场景,涵盖数据去重、有序数据查询等核心需求。

案例1:利用set实现数据去重与排序

需求:读取一组无序且可能包含重复元素的整数,使用set自动去重并按升序输出,再按降序输出。

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

int main() {
    set<int> s_asc;  // 升序set,用于去重+升序
    set<int, greater<int>> s_desc;  // 降序set,用于去重+降序
    
    int num;
    cout << "请输入一组整数(以非整数结束):";
    while (cin >> num) {
        s_asc.insert(num);
        s_desc.insert(num);
    }
    
    // 输出升序结果(去重后)
    cout << "去重后升序:";
    for (auto x : s_asc) {
        cout << x << " ";
    }
    cout << endl;
    
    // 输出降序结果(去重后)
    cout << "去重后降序:";
    for (auto x : s_desc) {
        cout << x << " ";
    }
    cout << endl;
    
    return 0;
}

运行结果:

请输入一组整数(以非整数结束):5 2 8 2 1 5 9 a

去重后升序:1 2 5 8 9

去重后降序:9 8 5 2 1

案例2:set 存储自定义结构体(需重载比较运算符)

需求:定义一个"商品"结构体,包含商品名称和价格,使用set存储商品列表,按价格升序排序;若价格相同,按名称字典序排序。

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

// 定义商品结构体
struct Goods {
    string name;  // 商品名称
    double price; // 商品价格
    
    // 重载<运算符,定义排序规则:先按价格升序,价格相同按名称字典序升序
    bool operator<(const Goods& other) const {
        if (price != other.price) {
            return price < other.price;
        }
        return name < other.name;
    }
};

int main() {
    // 初始化商品列表(包含重复价格的商品)
    set<Goods> goods_set = {
        {"Apple", 5.99},
        {"Banana", 3.99},
        {"Orange", 5.99},
        {"Grape", 4.99},
        {"Banana", 3.99}  // 重复元素,会被去重
    };
    
    // 输出排序后的商品信息
    cout << "按规则排序后的商品列表:" << endl;
    for (const auto& g : goods_set) {
        cout << "商品:" << g.name << ",价格:" << g.price << endl;
    }
    
    // 查找价格为5.99的商品
    auto it = goods_set.find({"", 5.99});  // 利用重载的<运算符,仅需匹配价格
    if (it != goods_set.end()) {
        cout << "\n找到价格为5.99的商品:" << it->name << endl;  // 输出:Apple
    }
    
    return 0;
}

运行结果:

按规则排序后的商品列表:

商品:Banana,价格:3.99

商品:Grape,价格:4.99

商品:Apple,价格:5.99

商品:Orange,价格:5.99

找到价格为5.99的商品:Apple

六、常见问题与注意事项

6.1 自定义类型必须重载比较运算符

当set存储自定义结构体/类时,必须为该类型重载<运算符(或在定义set时指定自定义比较器),否则编译器无法判断元素的排序规则,会直接报错。重载<运算符时,需确保逻辑是"严格弱序"(即满足自反性、反对称性、传递性),否则可能导致set的操作异常。

自定义比较器的替代方案(无需重载运算符):

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

struct Goods {
    string name;
    double price;
};

// 自定义比较器结构体
struct GoodsCompare {
    bool operator()(const Goods& g1, const Goods& g2) const {
        if (g1.price != g2.price) {
            return g1.price < g2.price;
        }
        return g1.name < g2.name;
    }
};

// 定义set时指定比较器
set<Goods, GoodsCompare> goods_set;

6.2 无法直接修改set中的元素

如前文所述,set的元素是红黑树的键值,修改元素会破坏树的有序性和平衡性,因此set的迭代器是常量迭代器,不允许直接修改元素。若需修改,需先删除旧元素,再插入新元素:

cpp 复制代码
set<int> s = {1, 2, 3};
// 错误:无法修改常量迭代器指向的元素
// auto it = s.find(2);
// *it = 4;

// 正确做法:删除旧元素,插入新元素
s.erase(2);
s.insert(4);  // 插入后s为{1,3,4}

6.3 set 与 unordered_set 的选择

C++11及以上提供了unordered_set容器,其底层基于哈希表实现,与set的核心区别如下,选择时需结合场景:

特性 set unordered_set
底层实现 红黑树 哈希表
排序性 自动有序 无序
时间复杂度 插入/删除/查找均为O(log n)(稳定) 平均O(1),最坏O(n)(受哈希函数影响)
适用场景 需要有序数据、频繁范围查询 无需排序、追求高效插入查找(无哈希冲突时)

6.4 避免使用低效的遍历查找

部分开发者会习惯用范围for循环遍历set查找元素,这种方式的时间复杂度为O(n),远低于set自带的find()方法(O(log n))。因此,查找元素时务必使用find()方法,而非手动遍历。

七、总结

set是C++ STL中极具价值的有序关联容器,其基于红黑树的实现保证了稳定高效的插入、删除、查找操作,自动排序和去重特性使其在有序数据管理场景中大放异彩。掌握set的核心操作、实现原理及适用场景,能帮助我们在数据去重、有序存储、高效查询等需求中选择最优容器,编写高效、简洁的代码。

相关推荐
坚持就完事了17 小时前
Linux的学习03:时间没有更新怎么解决
学习
李泽辉_17 小时前
深度学习算法学习(一):梯度下降法和最简单的深度学习核心原理代码
深度学习·学习·算法
沛沛老爹17 小时前
Web开发者进阶AI:Agent Skills-深度迭代处理架构——从递归函数到智能决策引擎
java·开发语言·人工智能·科技·架构·企业开发·发展趋势
Good_Starry17 小时前
Java——正则表达式
java·开发语言·正则表达式
二哈喇子!17 小时前
前端HTML、CSS、JS、VUE 汇总
开发语言·前端
scx2013100417 小时前
20260105 莫队总结
c++
欧洵.17 小时前
Java.基于UDP协议的核心内容
java·开发语言·udp
情缘晓梦.17 小时前
C语言数据存储
c语言·开发语言
xunyan623417 小时前
第九章 JAVA常用类
java·开发语言