C++ STL 中向 Set 插入元素的各种方法

2025年5月20日 | 阅读17分钟

在竞争性编程、软件开发和系统编程领域,高效地管理元素的唯一集合是一个常见的需求。C++ 的标准模板库 (STL) 中的 set 容器完美地满足了这一需求。作为 STL 的基础数据结构之一,set 提供了独特的特性,使其非常适合各种应用,从处理唯一的用户名到维护排序数据集合。set 的独特性源于其两个主要特性:它只存储唯一元素,并且它以特定的、排序的方式维护这些元素。本文将深入探讨向 set 中插入元素的各种方法,理解每种方法及其对性能和易用性的影响。

C++ 中的 set 是关联容器家族的一部分,这意味着它们的组织方式允许基于键值快速检索元素。它们通常实现为平衡二叉搜索树,通常是红黑树,这为键操作(如插入、删除和搜索)提供了对数复杂度(O(log n))。这种底层结构使得 set 能够保持其排序顺序并消除重复项。set 的独特性质使其非常适合不允许重复值且顺序很重要的场景。例如,在管理唯一记录、计算不同元素数量或确保排序集合时,set 是 C++ 程序员工具箱中的一个绝佳工具。

在 C++ 中,set 定义在 <set> 库中,其声明与指定它将容纳的元素数据类型一样简单。Set 可以存储各种类型的数据,例如整数、字符串,甚至支持比较操作的自定义对象。以下是整数 set 的示例声明:

这一行就设置了一个空的 set,可以容纳整数值,确保它们保持唯一且按升序排列。当我们探索向 set 插入元素的各种方法时,需要注意尝试插入重复元素不会改变 set,只保留每个唯一值的一个实例。

C++ 提供了多种向 set 插入元素的方法,每种方法都针对特定情况进行了优化。一些最常用的方法包括:

  • 使用 insert():这是添加单个元素最基本、最常用的函数。它尝试将一个元素插入到 set 中,并返回一个包含指向该元素的迭代器和一个指示插入是否成功的布尔值的 pair。如果元素已存在于 set 中,则不会再次添加,并且布尔值将返回 false。
  • 使用 emplace():与 insert() 类似,emplace() 函数添加元素到 set 中,但对于复杂对象,效率稍高。emplace() 不是在 set 外部构造一个对象然后将其传递给 insert(),而是直接在 set 的内存中构造该对象,消除了额外的复制或移动的需要。这在处理自定义类或大型数据结构时尤其有益。
  • 插入元素范围:有时,需要从另一个容器(如数组、vector 或另一个 set)添加大量元素。在这种情况下,逐个插入元素效率不高。set 容器允许插入来自另一个容器的元素范围,这更快、更简洁。这种方法非常适合用现有集合中的数据初始化 set。
  • 使用 initializer_list:使用预定义的列表值初始化 set 或在单个语句中添加多个元素时,initializer_list 提供了一种简洁的语法。此方法允许我们在花括号 {} 中列出值,然后一次性将它们添加到 set 中。

这些方法中的每一种都根据用例提供了独特的优势。例如,insert() 最适合偶尔添加,emplace() 可以提高复杂数据类型的性能,而范围插入或 initializer_list 对批量操作很有效。

在本文中,我们将深入研究这些方法,比较它们的性能和易用性。通过理解每种插入方法的优点和局限性,C++ 开发人员可以更好地利用 set 容器来高效地管理唯一集合,无论是用于竞争性编程还是大规模应用程序。

与 Set 关联的关键函数

在 C++ 中,标准模板库 (STL) 中的 set 容器提供了一系列强大的函数,使开发人员能够轻松管理唯一元素的集合。由于 set 容器的平衡二叉搜索树结构,这些函数以对数复杂度实现了插入、删除、搜索和其他 set 操作。下面,我们将探讨与 set 关联的一些关键函数及其实际应用。

  • insert():insert() 函数用于向 set 中添加元素。如果元素已存在,set 不允许重复,因此会忽略插入请求。此函数返回一个 pair:pair 的第一个元素是一个指向插入元素(或已存在的元素)的迭代器,第二个是一个布尔值,指示成功(true)还是失败(false)。这使得可以轻松检查插入尝试是否成功。
  • emplace():与 insert() 类似,emplace() 是添加元素的更有效的方法,尤其是对于复杂对象。它在原地构造元素,减少了不必要的复制或移动。此函数非常适合在插入需要首先在 set 外部构造的对象的场景。
  • size():size() 函数返回 set 中的元素数量,这对于了解集合的范围或当条件基于唯一元素数量时特别有用。
  • clear():为了同时删除 set 中的所有元素,clear() 函数提供了一种重置 set 的有效方法。调用 clear() 后,set 会变为空,但仍然会分配内存,并可以接受新元素。
  • find():find() 函数搜索特定元素,如果找到则返回指向它的迭代器。如果元素不存在,则返回 set::end()。此函数有助于以恒定时间(对数复杂度)检查成员资格。
  • count():此函数返回给定元素在 set 中的出现次数。由于 set 包含唯一值,count() 将返回 0(如果元素不存在)或 1(如果存在)。这是一种快速检查元素是否存在的方法。

