好的,我们来深入探讨C++标准模板库(STL)中的setmultiset容器,重点关注其接口使用和核心特性。

一、核心概念与特性

  1. 有序关联容器

    • setmultiset都是有序的关联容器。它们存储元素(键),并根据元素自身的值(或指定的比较函数)进行自动排序
    • 元素在容器中总是保持特定的顺序(默认为升序),这使得基于范围的查询(如lower_bound, upper_bound)非常高效。
  2. 键值唯一性 - 核心区别:

    • set:容器中的键值必须是唯一的。尝试插入一个已存在的键值会被忽略(或返回失败信息)。
    • multiset允许键值重复。可以存储多个具有相同值的元素。
  3. 底层数据结构

    • 两者通常基于红黑树(Red-Black Tree) 实现。红黑树是一种自平衡的二叉搜索树(BST)
    • 平衡性:红黑树通过特定的着色规则和旋转操作,保证了树在插入和删除操作后仍能大致平衡,从而确保了最坏情况下插入、删除和查找操作的时间复杂度为$O(\log n)$。
  4. 不可修改键值

    • 由于元素在容器中的位置由其键值决定(用于排序),一旦元素被插入到setmultiset中,其键值(即元素本身)就不能被直接修改
    • 修改键值会破坏容器的有序性。如果需要修改元素,通常的做法是先删除旧元素,再插入新元素。
  5. 性能特点

    • 查找:$O(\log n)$(得益于二叉搜索树特性)
    • 插入:$O(\log n)$(查找插入位置 + 可能的树平衡操作)
    • 删除:$O(\log n)$(查找元素 + 可能的树平衡操作)
    • 范围查询:$O(\log n + k)$,其中$k$是范围内元素的数量(因为元素在树中是按顺序存储的,遍历子树即可)。
    • 内存:每个元素需要存储额外的信息(如颜色标记、父/子指针),因此空间开销比顺序容器(如vector)大。

二、关键接口使用

  1. 构造与赋值

    std::set<int> s1; // 空set,使用默认比较器 (operator<)
    std::set<int, std::greater<int>> s2; // 使用std::greater排序,降序
    std::set<int> s3 = {1, 3, 2}; // 初始化列表,元素自动排序 {1, 2, 3}
    std::multiset<int> ms1(s3.begin(), s3.end()); // 用迭代器范围构造multiset
    s1 = s3; // 赋值
    

  2. 插入元素

    • set
      std::pair<iterator, bool> insert(const value_type& val);
      // 尝试插入val。返回一个pair:
      //   pair.first: 指向被插入元素(或阻止插入的已有元素)的迭代器
      //   pair.second: bool值,true表示插入成功,false表示键已存在(插入被阻止)
      auto ret = s1.insert(4); // 插入新元素4, ret.second == true
      ret = s1.insert(3); // 尝试插入已存在的3, ret.second == false
      

    • multiset
      iterator insert(const value_type& val);
      // 总是插入val。返回指向新插入元素的迭代器。
      auto it = ms1.insert(3); // 成功插入一个3,返回指向这个新3的迭代器
      

  3. 查找元素

    • find(key):查找键等于key的元素,返回指向它的迭代器。如果没找到,返回end()。时间复杂度$O(\log n)$。
      auto it = s1.find(2); // 找到元素2
      if (it != s1.end()) { /* 找到了 */ }
      

    • count(key)
      • set:返回0或1(因为键唯一)。
      • multiset:返回键等于key的元素个数。时间复杂度$O(\log n + count)$。
      int cnt = ms1.count(3); // 统计值为3的元素个数
      

  4. 范围查询

    • lower_bound(key):返回指向第一个不小于key的元素的迭代器。
    • upper_bound(key):返回指向第一个大于key的元素的迭代器。
    • equal_range(key):返回一个pair<iterator, iterator>,表示键等于key的元素范围([first, last))。对于set,这个范围最多包含一个元素(如果存在)。
      // 在s1中查找所有 >= 2 且 < 4 的元素
      auto low = s1.lower_bound(2);
      auto high = s1.upper_bound(4);
      for (auto it = low; it != high; ++it) {
          std::cout << *it << " ";
      }
      // 或者使用 equal_range (对于multiset更常用)
      auto range = ms1.equal_range(3);
      for (auto it = range.first; it != range.second; ++it) {
          std::cout << *it << " "; // 输出所有3
      }
      

  5. 删除元素

    • erase(iterator pos):删除迭代器pos指向的元素。返回指向被删除元素之后元素的迭代器(或end())。
    • erase(key):删除所有键等于key的元素(对于set最多删一个)。
      • set:返回删除的元素个数(0或1)。
      • multiset:返回删除的元素个数。
    • erase(iterator first, iterator last):删除[first, last)范围内的元素。
      s1.erase(3); // 删除键为3的元素(如果存在)
      auto it = s1.find(2);
      if (it != s1.end()) {
          s1.erase(it); // 通过迭代器删除
      }
      ms1.erase(ms1.lower_bound(3), ms1.upper_bound(3)); // 删除multiset中所有3
      

  6. 大小与容量

    • size():返回容器中元素的数量。
    • empty():判断容器是否为空。
    • max_size():返回容器理论上能容纳的最大元素数(通常很大)。
  7. 迭代器

    • 提供双向迭代器(begin(), end(), cbegin(), cend(), rbegin(), rend(), crbegin(), crend())。
    • 迭代器按排序顺序遍历元素(升序或降序,取决于比较器)。
    • 注意:删除或插入元素可能使指向其他元素的迭代器失效(但指向被删除元素的迭代器总是失效)。指向未修改元素的迭代器通常不会失效(红黑树实现保证了这点)。

