【C++进阶】关联容器:set类型

目录

一、set 基本概念

1.1 定义与特点

1.2 头文件与声明

1.3 核心特性解析

二、set 底层实现

2.1 红黑树简介

2.2 红黑树在 set 中的应用

三、set 常用操作

3.1 插入元素

3.2 删除元素

3.3 查找元素

3.4 遍历元素

3.5 性能特征

四、set 高级应用

4.1 自定义比较函数

4.2 交集、并集和差集操作

4.3 与其他容器的结合使用

4.4 迭代器操作

4.5 性能优化技巧

五、set 与其他容器的比较

5.1 set 与 vector

5.2 set 与 unordered_set

5.3 选择建议

六、注意事项与常见错误

6.1 迭代器失效问题

6.2 性能考虑

6.3 内存占用

6.4 易错点提醒

七、应用场景与实战案例

八、总结

九、参考资料


在 C++ 编程中,容器是非常重要的工具,它可以帮助我们高效地管理和操作数据。关联容器是 C++ 标准模板库(STL)中的一类特殊容器,它们通过键(key)来存储和访问元素。set 作为关联容器的一种,在很多场景下都有着广泛的应用。本文将全面深入地介绍 set 类型,包括其基本概念、底层实现、常用操作、高级应用以及与其他容器的比较等内容。

一、set 基本概念

1.1 定义与特点

set 是一种关联容器,它用于存储唯一的元素,并且这些元素会根据其值自动进行排序。在 set 中,键(key)和值(value)是相同的,即每个元素既是键也是值。由于 set 不允许有重复的元素,因此插入重复元素时会被忽略。

1.2 头文件与声明

要使用 set,需要包含 <set> 头文件。以下是一个简单的 set 声明示例:

#include <iostream>
#include <set>

int main() {
    std::set<int> mySet; // 声明一个存储 int 类型元素的 set
    return 0;
}

1.3 核心特性解析

std::set是基于红黑树实现的有序关联容器,其设计目标是通过平衡二叉搜索树结构,在保证元素唯一性的同时,实现以下核心特性:

①自动排序机制

  • 插入元素时自动按升序排列
  • 默认使用<运算符比较元素
  • 支持自定义比较函数

②唯一性约束

  • 不允许重复元素
  • 插入重复值时自动去重

③对数时间复杂度操作

  • 插入:O(log n)
  • 删除:O(log n)
  • 查找:O(log n)

④内存动态管理

  • 自动处理内存分配/释放
  • 支持迭代器失效保护

二、set 底层实现

2.1 红黑树简介

set 通常使用红黑树(Red-Black Tree)作为底层数据结构。红黑树是一种自平衡的二叉搜索树,它通过对节点进行着色(红色或黑色)并遵循一些特定的规则来保持树的平衡,从而保证了插入、删除和查找操作的时间复杂度都是 O(logn)。

红黑树的主要特性包括:

  • 每个节点要么是红色,要么是黑色。
  • 根节点是黑色。
  • 每个叶子节点(NIL 节点,空节点)是黑色。
  • 如果一个节点是红色的,则它的两个子节点都是黑色的。
  • 对每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。

2.2 红黑树在 set 中的应用

在 set 中,红黑树的节点存储了 set 中的元素。当插入一个新元素时,set 会根据元素的值将其插入到红黑树的合适位置,并通过旋转和着色操作来保持树的平衡。查找元素时,set 会从根节点开始,根据元素的值与节点的值进行比较,然后递归地在左子树或右子树中查找,直到找到目标元素或到达叶子节点。

三、set 常用操作

3.1 插入元素

可以使用 insert() 函数向 set 中插入元素。如果插入的元素已经存在于 set 中,则插入操作会被忽略。

#include <iostream>
#include <set>

int main() {
    std::set<int> mySet;
    mySet.insert(3);
    mySet.insert(1);
    mySet.insert(2);

    for (const auto& element : mySet) {
        std::cout << element << " ";
    }
    std::cout << std::endl;

    return 0;
}

输出:1 2 3,因为 set 会自动对元素进行排序。

3.2 删除元素

可以使用 erase() 函数从 set 中删除元素。erase() 函数有多种重载形式,可以根据元素的值或迭代器来删除元素。

#include <iostream>
#include <set>

int main() {
    std::set<int> mySet = {1, 2, 3, 4, 5};
    mySet.erase(3); // 根据元素的值删除

    auto it = mySet.find(4);
    if (it != mySet.end()) {
        mySet.erase(it); // 根据迭代器删除
    }

    for (const auto& element : mySet) {
        std::cout << element << " ";
    }
    std::cout << std::endl;

    return 0;
}

3.3 查找元素