这些函数中的每一个都使得 set 容器高度通用,使其成为 C++ 中各种需要唯一、排序数据集合的应用程序的有用工具。

方法 1:使用 insert()

insert() 函数是用于向 C++ STL 的 set 中添加元素的主要方法之一。insert() 设计用于维护 set 容器的唯一和有序特性,确保不会添加重复元素。当使用 insert() 添加元素时,函数会检查元素是否已存在于 set 中。如果存在,函数不会再次添加它,从而有效地防止了重复。

基本用法和语法

insert() 函数易于使用。insert() 最常见的语法是:

在此,value 是您要添加到 set 中的元素。由于 set 默认以排序方式维护元素,因此新元素会自动放置在 set 中的正确位置。

insert() 的返回值

  • insert() 的一个独特之处在于其返回类型。该函数返回一个 std::pair,其中包含两个元素:
  • 一个指向插入元素(或已存在的元素)的迭代器。
  • 一个布尔值,指示插入是否成功。

此返回值使我们能够知道元素是否成功添加,或者它是否已存在。以下是返回值的用法:

输出

5 inserted successfully.   

在此示例中,如果 5 尚不存在于 mySet 中,insert() 将添加它,并将 true 作为 pair 的第二个值返回。如果 5 已存在,则函数返回 false,表示插入未发生。

使用 insert() 进行多次插入

insert() 函数还可以通过接受迭代器范围作为参数来处理多次插入。这对于在单个操作中从另一个容器(如 vector 或 set)添加元素很有用:

输出

1 2 3 4 5   

在此,insert(vec.begin(), vec.end()) 将 vector vec 中的所有元素插入到 set1 中,确保只添加唯一元素。

性能考虑

由于 set 内部的平衡树结构,insert() 函数以对数复杂度 O(log n) 运行。这种效率非常适合需要频繁插入和搜索的场景,在不允许重复的大集合中尤其有用。

常见用例

  • 唯一元素集合:在需要唯一值的情况下,例如存储唯一 ID 或从列表中删除重复项,insert() 提供了一种简单有效的工具。
  • 排序集合:由于 set 是有序容器,使用 insert() 添加元素意味着 set 始终保持排序状态,这对于有序数据管理很有用。
  • 插入前检查存在性:insert() 返回的布尔值可以轻松检查元素是否已在 set 中,这对于基于唯一性的条件逻辑很有用。

总而言之,insert() 是 C++ set 中的一个基本函数,它将简单性与自动排序和唯一性管理功能相结合。

方法 2:使用 emplace()

在 C++ STL 中,emplace() 函数提供了一种替代且通常更有效的方法来将元素插入到 set 等容器中。虽然 emplace() 与 insert() 函数的目的相似,但它在特定场景下具有优势,尤其是在处理复杂对象或自定义数据结构时。emplace() 与 insert() 的关键区别在于,emplace() 直接在容器内构造对象,避免了在 set 外部创建对象然后将其移动或复制到 set 中的额外步骤。这种就地构造可以使 emplace() 更快、更有效,尤其是在处理大型或非平凡复制对象时。

基本用法和语法

emplace() 的语法很简单:

在这种情况下,value 是您要添加到 set 中的元素。对于 int 或 double 等基本类型,emplace() 和 insert() 之间没有明显的性能差异。但是,对于复杂的数据类型,emplace() 可以帮助减少开销并提高效率。

insert() 和 emplace() 之间的区别

  • 虽然 insert() 和 emplace() 都旨在将元素添加到 set 中,但它们在处理元素构造方面有所不同:
  • insert():需要先在 set 外部构造一个元素,然后将其传入。这可能导致不必要的复制或移动,尤其是在使用非平凡数据类型时。
  • emplace():直接在 set 的内存空间中构造元素,无需额外的复制或移动。
  • 例如,如果您有一个需要复杂构造或初始化的自定义对象 set,emplace() 允许您直接传递必要的参数,然后该函数将在原地使用这些参数构造对象。

