C++ std::set

std::set 是 C++ STL 中的有序关联容器 ,其核心特性是存储唯一元素并自动按指定规则排序 。底层通常基于红黑树(自平衡二叉搜索树)实现,这使得它在查找、插入、删除操作上具有稳定的高效性。

1、底层数据结构与核心概念

1.1 底层数据结构

  • 底层结构 :通常实现为红黑树(Red-Black Tree),这是一种自平衡的二叉搜索树(BST)。
  • 核心特性
    1. 关联性 :元素是键(Key)本身 。在 set 中,值就是键。
    2. 有序性 :容器中的元素会根据特定的比较准则 (默认为 std::less<Key>)自动进行排序。你遍历集合时,元素总是按升序排列的。
    3. 唯一性 :集合中每个元素的键都是唯一的。不允许存在两个键相等的元素。
  • 节点结构:红黑树的每个节点存储一个元素(即键),以及必要的父指针、左孩子指针、右孩子指针和颜色标记(用于平衡)。

1.2 核心特性与原理

  1. 自动排序与唯一性

    • 每当插入一个元素时,set 会根据其内部的红黑树结构,将该元素放到正确的位置以维持二叉搜索树的性质(左子树所有键 < 根节点键 < 右子树所有键)。
    • 在插入过程中,它会检查新元素的键是否已存在。如果存在,则插入操作失败,容器保持不变。这保证了元素的唯一性。
  2. 操作效率 (O(log n))

    • 由于红黑树是自平衡的,它能保证在最坏情况下,插入、删除和查找 操作的时间复杂度都是对数级 的,即 O(log n) ,其中 n 是树中元素的数量。
    • 这种效率远高于在 vectorlist 中进行线性查找 (O(n)),但低于 std::unordered_set 的平均常数时间复杂度 (O(1))。
  3. 迭代器稳定性

    • 插入和删除操作不会使任何现有迭代器失效(除了指向被删除元素的迭代器)。
    • 这是因为树的结构调整是通过旋转和重新着色完成的,节点在内存中的地址通常不会改变。这与 vector 的动态内存重新分配形成鲜明对比。
  4. 不可修改元素

    • set 元素的键是 const 的。一旦一个元素被插入到 set 中,你就不能直接修改它,因为这会破坏树内部的有序性。
    • 如果需要修改一个元素,必须先将其删除,然后插入修改后的新值。

2、操作指导与代码示例

2.1 初始化与构造函数

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

// 1. 空set
std::set<int> set1;

// 2. 使用初始化列表 (C++11)
std::set<int> set2 = {5, 2, 8, 1, 2, 4}; // 重复的2会被忽略 -> {1, 2, 4, 5, 8}

// 3. 通过迭代器范围(另一个容器的)
std::vector<int> vec = {6, 7, 7, 8, 1};
std::set<int> set3(vec.begin(), vec.end()); // {1, 6, 7, 8}

// 4. 自定义比较准则(例如:降序)
struct Greater {
    bool operator()(const int& a, const int& b) const {
        return a > b;
    }
};
std::set<int, Greater> set4 = {5, 2, 8, 1}; // {8, 5, 2, 1}

// 5. 拷贝构造函数
std::set<int> set5(set2); // {1, 2, 4, 5, 8}

2.2 元素访问与查找

cpp 复制代码
std::set<int> mySet = {50, 20, 60, 10, 30};

// 查找元素 count & find
// 1. count: 由于唯一性,返回值只能是0或1
if (mySet.count(30)) {
    std::cout << "30 is in the set\n";
}

// 2. find: 更高效和常用的方法
auto it = mySet.find(20);
if (it != mySet.end()) { // 一定要检查是否找到!
    std::cout << "Found: " << *it << "\n"; // Output: Found: 20
} else {
    std::cout << "Not found\n";
}

// 3. 获取范围边界:lower_bound, upper_bound, equal_range
// 这些在需要范围查询时非常有用
// lower_bound(30): 返回第一个 >= 30 的元素的迭代器
// upper_bound(30): 返回第一个 > 30 的元素的迭代器
auto low = mySet.lower_bound(25); // 指向30
auto high = mySet.upper_bound(55); // 指向60

2.3 增加元素

cpp 复制代码
std::set<int> s;

// 1. insert: 插入一个值
std::pair<std::set<int>::iterator, bool> result = s.insert(100);
if (result.second) {
    std::cout << "Insertion successful\n";
} else {
    std::cout << "Element already exists\n";
}

// 2. emplace: 原地构造,避免临时对象,更高效(对于非内置类型)
// 直接传递构造参数给emplace
s.emplace(200);
// 对于自定义类型,优势明显:
// std::set<MyClass> s;
// s.emplace(arg1, arg2); // 直接在set内部构造MyClass对象

// 3. 插入一个迭代器范围(提示位置),提示位置是为了优化插入效率
// 如果提示位置正确,插入可能从O(log n)加速到接近O(1)
auto hint = s.end();
s.insert(hint, 150); // 提供一个插入位置的"提示"

2.4 删除元素

cpp 复制代码
std::set<int> s = {10, 20, 30, 40, 50};

// 1. erase by key: 删除键为30的元素,返回删除的元素个数(0或1)
size_t numErased = s.erase(30); // numErased = 1, s: {10, 20, 40, 50}

// 2. erase by iterator: 删除指定位置的元素,更高效
auto it = s.find(20);
if (it != s.end()) {
    s.erase(it); // s: {10, 40, 50}
}

