C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++   set与 map使用

C++ STL关联式容器 set与 map使用指南

作者:Fuyo_1119

set和map是C++ STL里的关联式容器,底层都用红黑树实现,插入、删除、查找都是 O(log n) 复杂度,本文给大家介绍C++ STL关联式容器 set与map使用指南,感兴趣的朋友跟随小编一起看看吧

set 和 map 是 C++ STL 里的关联式容器,底层都用红黑树实现,插入、删除、查找都是 O(log n) 复杂度。set 适合“去重 + 排序 + 快速判断在不在”,map 适合“键值对映射 + 按键自动排序”。‌ 你已熟悉 Java 和 C++ 基础,可直接对比记忆:set 类似 Java 的 TreeSet,map 类似 TreeMap,但 C++ 的迭代器和 pair 用法更灵活。

1. 序列式容器和关联式容器

在 C++ STL 中,容器主要分为两大类:

1. 序列式容器:如 vector、list、deque 等,这些容器统称为序列式容器。因为逻辑结构为线性序列的数据结构,两个位置存储的值之间一般没有紧密的关联关系,比如交换一下,它依旧是序列式容器。顺序容器中的元素是按它们在容器中的存储位置来顺序保存和访问的。

2. 关联式容器:如 set、map、multiset、multimap,也是用来存储数据的,与序列式容器不同的是,关联式容器逻辑结构通常是非线性结构,两个位置有紧密的关联关系,交换一下,它的存储结构就被破坏了。关联式容器中的元素是按照 Key (键值)来保存和访问的。

关联式容器的底层几乎无一例外地采用了 红黑树(一种自平衡二叉搜索树)。这使得它们在进行插入、删除、查找等操作时,均能稳定保持 O ( log ⁡ N ) O(\log N) O(logN) 的时间复杂度。

2. 关联式容器 set 的核心用法

2.1 set和multiset 参考文档

set和multiset 参考文档

set 是存储单一键值(Key)的关联式容器。它有两个最核心的特性:自动排序与强制去重。
在 set 中,Value 就是 Key 本身。

2.1 set 的元素插入、去重与遍历限制

默认情况下,set 会使用 less<T> 仿函数将元素按升序排列。如果试图插入已存在的重复元素,插入操作将会宣告失败。

示例1:默认升序与去重机制

// 1. 默认升序且去重的 set
set<int> s;
s.insert(5);
s.insert(2);
s.insert(7);
// ⚠️ 尝试插入重复元素 5
// insert 返回一个 pair<iterator, bool>
// 插入失败时,bool 值为 false,iterator 指向已存在的节点 5
auto ret = s.insert(5);
if (!ret.second)
{
    cout << "元素 5 插入失败,已存在!" << endl;
}
cout << "迭代器遍历 set (自动升序 + 去重): ";
for (auto it = s.begin(); it != s.end(); ++it)
{
    cout << *it << " "; // 输出: 2 5 7
}
cout << endl;

⭐ 语法细节与行为机制:

示例 2:利用 initializer_list 批量插入

// 2. 利用 initializer_list 批量插入
s.insert({ 2, 8, 3, 9 });
cout << "批量插入后 (自动排序去重): ";
for (auto e : s)
{
    cout << e << " "; // 输出: 2 3 5 7 8 9
}
cout << endl;

⭐ 语法细节与行为机制:

示例 3:类型构造与 string 类型的 set

// 3. 隐式类型转换与 string 类型的 set
set<string> strset = { "sort", "insert", "add" };
cout << "字符串 set (按 ASCII 码序): ";
for (const auto& str : strset)
{
    cout << str << " "; // 输出: add insert sort
}
cout << endl;

⭐ 语法细节与行为机制:

2.2 set 的高效查找与多种删除方式

set 提供了按 Key 值删除 与 按迭代器删除两种形态,并且提供了专属的高效查找算法。

示例 1:按迭代器删除与越界风险

set<int> s = { 4, 2, 7, 2, 8, 5, 9 }; // 实际存储: 2 4 5 7 8 9
// 1. 按迭代器删除(删除最小值/头节点)
// ⚠️ 注意:必须保证迭代器有效!如果传递 s.end() 会导致未定义行为 (Segmentation Fault)
s.erase(s.begin()); // 删除 2

⭐ 语法细节与行为机制:

示例 2:按 Key 值直接删除

// 2. 按 Key 值直接删除
// 返回值是成功删除的元素个数(对于 set 来说只能是 0 或 1)
int x = 5;
size_t num = s.erase(x);
if (num == 0)
{
    cout << x << " 不存在,删除失败!" << endl;
}

⭐ 语法细节与行为机制:

示例 3:安全删除的标准范式(find + erase)

// 3. 安全删除的标准范式:先查找,校验后再用 iterator 删除
int x = 7;
auto pos = s.find(x);
if (pos != s.end())
{
    s.erase(pos); // 确认 pos != end() 后再删除,绝对安全
}

⭐ 语法细节与行为机制:

示例 4:利用 count 间接判断存在性