emplace() 的示例

考虑一个存储自定义类 Person 对象的 set,该类在其构造函数中接受姓名和年龄作为参数:

输出

Person created: Alice, 30
Person created: Bob, 25
Bob (25 years old)
Alice (30 years old)   

在此示例中,emplace() 直接在 set 中构造 Person 对象,避免了额外的复制。Person 构造函数每次创建对象时都会输出一条消息,这使我们能够验证构造是就地进行的。

emplace() 的返回值

  • 与 insert() 类似,emplace() 返回一个 std::pair:
  • 第一个元素是指向 set 中元素的迭代器。
  • 第二个元素是一个布尔值,指示插入是否成功(true)或元素是否已存在(false)。
  • 这种行为允许我们在不单独搜索的情况下检查元素的添加是否成功。

性能考虑

当添加复杂对象时,使用 emplace() 特别有益,因为它可以直接在容器内构造对象,从而减少不必要的复制或移动。对于简单数据类型,insert() 和 emplace() 之间的性能差异可以忽略不计。但是,对于用户定义的类型(如结构体或类),特别是具有多个字段的类型,emplace() 可以通过避免临时对象来提高性能。

实际用例

  • 复杂对象初始化:对于具有多个字段或自定义构造函数的对象,emplace() 允许您直接指定构造函数参数,减少了额外代码行的需要,并避免了临时对象。
  • 优化性能:在性能至关重要的场景中,例如实时应用程序或高性能系统,emplace() 可以帮助减少开销。
  • 有条件对象创建:emplace() 方法仅当 set 不包含等效元素时才构造对象,这可以避免昂贵的重复元素构造。

总之,emplace() 提供了一种高效的向 set 添加元素的方法,尤其是在处理复杂对象时,因为它避免了额外的内存分配和复制操作。通过了解何时使用 emplace() 而不是 insert(),开发人员可以实现更高效的代码并在其C++ 程序中优化内存使用。

方法 3:插入元素范围

在 C++ 的标准模板库 (STL) 中,set 容器支持通过范围插入在单个操作中插入多个元素。当您需要从另一个容器(如 vector、数组甚至另一个 set)添加一批元素时,此方法特别有用,而无需为每个单独的元素反复调用 insert() 或 emplace()。范围插入提高了代码的可读性,在某些情况下也提高了性能,因为它减少了填充 set 所需的操作次数。

范围插入方法接受两个迭代器作为参数,定义要插入的元素范围的开始和结束。调用时,它会遍历指定的范围并将每个唯一元素插入到 set 中。由于 set 只存储唯一值,因此范围中的任何重复元素都将被忽略。

语法和基本用法

set 中范围插入的语法如下:

其中

  • start_iterator 是指向源容器中范围开头的迭代器。
  • end_iterator 是指向范围末尾的迭代器。

此语法允许您定义容器的任何片段来插入到 set 中,使其适用于各种应用。

范围插入的示例

让我们看一个将 vector 中的元素范围插入到 set 中的示例:

输出

Elements in the set: 1 2 3 4 5 6   

在此示例中,numbers vector 包含重复值(5 出现两次),但当我们将范围 numbers.begin() 到 numbers.end() 插入到 uniqueNumbers 中时,set 中只保留了唯一值。set 维护唯一性的此属性在插入范围时会自动应用,帮助您清理重复项而无需额外代码。

范围插入的实际应用

范围插入在以下场景中特别有用:

  • 从集合中删除重复项:如果您有一个包含重复值的列表或 vector,并想创建一个唯一元素集合,使用带有范围插入的 set 是实现此目的的有效方法。它会自动丢弃重复项并将元素按排序顺序组织。
  • 合并容器:当将两个容器合并到 set 中时,范围插入提供了一种有效的方法来组合元素,同时保持唯一性和顺序。例如,您可以使用范围插入将两个 vector 合并到一个 set 中,创建一个排序的唯一元素集合。
  • 在容器之间传输数据:如果您有一个容器(如数组或列表)包含您想要添加到 set 的部分数据,范围插入允许选择性插入。例如,仅插入大型数据集中的特定元素范围在过滤或分段数据时可能很有益。

与其他容器的范围插入

范围插入不仅限于 vector 和 数组。您还可以将其与其他 STL 容器一起使用,例如 set 或 list。例如,如果您有两个 set 并想合并它们,范围插入允许您将一个 set 的内容插入到另一个 set 中,而无需手动遍历每个元素。

输出

1 2 3 4 5   

