C++中的set容器:有序、唯一元素的高效管理

C++中的set容器:有序、唯一元素的高效管理

在C++标准模板库(STL)中,set是一种基于红黑树(Red-Black Tree) 实现的关联容器,其核心特性是元素有序且唯一。它支持高效的插入、删除和查找操作,适用于需要快速检索且元素不重复的场景。本文将详细解析set的特性、用法、底层实现及实际应用,帮助开发者掌握这一常用容器。

一、set的核心特性与定义

set容器的本质是有序且无重复元素的集合,其核心特性如下:

  1. 元素唯一性set中不会存储重复元素,插入已存在的元素会被自动忽略。
  2. 自动排序:元素在set中按特定规则(默认升序)自动排序,无需手动维护顺序。
  3. 基于红黑树:底层采用平衡二叉搜索树(红黑树)实现,插入、删除、查找的平均时间复杂度为O(log n)
  4. 不可直接修改元素set的元素被视为const(常量),无法直接修改(若需修改,需先删除旧元素再插入新元素)。

set的定义与头文件

使用set需包含头文件<set>,并通过命名空间std访问。其模板定义如下:

template <
    class T,                        // 元素类型
    class Compare = std::less<T>,   // 比较器(默认升序)
    class Allocator = std::allocator<T>  // 分配器(默认即可)
> class set;

基本定义示例

#include <set>
using namespace std;

// 存储int类型,默认按less<int>(升序)排序
set<int> s1;

// 存储string类型,按降序排序(使用greater<string>)
set<string, greater<string>> s2;

二、set的基本操作

set提供了丰富的成员函数,涵盖元素的插入、删除、查找、遍历等常用操作。

1. 插入元素(insert)

insert函数用于向set中添加元素,若元素已存在则插入失败(返回值可判断是否插入成功)。

用法

set<int> s;

// 插入单个元素
s.insert(3);
s.insert(1);
s.insert(2);
s.insert(2);  // 重复元素,插入失败

// 插入多个元素(C++11初始化列表)
s.insert({5, 4, 6});

// 插入返回值:pair<iterator, bool>
// - first:指向插入元素或已存在元素的迭代器
// - second:插入成功为true,失败为false
auto result = s.insert(7);
if (result.second) {
    cout << "插入成功:" << *result.first << endl;
} else {
    cout << "插入失败(元素已存在):" << *result.first << endl;
}

插入后集合状态{1, 2, 3, 4, 5, 6, 7}(自动按升序排序)。

2. 删除元素(erase)

erase函数用于删除set中的元素,支持按值、迭代器或范围删除。

用法

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

// (1)按值删除:返回删除的元素个数(0或1,因元素唯一)
size_t count = s.erase(3);  // 删除值为3的元素,返回1
cout << "删除了" << count << "个元素" << endl;

// (2)按迭代器删除:删除迭代器指向的元素
auto it = s.find(4);  // 查找值为4的元素
if (it != s.end()) {
    s.erase(it);  // 删除迭代器指向的元素
}

// (3)删除范围:删除[first, last)之间的元素
it = s.find(2);
s.erase(it, s.end());  // 删除从2开始到末尾的元素(即2,5)

// 此时集合状态:{1}

3. 查找元素(find)

find函数用于查找指定值的元素,返回指向该元素的迭代器;若未找到,返回end()

用法

set<string> fruits = {"apple", "banana", "orange"};

// 查找元素
auto it = fruits.find("banana");
if (it != fruits.end()) {
    cout << "找到元素:" << *it << endl;  // 输出:找到元素:banana
} else {
    cout << "未找到元素" << endl;
}

4. 其他常用操作

函数功能描述
size()返回当前元素个数
empty()判断容器是否为空(空返回true)
clear()清空所有元素
count(val)返回值为val的元素个数(0或1,因元素唯一)
lower_bound(val)返回第一个不小于val的元素迭代器
upper_bound(val)返回第一个大于val的元素迭代器
begin()/end()返回首元素/尾后迭代器(用于遍历)

示例

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

cout << "元素个数:" << s.size() << endl;  // 输出:4

// 遍历元素(默认升序)
for (auto it = s.begin(); it != s.end(); ++it) {
    cout << *it << " ";  // 输出:1 2 3 4
}
cout << endl;

// lower_bound与upper_bound
auto low = s.lower_bound(2);  // 指向2
auto high = s.upper_bound(3); // 指向4
cout << "lower_bound(2):" << *low << endl;   // 2
cout << "upper_bound(3):" << *high << endl;  // 4

三、set的排序规则:自定义比较器

set默认使用std::less<T>作为比较器,对元素按升序排序。若需自定义排序规则(如降序、按元素某字段排序),可通过以下两种方式实现:

1. 使用预定义比较器(如greater)

std::greater<T>是STL提供的另一种比较器,可实现降序排序。

示例

#include <set>
#include <functional>  // 包含greater

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

// 遍历(降序)
for (int num : s) {  // 范围for循环(C++11)
    cout << num << " ";  // 输出:4 3 2 1
}

2. 自定义比较器(函数对象或lambda)

对于复杂类型(如自定义结构体),需定义比较规则。可通过函数对象(Functor)lambda表达式实现。

(1)函数对象(适用于C++11前)
#include <string>

// 自定义结构体
struct Person {
    string name;
    int age;
    Person(string n, int a) : name(n), age(a) {}
};