// 4. 利用 count 间接判断存在性
int x = 8;
if (s.count(x)) // 返回 1 即为存在
{
    cout << x << " 存在于 set 中!" << endl;
}

⭐ 语法细节与行为机制:

2.3 区间提取:lower_bound 与 upper_bound

当我们需要提取或删除某个闭区间 [ A , B ] [A, B] [A,B] 内的所有元素时,C++ 关联式容器提供了 lower_bound 与 upper_bound 两个效率极高的成员函数。

示例 1:界限迭代器的查找

std::set<int> myset;
for (int i = 1; i < 10; i++)
    myset.insert(i * 10); // 存储: 10 20 30 40 50 60 70 80 90
// 目标:精准删除闭区间 [30, 60] 内的所有元素
// lower_bound(25): 返回第一个 Key >= 25 的节点迭代器 (即 30)
auto itlow = myset.lower_bound(25);
// upper_bound(60): 返回第一个 Key > 60 的节点迭代器 (即 70)
auto itup = myset.upper_bound(60);

⭐ 语法细节与行为机制:

示例 2:利用界限迭代器进行区间删除

// s.erase(first, last) 删除的是左闭右开区间 [first, last)
// 即删除 [30, 70) -> 包含 30, 40, 50, 60
myset.erase(itlow, itup);
cout << "区间删除后剩余元素: ";
for (auto e : myset) cout << e << " "; // 输出: 10 20 70 80 90
cout << endl;

⭐ 语法细节与行为机制:

2.4 multiset:允许键值重复的变体容器

multiset 与 set 同样定义在 <set> 头文件中,接口几乎完全一致。
唯一区别是multiset允许插入 Key 相同的节点。(允许键值重复)

示例 1:multiset 中 find 的返回位置机制

multiset<int> s = { 4, 2, 7, 2, 4, 8, 4, 5, 4, 9 };
// 排序后存储: 2 2 4 4 4 4 5 7 8 9
// 1. multiset 的 find 机制
int x = 4;
auto pos = s.find(x); // 关键:当有多个 4 时,find 返回哪一个?
// STL 规范保证:find 返回的是按中序遍历遇到的第一个 key == 4 的节点迭代器
cout << "顺序遍历所有的 4: ";
while (pos != s.end() && *pos == x)
{
    cout << *pos << " "; // 连续输出所有的 4
    ++pos;
}
cout << endl;

⭐ 语法细节与行为机制:

示例 2:count 与两种不同参的 erase 操作

// 2. count 与 erase 的差异
cout << "4 出现的实际次数: " << s.count(x) << endl; // 输出 4
// ⚠️ 重点区分 erase 的两种传参:
// 方式 A: s.erase(x) -> 传入 Key,会抹去容器中所有 Key == x 的节点,返回删除数量
// 方式 B: s.erase(s.find(x)) -> 传入迭代器,仅删除当前这单个节点!
s.erase(x); // 删除所有的 4
cout << "删除所有的 4 后,count(4) = " << s.count(4) << endl; // 输出 0

⭐ 语法细节与行为机制:

3. 键值对结构 std::pair 与 map 的关系

map 是存储键值对(Key-Value)的关联容器。在深入 map 前,需要先了解 C++ 用于封装映射关系的基础结构 —— std::pair。

3.1std::pair接口结构与类型定义

在 STL 源码中,pair 被定义为一个简易的类模板结构体:

template <class T1, class T2>
struct pair
{
    typedef T1 first_type;
    typedef T2 second_type;
    T1 first;  // 键 Key
    T2 second; // 值 Value
    // 默认构造函数
    pair() : first(T1()), second(T2()) {}
    // 值构造函数
    pair(const T1& a, const T2& b) : first(a), second(b) {}
    // 支持隐式类型转换的拷贝构造函数
    template<class U, class V>
    pair(const pair<U, V>& pr) : first(pr.first), second(pr.second) {}
};

4. 映射容器 map 的核心用法与场景

map 容器将 Key 作为排序和去重的依据,而 Value 作为与 Key 绑定的映射数据。

4.1 map 的四种常用插入方式

向 map 中插入节点需要构造 pair<const Key, Value>,下面逐一拆解四种常见写法:

示例 1:有名对象与匿名对象构造插入

map<string, string> dict;
// 写法 1:定义有名 pair 对象
pair<string, string> kv1("left", "左边");
dict.insert(kv1);
// 写法 2:构造匿名 pair 对象
dict.insert(pair<string, string>("right", "右边"));

⭐ 语法细节与行为机制:

示例 2:make_pair函数模板构造(最推荐)

// 写法 3:利用 make_pair 函数模板 (最推荐,自动推导类型)
dict.insert(make_pair("sort", "排序"));

⭐ 语法细节与行为机制:

示例 3:C++11 列表初始化构造

// 写法 4:C++11 列表初始化 (隐式类型转换)
dict.insert({ "string", "字符串" });

⭐ 语法细节与行为机制:

示例 4:验证 Key 重复时的插入行为

