【C++基础】std::set详解

std::set 是 C++ 标准库中的关联容器,它提供了一种存储唯一元素的有序集合。它提供了高效的插入、删除和查找操作,并且能够自动维护元素的有序性和唯一性。

一、底层实现

std::set 的底层原理是基于红黑树(Red-Black Tree)。红黑树是一种自平衡的二叉搜索树,保证了树的平衡性,从而保证了插入、删除和查找操作的时间复杂度为 O(log n)。它满足以下性质:

1、 每个节点要么是红色,要么是黑色。
2、 根节点是黑色的。(插入的第一个元素)
3、 所有叶子节点(NIL 节点)都是黑色的。
4、 如果一个节点是红色的,则它的两个子节点都是黑色的。
5、 对于每个节点,从该节点到其后代叶子节点的简单路径上,黑色节点的数量都相同。

std::set 中,每个元素都被插入到红黑树中作为一个节点。当插入一个新元素时,std::set 会根据元素值的大小将其插入到合适的位置,同时保持红黑树的平衡性。在红黑树中查找元素时,根据红黑树的特性,可以通过二分查找的方式高效地定位到目标元素。
由于红黑树是一种高效的自平衡二叉搜索树,因此 std::set 能够提供高效的插入、删除和查找操作,同时保持元素的有序性和唯一性。

二、成员函数

std::set提供了一系列成员函数来操作集合。
insert: 向集合中插入元素。
emplace: 在集合中就地构造元素。
erase: 从集合中删除指定元素。
find: 查找集合中是否存在指定元素。
count: 返回集合中与指定元素相等的元素的数量(通常为0或1)。
clear: 清空集合中的所有元素。
size: 返回集合中元素的数量。
empty: 检查集合是否为空。
swap: 交换两个集合的内容。
begin: 返回指向集合中第一个元素的迭代器。
end: 返回指向集合中最后一个元素之后位置的迭代器。
lower_bound: 返回指向第一个不小于给定键值的元素的迭代器。
upper_bound: 返回指向第一个大于给定键值的元素的迭代器。
equal_range: 返回一个范围,其中包含所有与给定键值相等的元素。
max_size: 返回集合能容纳的最大元素数量。
Demo演示:

std::set<int> set1;
set1.insert(1);
set1.insert(2);
set1.insert(3);
set1.insert(4);
std::cout<<set1.size()<<std::endl; // 4
std::cout<<set1.empty()<<std::endl; // 0
std::cout<<*set1.find(2)<<std::endl; // 2
set1.erase(2);
std::cout<<(set1.find(2)==set1.end())<<std::endl; // 1
std::cout<<set1.count(3)<<std::endl; // 1
auto lower = set1.lower_bound(3);
auto upper = set1.upper_bound(3);
std::cout << "Lower bound of 3: " << *lower << std::endl; // Lower bound of 3: 3
std::cout << "Upper bound of 3: " << *upper << std::endl; // Upper bound of 4: 4
std::set<int> set2;
set2.swap(set1);
std::cout<<set1.size()<<" "<<set2.size()<<std::endl; // 0 3
std::cout<<set1.max_size()<<std::endl; // 230584300921369395
for (auto it = set2.begin(); it != set2.end(); ++it) {
    std::cout << *it << " "<<std::endl; // 1 3 4
}
auto range = set2.equal_range(3);
std::cout<<*range.first<<std::endl; // 3

三、自定义排序规则

std::set 默认是根据元素的大小进行排序的,采用的是严格弱序(Strict Weak Ordering)的比较方式。这意味着元素必须支持 < 运算符,元素按照升序排列。

可以通过提供自定义的比较函数或者函数对象来实现排序规则。需要两个步骤:
1、 定义比较函数或者函数对象
比较函数:定义一个函数,接受两个参数,比较它们的大小,并返回 truefalse,表示它们的顺序关系。
函数对象(Functor):定义一个类,重载 operator(),使得对象可以像函数一样被调用,在其中实现比较逻辑。

2、 传递比较函数或者函数对象给 std::set 构造函数
在创建 std::set 对象时,通过传递比较函数或者函数对象作为第二个参数,告诉集合如何比较元素的顺序。

#include <iostream>
#include <set>

// 自定义比较函数
bool customCompare(int a, int b) {
    // 实现自定义的比较逻辑,这里以整数的绝对值大小为例
    return abs(a) < abs(b);
}