在此示例中,set2 有一个元素(3)与 set1 重叠,但在范围插入后,set1 中只保留每个元素的实例。此代码优雅地合并了两个 set,同时使它们保持排序且没有重复项。

性能考虑

范围插入通常效率很高,由于 set 底层的平衡二叉树结构,每次插入操作的复杂度为 O(log n)。在处理大型数据集时,范围插入通过在单个调用中执行所有插入来节省时间和代码复杂性。这种方法比在循环中使用单个 insert() 调用更优,后者会由于重复的函数调用而引入额外的开销。

范围插入是一种强大的方法,用于从其他容器或数据子集填充 set。它提供了简洁性,有助于减少重复,并确保元素保持排序和唯一。通过理解和利用范围插入,C++ 开发人员可以在处理唯一、有序数据集合时简化其代码。

方法 4:使用 initializer_list

C++ 中的 initializer_list 提供了一种简洁方便的方法,可以在声明或插入时用固定列表的值初始化容器。对于 C++ 中的 set 容器,initializer_list 使开发人员能够在一行语句中插入多个元素,从而在需要将特定值添加到 set 时简化代码。此功能对于少量已知值集合特别有用,并且通常用于初始化或快速测试。

initializer_list 随 C++11 一起引入,允许在 {} 花括号内指定元素,而无需为每个元素显式调用 insert() 或 emplace()。语法更清晰、更易读,非常适合简洁性和清晰性是优先级的场景。在这里,我们将探讨 initializer_list 如何与 set 配合使用,以及它何时最有利。

基本用法和语法

当使用 initializer_list 和 set 时,您可以使用 {} 包围的值列表来初始化或向 set 添加元素。然后,该列表会被隐式转换为 initializer_list 类型,set 可以对其进行处理以进行插入。

语法如下:

或者,如果您已经有一个 set 并想使用 initializer_list 添加元素,您可以直接调用带有列表的 insert:

这样,initializer_list 提供了一种简单的单行解决方案来插入多个值。

使用 initializer_list 和 set 的示例

这是一个简单的示例,演示了如何用多个值初始化 set,然后稍后使用 initializer_list 语法添加其他值:

输出

Initial set: 10 20 30 40 50 
After insertion: 10 20 30 40 50 60 70 80   

在此示例中,mySet 首先用值 {10, 20, 30, 40, 50} 初始化。然后,使用 insert() 的 initializer_list 语法插入其他值 {60, 70, 80}。结果是一个包含所有唯一值且按排序顺序排列的 set。

使用 initializer_list 和 set 的好处

  • 可读性和简洁性:initializer_list 允许更简洁、更易读地初始化或插入元素,尤其是在预先知道值的情况下。它减少了多次调用 insert() 的需要,使代码更整洁。
  • 最适合小的固定集合:在处理少量预定义元素(如常量或设置)时,initializer_list 非常高效,并使代码更具表现力。
  • 消除冗余代码:通过将所有元素分组在 {} 中,initializer_list 减少了冗余,并消除了单独插入的需要,这在单元测试、示例或初始配置中尤其有用。

性能考虑

initializer_list 的性能取决于要插入的元素数量和 set 的当前大小。对于少量元素,initializer_list 性能良好,因为它避免了对单个项目多次调用 insert()。在内部,列表中的每个元素都会被依次处理并添加到 set 中,遵守容器关于唯一性和顺序的规则。

但是,值得注意的是,如果 set 很大或需要从 initializer_list 插入许多元素,由于 set 底层平衡二叉树结构的平衡,每次插入仍然需要 O(log n) 的复杂度。对于具有许多项目的批量插入,其他方法(如范围插入)可能提供更好的性能。

实际用例

  • 快速初始化:initializer_list 在单行中用预定义值初始化小型 set 尤其有用,通常用于配置或常量值。
  • 函数参数:有时,函数可能需要根据参数插入一组特定的值。将 initializer_list 传递给函数可以简化这一点。
  • 测试和原型设计:在测试或原型设计期间,initializer_list 允许开发人员无需多次调用 insert 即可快速定义具有特定值的 set。
  • 与其他容器结合:initializer_list 在插入硬编码值以及来自其他容器的元素时很有用,例如在从 vector 进行范围插入后附加特定项。

使用 initializer_list 向 set 插入元素提供了一种高度可读且高效的方法来处理少量已知值集合。此方法减少了初始化或插入值所需的代码,确保了简洁性和表现力。虽然它最适合小型 set,但由于性能考虑,initializer_list 可能不是大型数据集的最佳选择,但对于快速、固定值的插入,它是 C++ 开发人员的绝佳工具。


下一个主题C++ 中的鸭子数