可以使用 find() 函数在 set 中查找元素。如果找到元素,则返回指向该元素的迭代器;如果未找到,则返回 end() 迭代器。

#include <iostream>
#include <set>

int main() {
    std::set<int> mySet = {1, 2, 3, 4, 5};
    auto it = mySet.find(3);
    if (it != mySet.end()) {
        std::cout << "Found: " << *it << std::endl;
    } else {
        std::cout << "Not found" << std::endl;
    }

    return 0;
}

3.4 遍历元素

可以使用范围 for 循环或迭代器来遍历 set 中的元素。

#include <iostream>
#include <set>

int main() {
    std::set<int> mySet = {1, 2, 3, 4, 5};

    // 使用范围 for 循环遍历
    for (const auto& element : mySet) {
        std::cout << element << " ";
    }
    std::cout << std::endl;

    // 使用迭代器遍历
    for (auto it = mySet.begin(); it != mySet.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    return 0;
}

3.5 性能特征

操作 时间复杂度 说明
insert() O(log n) 插入新元素
erase() O(log n) 删除元素
find() O(log n) 查找元素
count() O(log n) 统计元素出现次数
size() O(1) 获取元素数量
empty() O(1) 检查是否为空

四、set 高级应用

4.1 自定义比较函数

默认情况下,set 使用 std::less 作为比较函数来对元素进行排序。如果需要自定义排序规则,可以在声明 set 时提供自定义的比较函数。

#include <iostream>
#include <set>

// 自定义比较函数,实现降序排序
struct Greater {
    bool operator()(const int& a, const int& b) const {
        return a > b;
    }
};

int main() {
    std::set<int, Greater> mySet = {1, 2, 3, 4, 5};
    for (const auto& element : mySet) {
        std::cout << element << " ";
    }
    std::cout << std::endl;

    return 0;
}

4.2 交集、并集和差集操作

可以使用 <algorithm> 头文件中的 set_intersection()set_union() 和 set_difference() 函数来实现 set 的交集、并集和差集操作。

#include <iostream>
#include <set>
#include <algorithm>
#include <vector>

int main() {
    std::set<int> set1 = {1, 2, 3, 4, 5};
    std::set<int> set2 = {3, 4, 5, 6, 7};

    std::vector<int> intersection;
    std::set_intersection(set1.begin(), set1.end(), set2.begin(), set2.end(), std::back_inserter(intersection));
    std::cout << "Intersection: ";
    for (const auto& element : intersection) {
        std::cout << element << " ";
    }
    std::cout << std::endl;

    std::vector<int> unionSet;
    std::set_union(set1.begin(), set1.end(), set2.begin(), set2.end(), std::back_inserter(unionSet));
    std::cout << "Union: ";
    for (const auto& element : unionSet) {
        std::cout << element << " ";
    }
    std::cout << std::endl;

    std::vector<int> difference;
    std::set_difference(set1.begin(), set1.end(), set2.begin(), set2.end(), std::back_inserter(difference));
    std::cout << "Difference (set1 - set2): ";
    for (const auto& element : difference) {
        std::cout << element << " ";
    }
    std::cout << std::endl;

    return 0;
}

4.3 与其他容器的结合使用

set 可以与其他容器(如 vectorlist 等)结合使用,以实现更复杂的功能。例如,可以将 vector 中的元素插入到 set 中进行去重和排序。

#include <iostream>
#include <set>
#include <vector>

int main() {
    std::vector<int> vec = {3, 1, 2, 3, 4, 2};
    std::set<int> mySet(vec.begin(), vec.end());

    for (const auto& element : mySet) {
        std::cout << element << " ";
    }
    std::cout << std::endl;

    return 0;
}

4.4 迭代器操作

std::set<int> s = {1, 3, 5, 7, 9};

// 反向迭代器
std::cout << "反向遍历: ";
for (auto rit = s.rbegin(); rit != s.rend(); ++rit) {
    std::cout << *rit << " ";
}

// 迭代器失效处理
auto it = s.find(5);
s.erase(it); // 迭代器失效,不可继续使用

4.5 性能优化技巧

批量插入优化:

std::set<int> s;
s.insert({1, 2, 3, 4, 5}); // C++11初始化列表优化

emplace原位构造:

s.emplace(10); // 直接在容器内构造对象,避免临时对象拷贝

预留空间(C++11起): 

s.reserve(100); // 预分配内存减少重分配次数

五、set 与其他容器的比较

操作 set unordered_set vector有序
插入 O(log n) O(1)平均 O(n)
查找 O(log n) O(1)平均 O(log n)二分
删除 O(log n) O(1)平均 O(n)
内存占用 较高 较低 紧凑
有序性 始终有序 无序 需排序

5.1 set 与 vector

  • 存储方式vector 是一种顺序容器,它使用连续的内存空间存储元素;而 set 是一种关联容器,使用红黑树存储元素。
  • 元素特性vector 允许有重复的元素,并且元素的顺序是按照插入的顺序排列的;set 不允许有重复的元素,并且元素会自动排序。
  • 查找效率vector 的查找操作的时间复杂度为 O(n),而 set 的查找操作的时间复杂度为 O(logn)。

5.2 set 与 unordered_set

  • 底层实现set 使用红黑树作为底层数据结构,而 unordered_set 使用哈希表作为底层数据结构。
  • 排序特性set 中的元素会自动排序,而 unordered_set 中的元素是无序的。
  • 查找效率unordered_set 的查找操作的平均时间复杂度为 O(1),而 set 的查找操作的时间复杂度为 O(logn)。

5.3 选择建议

  • 如果需要存储唯一的元素并且要求元素有序,那么可以选择 set
  • 如果只需要存储唯一的元素,不关心元素的顺序,并且对查找效率有较高的要求,那么可以选择 unordered_set
  • 如果需要存储多个相同的元素,并且对元素的顺序有要求,那么可以选择 vector

六、注意事项与常见错误

6.1 迭代器失效问题

在对 set 进行插入或删除操作时,可能会导致迭代器失效。例如,在删除一个元素后,指向该元素的迭代器将失效。因此,在使用迭代器时,需要特别注意避免迭代器失效的问题。

#include <iostream>
#include <set>

int main() {
    std::set<int> mySet = {1, 2, 3, 4, 5};
    auto it = mySet.find(3);
    if (it != mySet.end()) {
        mySet.erase(it); // 删除元素后,it 迭代器失效
        // 不能再使用 it 迭代器
    }

    return 0;
}

6.2 性能考虑

虽然 set 的插入、删除和查找操作的时间复杂度都是 O(logn),但在某些情况下,可能会有性能瓶颈。例如,当需要频繁插入和删除元素时,红黑树的旋转和着色操作可能会影响性能。此时,可以考虑使用 unordered_set 来提高性能。

6.3 内存占用

由于 set 使用红黑树作为底层数据结构,它需要额外的内存来存储节点的指针和颜色信息。因此,在对内存占用有严格要求的场景下,需要谨慎使用 set

6.4 易错点提醒

修改元素值:直接修改元素会导致未定义行为

auto it = s.begin();
// *it = 10;  // 错误!元素是const的

迭代器失效:仅删除操作会使指向被删元素的迭代器失效

自定义比较函数:需保证严格弱序关系

// 错误示例:未实现严格弱序
struct BadCompare {
    bool operator()(int a, int b) {
        return a <= b;  // 应该用<
    }
};

七、应用场景与实战案例

案例1:数据去重排序

std::vector<int> nums = {5, 2, 5, 1, 3, 2};
std::set<int> unique_nums(nums.begin(), nums.end());

// 输出:1 2 3 5
for(int num : unique_nums) {
    std::cout << num << " ";
}

案例2:字典序管理

std::set<std::string> dictionary;

void add_word(const std::string& word) {
    dictionary.insert(word);
}

bool check_spelling(const std::string& word) {
    return dictionary.count(word);
}

八、总结

set作为STL中的有序唯一集合容器,在需要维护有序数据且保证元素唯一性的场景中表现卓越。通过红黑树实现的高效操作使其成为处理排序、去重、快速查找等需求的理想选择。掌握set的特性和正确使用方式,将显著提升C++编程能力。

九、参考资料

  •  《C++ Primer(第 5 版)》这本书是 C++ 领域的经典之作,对 C++ 的基础语法和高级特性都有深入讲解。
  • 《Effective C++(第 3 版)》书中包含了很多 C++ 编程的实用建议和最佳实践。
  • 《C++ Templates: The Complete Guide(第 2 版)》该书聚焦于 C++ 模板编程,而using声明在模板编程中有着重要应用,如定义模板类型别名等。
  • C++ 官方标准文档:C++ 标准文档是最权威的参考资料,可以查阅最新的 C++ 标准(如 C++11、C++14、C++17、C++20 等)文档。例如,ISO/IEC 14882:2020 是 C++20 标准的文档,可从相关渠道获取其详细内容。
  • :这是一个非常全面的 C++ 在线参考网站,提供了详细的 C++ 语言和标准库文档。
  • :该网站提供了系统的 C++ 教程,配有丰富的示例代码和清晰的解释,适合初学者学习和理解相关知识。
  • 《Effective STL》Scott Meyers

  • 开源项目STL源码分析