// 自定义函数对象(Functor)
struct CustomComparator {
    bool operator()(int a, int b) const {
        // 实现自定义的比较逻辑,这里以整数的平方大小为例
        return (a * a) < (b * b);
    }
};

int main() {
    // 使用自定义比较函数创建 std::set
    std::set<int, decltype(&customCompare)> customSet(customCompare);

    // 使用自定义函数对象创建 std::set
    std::set<int, CustomComparator> functorSet;

    // 插入元素
    customSet.insert(5);
    customSet.insert(-3);
    customSet.insert(2);

    functorSet.insert(5);
    functorSet.insert(-3);
    functorSet.insert(2);

    // 遍历输出结果
    std::cout << "Custom compare function set: ";
    for (int num : customSet) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    std::cout << "Functor set: ";
    for (int num : functorSet) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

四、自定义对象排序

std::set 中存储的元素是自定义对象时,需要重载 < 运算符,以便 std::set 能够对对象进行比较和排序。重载 < 运算符的目的是定义对象之间的比较规则,以确定它们在集合中的顺序。这样,std::set 就可以使用这些规则来维护集合的有序性。
下面是一个示例,演示了如何定义一个自定义类型的 Person 对象,并在 std::set 中使用自定义的比较函数进行排序:

#include <iostream>
#include <set>
#include <string>

// 定义一个自定义的 Person 类
class Person {
private:
    std::string name;
    int age;

public:
    Person(std::string name, int age) : name(name), age(age) {}

    // 重载 < 运算符,用于自定义对象的比较规则
    bool operator<(const Person& other) const {
        // 按照年龄进行比较
        return age < other.age;
    }

    // 为了便于输出,重载 << 运算符
    friend std::ostream& operator<<(std::ostream& os, const Person& person) {
        os << "Name: " << person.name << ", Age: " << person.age;
        return os;
    }
};

int main() {
    // 创建一个存储 Person 对象的 std::set 容器,并使用自定义的比较函数进行排序
    std::set<Person> people;

    // 向集合中插入一些 Person 对象
    people.insert(Person("Alice", 30));
    people.insert(Person("Bob", 25));
    people.insert(Person("Charlie", 35));

    // 遍历输出集合中的元素,由于使用了自定义的比较规则,输出时会按照年龄升序排列
    for (const auto& person : people) {
        std::cout << person << std::endl;
    }

    return 0;
}

在这个示例中,Person 类具有 nameage 两个属性,我们通过重载 < 运算符来定义了对象之间的比较规则,按照年龄进行比较。然后,我们创建了一个 std::set<Person> 容器,并向其中插入了几个 Person 对象。由于我们使用了自定义的比较函数,因此在输出容器中的元素时,它们会按照年龄的升序排列。

五、性能问题

std::set 在插入、删除和查找等操作上具有稳定且良好的性能特征,适用于需要高效管理有序唯一元素集合的场景。然而,在空间效率方面,它可能不如其他数据结构(如 std::unordered_set)那么优越。因此,在选择数据结构时,需要根据具体的需求权衡其性能特征。

1、 插入和删除操作的性能
插入和删除操作的时间复杂度为 O(log n),其中 n 是集合中的元素数量。这是因为红黑树是一种自平衡的二叉搜索树,插入和删除操作会导致树的重新平衡,但是由于红黑树的特性,重新平衡的代价是受控的。
因此,std::set 在插入和删除方面的性能比较稳定,不受集合大小的影响。

2、 查找操作的性能
查找操作的时间复杂度也是 O(log n),因为红黑树具有良好的平衡性,可以保证在平均情况下快速地查找元素。
std::set 提供了高效的查找功能,适用于需要快速检索元素的场景。

3、 迭代器的性能
std::set 的迭代器是双向迭代器,支持前向和后向遍历,其性能与集合大小无关,是常数时间复杂度的操作。
这意味着在遍历 std::set 中的元素时,无论集合大小如何,迭代器的操作都非常高效。

4、 内存占用
std::set 使用红黑树作为底层数据结构,它是一种高效的平衡二叉搜索树,但是相对于其他数据结构(如哈希表),它可能会占用更多的内存。
红黑树需要维护额外的节点信息以保持平衡,因此在存储相同数量的元素时,std::set 可能会占用更多的内存空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值