三、示例代码

#include <iostream>
#include <set>
#include <string>
#include <functional> // for std::greater

int main() {
    // Set (唯一键,升序)
    std::set<int> uniqueSet = {5, 2, 8, 2, 1}; // {1, 2, 5, 8}
    uniqueSet.insert(3); // 插入成功
    uniqueSet.insert(2); // 插入失败,键已存在

    std::cout << "Set contains: ";
    for (int elem : uniqueSet) {
        std::cout << elem << " "; // 输出: 1 2 3 5 8
    }
    std::cout << "\nCount of 2 in set: " << uniqueSet.count(2) << "\n"; // 输出: 1

    // Multiset (允许重复键,降序)
    std::multiset<int, std::greater<int>> multiSet = {5, 2, 8, 2, 1}; // {8, 5, 2, 2, 1}
    multiSet.insert(2); // 插入成功,现在有三个2

    std::cout << "Multiset contains: ";
    for (int elem : multiSet) {
        std::cout << elem << " "; // 输出: 8 5 2 2 2 1
    }
    std::cout << "\nCount of 2 in multiset: " << multiSet.count(2) << "\n"; // 输出: 3

    // 范围查询 (在multiset中查找所有等于2的元素)
    auto range = multiSet.equal_range(2);
    std::cout << "All '2's: ";
    for (auto it = range.first; it != range.second; ++it) {
        std::cout << *it << " "; // 输出: 2 2 2
    }
    std::cout << "\n";

    // 删除multiset中所有2
    multiSet.erase(2);
    std::cout << "Count of 2 after erase: " << multiSet.count(2) << "\n"; // 输出: 0

    return 0;
}

四、总结与提示

  • 选择依据:需要键唯一且有序,用set;需要有序但允许重复键,用multiset
  • 性能优势:在需要频繁查找、插入、删除且维护元素顺序的场景下表现优异($O(\log n)$)。
  • 键不可变:记住不能直接修改容器中的键值。
  • 自定义类型:如果元素是自定义类型(类或结构体),需要提供比较函数或重载operator<(或指定自定义比较器)。
  • 迭代器失效:删除操作会使指向被删除元素的迭代器失效。插入操作通常不会使其他迭代器失效(红黑树特性)。
  • 替代方案:如果不需要顺序,只关心存在性,考虑unordered_set/unordered_multiset(哈希表实现,平均$O(1)$操作,但不保证顺序)。

通过理解其底层红黑树实现和提供的接口,你可以更有效地在C++程序中使用setmultiset来解决需要有序存储和高效查询的问题。

更多推荐