// 自定义比较器:按年龄升序(年龄相同则按姓名升序)
struct ComparePerson {
    bool operator()(const Person& p1, const Person& p2) const {
        if (p1.age != p2.age) {
            return p1.age < p2.age;  // 年龄小的在前
        } else {
            return p1.name < p2.name;  // 年龄相同则姓名字典序
        }
    }
};

// 使用自定义比较器的set
set<Person, ComparePerson> people;

int main() {
    people.insert(Person("Alice", 25));
    people.insert(Person("Bob", 20));
    people.insert(Person("Charlie", 25));

    // 遍历(按年龄升序,同年龄按姓名升序)
    for (const auto& p : people) {
        cout << p.name << "(" << p.age << "), ";
    }
    // 输出:Bob(20), Alice(25), Charlie(25), 
    return 0;
}
(2)lambda表达式(C++11及以上,配合decltype)

对于简单场景,可直接用lambda表达式定义比较规则,但需注意:lambda作为模板参数时需用decltype推导类型,并显式传递给构造函数。

#include <set>

int main() {
    // 定义lambda比较器:按字符串长度降序
    auto cmp = [](const string& a, const string& b) {
        return a.size() > b.size();
    };

    // 声明set:用decltype获取lambda类型
    set<string, decltype(cmp)> words(cmp);

    words.insert({"apple", "banana", "pear", "grape"});

    // 遍历(按长度降序:banana(6), apple(5), grape(5), pear(4))
    for (const auto& w : words) {
        cout << w << "(" << w.size() << "), ";
    }
    return 0;
}

四、set与multiset的区别

multisetset的变体,唯一区别是允许存储重复元素,其他特性(如有序、基于红黑树)完全一致。

multiset的特殊点

  • insert函数总是成功(允许重复);
  • count(val)返回值为val的元素个数(可能大于1);
  • erase(val)会删除所有值为val的元素(若需删除单个,需通过迭代器)。

示例

#include <set>

int main() {
    multiset<int> ms = {2, 1, 2, 3, 2};

    // 遍历(有序,保留重复):1 2 2 2 3
    for (int num : ms) {
        cout << num << " ";
    }
    cout << endl;

    cout << "值为2的元素个数:" << ms.count(2) << endl;  // 输出:3

    // 删除所有值为2的元素
    ms.erase(2);
    // 此时集合:1 3
    return 0;
}

五、set的底层实现:红黑树

set的高效性能源于其底层的红黑树实现,这是一种自平衡的二叉搜索树(BST),具有以下特性:

  1. 二叉搜索树特性:对于任意节点,左子树所有元素小于该节点,右子树所有元素大于该节点(基于比较器),确保查找、插入、删除可通过二分法快速定位(O(log n))。
  2. 自平衡机制:通过颜色规则(节点分为红/黑)和旋转操作,确保树的高度始终保持在O(log n),避免普通BST在极端情况下退化为链表(O(n)性能)。
  3. 迭代器特性set的迭代器是双向迭代器(可++/–),遍历顺序为中序遍历(左→根→右),因此能按排序规则访问元素。

六、set的适用场景

set适合以下场景:

  1. 需要自动去重并排序的场景:如存储唯一ID、关键词去重并按字典序排列。
  2. 高效查找、插入、删除:如日志系统中按时间戳存储事件(需有序且快速检索)。
  3. 范围查询:利用lower_boundupper_bound快速获取区间元素(如查找分数在[60, 80]之间的学生)。

不适用场景

  • 需要频繁修改元素值(需先删除再插入,效率低);
  • 追求极致的插入/查找性能且元素无序(可考虑unordered_set,基于哈希表,平均O(1))。

七、注意事项与最佳实践

  1. 元素不可直接修改set的元素被视为const,若需修改,需先删除旧元素再插入新元素:

    set<int> s = {1, 2, 3};
    // 错误:不能修改迭代器指向的元素
    // auto it = s.find(2); *it = 5;
    
    // 正确方式:删除旧元素,插入新元素
    auto it = s.find(2);
    if (it != s.end()) {
        s.erase(it);
        s.insert(5);  // 集合变为{1, 3, 5}
    }
    
  2. 比较器的一致性:自定义比较器必须满足严格弱序(Strict Weak Ordering),否则可能导致未定义行为(如插入无限循环、查找错误)。严格弱序要求:

    • 反自反性:cmp(a, a)必须为false;
    • 传递性:若cmp(a, b)cmp(b, c)为true,则cmp(a, c)为true;
    • 对称性:若!cmp(a, b)!cmp(b, a),则a和b视为等价(set中会去重)。
  3. 选择set还是unordered_set

    • set:有序、基于红黑树、O(log n)操作、支持范围查询;
    • unordered_set:无序、基于哈希表、平均O(1)操作、不支持范围查询。
      根据是否需要有序和范围查询选择,无序且追求速度优先选unordered_set

八、总结

set是C++ STL中用于管理有序、唯一元素的关联容器,基于红黑树实现,提供O(log n)的插入、删除和查找效率。其核心特性包括自动排序、去重、支持范围查询,适用于需要高效检索且元素无重复的场景。

通过自定义比较器,set可灵活适应不同的排序需求;而multiset作为变体,允许重复元素,扩展了使用场景。在实际开发中,需根据是否需要有序、是否允许重复、性能需求等因素,选择setmultisetunordered_set

掌握set的操作和底层原理,能帮助开发者在数据管理场景中写出高效、清晰的代码,是STL容器使用的基础技能之一。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bkspiderx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值