// ⚠️ 验证 Key 重复时的插入行为
// insert 返回 pair<iterator, bool>
auto ret = dict.insert({ "left", "左边,剩余" });
if (!ret.second)
{
    cout << "插入失败!Key ['left'] 已存在,对应的 Value 仍为: " 
         << ret.first->second << endl;
}

⭐ 语法细节与行为机制:

4.2 迭代器访问与“双箭头”现象解密

示例 1:通过解引用(*)访问成员

map<string, string> dict = { {"left", "左边"}, {"right", "右边"} };
auto it = dict.begin();
// 写法 A:先解引用获取 pair 对象,再通过 . 访问成员
cout << (*it).first << ":" << (*it).second << endl;

⭐ 语法细节与行为机制:

示例 2:通过指针重载(->)与语法简化

// 写法 B:直接使用指针重载运算符 -> (最常用写法)
cout << it->first << ":" << it->second << endl;
// 修改限制验证:
// it->first += "x";   // ❌ 编译报错!Key 为 const,无法修改
it->second += " (已核对)"; // ✅ 合法!Value 可自由修改

⭐ 语法细节与行为机制(双箭头 -> 的语法简化):

4.3 核心接口:operator[]的逻辑与防坑指南

4.3.1operator[]的内部逻辑展开

在 STL 标准实现中,operator[] 的功能彻底依赖于 insert() 函数。其逻辑等价于:

// operator[] 的等价逻辑实现
mapped_type& operator[] (const key_type& k)
{
    // 1. 尝试插入 {k, mapped_type()}
    // mapped_type() 是 Value 的默认构造值 (如 int 为 0,string 为 "")
    pair<iterator, bool> ret = this->insert(make_pair(k, mapped_type()));
    // 2. 无论插入成功与否,ret.first 都是指向该 Key 节点的迭代器
    iterator it = ret.first;
    // 3. 返回节点中 second (Value) 的引用
    return it->second;
}

4.3.2operator[]的四大细分场景拆解

为了深刻理解 operator[] 的机制,我们把它拆解为四个单独的场景:

场景 1:Key不存在,纯插入
map<string, string> dict;
// 场景 1:Key 不存在 -> 纯插入 {"insert", ""}
dict["insert"];

⭐ 行为剖析:由于字典里没有 "insert",程序会使用 string() 的默认构造(即 "")补全 Value 并插入到 map 中。

场景 2:Key不存在,插入 + 修改Value
// 场景 2:Key 不存在 -> 插入 + 修改 Value
dict["left"] = "左边";

⭐ 行为剖析:首先插入节点 {"left", ""},随后 operator[] 返回对节点 Value 的引用,最后通过赋值运算符 = 将该引用修改为了 “左边”。

场景 3:Key存在,纯修改Value
// 场景 3:Key 存在 -> 纯修改 Value
dict["left"] = "左边、剩余";

⭐ 行为剖析:insert 检测到 "left" 已存在,插入失败,但依然返回了指向已存在节点的迭代器;后续的赋值操作把原本的 "左边" 覆写成了 "左边、剩余"。

场景 4:Key确定存在,读取Value
// 场景 4:Key 确定存在 -> 读取 Value
cout << dict["left"] << endl; // 输出: 左边、剩余

⭐ 行为剖析:直接拿到关联节点的 Value 引用并读取输出。

4.3.3 防坑指南:只读场景滥用 operator[] 的安全隐患💥

// ⚠️ 错误示范:只是想查一下 "apple" 是否存在,却引发了非预期插入
if (dict["apple"] == "苹果") 
{
    // ...
}

⭐ 语法细节与防坑逻辑:

4.4 实战案例:词频统计的代码演进

通过“水果词频统计”场景,感受 operator[] 带来的代码简化。

方案一:使用find + insert显式处理(旧写法)

string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "香蕉" };
map<string, int> countMap;
// 1. 使用 find 显式判断 (逻辑直接,但代码较多)
for (const auto& str : arr)
{
    auto pos = countMap.find(str);
    if (pos == countMap.end())
    {
        countMap.insert(make_pair(str, 1)); // 不存在则插入计数 1
    }
    else
    {
        pos->second++; // 存在则计数自增
    }
}

⭐ 行为剖析:逻辑严密,但每次都需要手动执行分支判断,代码行数较多。

方案二:利用operator[]一行代码搞定(推荐写法✅️)

countMap.clear();
// 2. 利用 operator[] 一行代码搞定!
for (const auto& str : arr)
{
    countMap[str]++; 
}
// 打印结果
for (const auto& kv : countMap)
{
    cout << kv.first << " 出现了: " << kv.second << " 次" << endl;
}
countMap.clear();
// 2. 利用 operator[] 一行代码搞定!
for (const auto& str : arr)
{
    countMap[str]++; 
}
// 打印结果
for (const auto& kv : countMap)
{
    cout << kv.first << " 出现了: " << kv.second << " 次" << endl;
}

⭐ countMap[str]++ 的内部执行轨迹:

到此这篇关于C++ STL关联式容器 set与 map使用指南的文章就介绍到这了,更多相关C++ set与 map使用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文