// 3. erase by range: 删除一个区间的元素 [first, last)
auto first = s.find(10);
auto last = s.find(50); // 注意:50不会被删除,因为是[first, last)
s.erase(first, last); // 删除10和40,s: {50}

2.5 遍历元素

cpp 复制代码
std::set<std::string> fruits = {"apple", "banana", "orange", "mango"};

// 1. 使用迭代器(总是有序的)
std::cout << "Using iterators: ";
for (auto it = fruits.begin(); it != fruits.end(); ++it) {
    std::cout << *it << " "; // Output: apple banana mango orange
}
std::cout << "\n";

// 2. 使用范围for循环 (C++11)
std::cout << "Using range-for: ";
for (const auto& fruit : fruits) { // 使用const引用,避免拷贝
    std::cout << fruit << " ";
}
std::cout << "\n";

// 3. 使用反向迭代器(逆序遍历)
std::cout << "Reverse order: ";
for (auto rit = fruits.rbegin(); rit != fruits.rend(); ++rit) {
    std::cout << *rit << " "; // Output: orange mango banana apple
}

2.6 自定义类型作为键

要让自定义类型(如MyClass)作为 set 的键,你需要为其定义排序准则。

2.6.1 在自定义类型内重载 operator<(推荐,简单直观)
cpp 复制代码
struct Person {
    std::string name;
    int age;

    // 必须定义为const成员函数
    bool operator<(const Person& other) const {
        // 定义你的比较逻辑:先按name比,再按age比
        if (name == other.name) {
            return age < other.age;
        }
        return name < other.name;
    }
};

std::set<Person> personSet; // 可以直接使用
personSet.insert({"Alice", 25});
personSet.insert({"Bob", 30});
2.6.2 提供自定义的函数对象(仿函数)作为模板参数
cpp 复制代码
struct Person {
    std::string name;
    int age;
};

// 自定义比较器
struct PersonCompare {
    bool operator()(const Person& a, const Person& b) const {
        return a.name < b.name; // 只按name比较
    }
};

// 使用时必须将比较器作为模板参数传入
std::set<Person, PersonCompare> personSet;

2.7 修改元素

std::set 的元素是常量(value_type 为 const T),无法通过迭代器修改,否则会破坏红黑树的有序性:

cpp 复制代码
set<int> s = {1, 2, 3};
auto it = s.find(2);
// *it = 5;  // 编译报错!不允许修改元素

修改元素的正确方式:先删除旧元素,再插入新元素:

cpp 复制代码
s.erase(it);       // 删除 2
s.insert(5);       // 插入 5,s 变为 {1, 3, 5}

3、常见问题

  1. std::set 的底层数据结构是什么?为什么选择红黑树?

    底层是红黑树。选择红黑树的原因:

    • 红黑树是自平衡二叉搜索树,保证插入、删除、查找的时间复杂度均为 O(log n)(稳定高效)。
    • 中序遍历可得到有序序列,满足 std::set 的自动排序需求。
    • 相比其他平衡树(如 AVL 树),红黑树的旋转操作更少,实际性能更优。
  2. std::set 的元素为什么不能修改?

    因为 std::set 基于红黑树实现,元素的排序依赖其值。若允许修改元素,可能导致红黑树的有序性被破坏(如将一个小值改为大值,可能使其应位于右子树却仍在左子树),进而导致后续操作(查找、插入等)出错。因此标准规定 std::set 的元素为 const

  3. std::setinsert 函数返回值有什么意义?

    返回 pair<iterator, bool>

    • 第一个元素是迭代器,指向插入位置(若元素已存在,则指向已有的那个元素)。
    • 第二个元素是布尔值,标识插入是否成功(true 表示新元素插入,false 表示元素已存在)。
  4. 如何在 std::set 中高效查找大于等于 x 的第一个元素?

    使用 lower_bound(x) 方法,其时间复杂度为 O(log n),直接返回符合条件的迭代器。这是 std::set 有序特性带来的高效操作,优于 unordered_set(需遍历所有元素)。

  5. 什么时候该用 set,什么时候该用 unordered_set

    如果需要元素有序 ,或者需要进行范围查询 ,或者非常关心最坏情况的性能,就选择 set。如果只需要快速查找、插入和删除 ,并且不关心元素的顺序,那么 unordered_set 的平均性能会更好。

相关推荐
侃侃_天下3 小时前
最终的信号类
开发语言·c++·算法
博笙困了3 小时前
AcWing学习——差分
c++·算法
青草地溪水旁4 小时前
设计模式(C++)详解—抽象工厂模式 (Abstract Factory)(2)
c++·设计模式·抽象工厂模式
青草地溪水旁4 小时前
设计模式(C++)详解—抽象工厂模式 (Abstract Factory)(1)
c++·设计模式·抽象工厂模式
感哥4 小时前
C++ std::vector
c++
zl_dfq4 小时前
C++ 之【C++11的简介】(可变参数模板、lambda表达式、function\bind包装器)
c++
每天回答3个问题4 小时前
UE5C++编译遇到MSB3073
开发语言·c++·ue5
凯子坚持 c4 小时前
精通 Redis list:使用 redis-plus-plus 的现代 C++ 实践深度解析
c++·redis·list
JCBP_5 小时前
QT(4)
开发语言·汇编·c++·qt·算法