C++语言原理与实践(十八):set和map的使用

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

C++ STL 中的容器可以分为序列式容器和关联式容器。

常见的序列式容器包括:

复制代码
string、vector、list、deque、array、forward_list

这些容器按照元素在容器中的位置保存和访问数据,数据之间通常是线性关系。

关联式容器则按照关键字保存和访问数据,常见的关联式容器包括:

复制代码
set、multiset、map、multimap

set 和 map 的底层通常使用红黑树实现。红黑树是一棵近似平衡的二叉搜索树,因此它们的插入、删除和查找操作的时间复杂度通常为:

复制代码
O(log N)

二者的主要使用场景如下:

  • set:保存关键字,适合搜索、排序和去重。

  • map:保存键值对,适合建立关键字和数据之间的映射关系。


2. set 系列容器

2.1 set 的特点

set 的基本声明如下:

复制代码
template<
    class T,
    class Compare = less<T>,
    class Alloc = allocator<T>
>
class set;

其中:

  • T 是容器中元素的类型。

  • Compare 是比较规则,默认使用 less<T>。

  • Alloc 是空间配置器,一般不需要手动设置。

set 具有以下特点:

  1. 元素不能重复。

  2. 元素会自动排序。

  3. 默认按照升序排列。

  4. 插入、删除和查找的时间复杂度通常为 O(log N)。

  5. 不能通过迭代器修改元素。

2.2 set 的构造

无参构造

复制代码
set<int> values;

初始化列表构造

复制代码
set<int> values = {5, 2, 8, 2, 1};

容器中的最终结果为:

复制代码
1 2 5 8

set 会自动排序并删除重复元素。

迭代器区间构造

复制代码
vector<int> numbers = {5, 2, 8, 2, 1};

set<int> values(
    numbers.begin(),
    numbers.end()
);

拷贝构造

复制代码
set<int> first = {1, 2, 3};
set<int> second(first);

2.3 set 的迭代器

set 支持正向迭代器和反向迭代器:

复制代码
values.begin();
values.end();

values.rbegin();
values.rend();

正向遍历:

复制代码
for (auto iterator = values.begin();
     iterator != values.end();
     ++iterator)
{
    cout << *iterator << " ";
}

范围 for 遍历:

复制代码
for (const auto& value : values)
{
    cout << value << " ";
}

反向遍历:

复制代码
for (auto iterator = values.rbegin();
     iterator != values.rend();
     ++iterator)
{
    cout << *iterator << " ";
}

由于 set 中的元素同时也是搜索关键字,修改元素可能会破坏底层搜索树的结构,因此不能通过迭代器修改元素。

复制代码
set<int> values = {1, 2, 3};

auto iterator = values.begin();

// 不允许这样修改
// *iterator = 10;

如果需要修改,应先删除原来的元素,再插入新元素:

复制代码
values.erase(1);
values.insert(10);

2.4 insert:插入元素

插入单个元素

复制代码
set<int> values;

values.insert(5);
values.insert(2);
values.insert(7);
values.insert(5);

第二次插入 5 会失败,因为 set 不允许元素重复。

判断是否插入成功

复制代码
auto result = values.insert(5);

if (result.second)
{
    cout << "插入成功" << endl;
}
else
{
    cout << "元素已经存在" << endl;
}

insert() 返回一个 pair<iterator, bool>:

  • first:指向对应元素的迭代器。

  • second:表示是否插入成功。

插入初始化列表

复制代码
values.insert({2, 8, 3, 9});

已经存在的元素不会被重复插入。

插入一段迭代器区间

复制代码
vector<int> numbers = {10, 20, 30};

values.insert(
    numbers.begin(),
    numbers.end()
);

2.5 find:查找元素

复制代码
set<int> values = {2, 4, 5, 7, 8, 9};

int target = 7;
auto position = values.find(target);

if (position != values.end())
{
    cout << "找到元素:" << *position << endl;
}
else
{
    cout << "元素不存在" << endl;
}

如果找到元素,find() 返回对应位置的迭代器;如果没有找到,则返回 end()。

需要注意,算法库中的 find() 和 set 自身的 find() 效率不同:

复制代码
// 时间复杂度为 O(N)
auto first = find(
    values.begin(),
    values.end(),
    target
);

// 时间复杂度通常为 O(log N)
auto second = values.find(target);

使用 set 时,应优先调用容器自身的 find()。

2.6 count:统计元素数量

复制代码
if (values.count(7))
{
    cout << "7存在" << endl;
}
else
{
    cout << "7不存在" << endl;
}

由于普通 set 不允许元素重复,因此 count() 的返回值只可能是:

  • 0:元素不存在。

  • 1:元素存在。

2.7 erase:删除元素

根据元素值删除

复制代码
size_t count = values.erase(7);

if (count == 0)
{
    cout << "元素不存在" << endl;
}

根据迭代器删除

复制代码
auto position = values.find(8);

if (position != values.end())
{
    values.erase(position);
}

删除最小元素

由于 set 默认按照升序排列,因此 begin() 指向最小元素:

复制代码
if (!values.empty())
{
    values.erase(values.begin());
}

删除迭代器区间

复制代码
set<int> values = {
    10, 20, 30, 40, 50,
    60, 70, 80, 90
};

// 第一个大于或等于30的元素
auto first = values.lower_bound(30);

// 第一个严格大于60的元素
auto last = values.upper_bound(60);

// 删除[30, 60]
values.erase(first, last);

删除后剩余:

复制代码
10 20 70 80 90

2.8 lower_bound 和 upper_bound

lower_bound

lower_bound(value) 返回第一个大于或等于 value 的元素。

复制代码
set<int> values = {
    10, 20, 30, 40, 50
};

auto position = values.lower_bound(25);

if (position != values.end())
{
    cout << *position << endl;
}

输出:

复制代码
30

upper_bound

upper_bound(value) 返回第一个严格大于 value 的元素。

复制代码
auto position = values.upper_bound(30);

if (position != values.end())
{
    cout << *position << endl;
}

输出:

复制代码
40

2.9 set 设置降序排列

set 默认使用 less<T>,按照升序排列。

使用 greater<T> 可以设置为降序:

复制代码
set<int, greater<int>> values = {
    5, 2, 8, 1
};

for (int value : values)
{
    cout << value << " ";
}

输出:

复制代码
8 5 2 1

2.10 multiset 和 set 的区别

multiset 和 set 的使用方式基本相同,主要区别是 multiset 允许保存重复元素。

复制代码
multiset<int> values = {
    4, 2, 7, 2, 4,
    8, 4, 5, 4, 9
};

遍历结果:

复制代码
2 2 4 4 4 4 5 7 8 9

统计某个元素出现的次数:

复制代码
cout << values.count(4) << endl;

输出:

复制代码
4

删除指定值时,会删除所有相等的元素:

复制代码
values.erase(4);

查找某个值对应的完整范围,可以使用:

复制代码
auto range = values.equal_range(2);

for (auto iterator = range.first;
     iterator != range.second;
     ++iterator)
{
    cout << *iterator << " ";
}

set 与 multiset 的区别可以总结为:

容器 是否排序 是否允许重复
set 是 否
multiset 是 是

3. map 系列容器

3.1 map 的特点

map 用来保存键值对,其基本声明如下:

复制代码
template<
    class Key,
    class T,
    class Compare = less<Key>,
    class Alloc = allocator<pair<const Key, T>>
>
class map;

其中:

  • Key 是键的类型。

  • T 是键所映射的数据类型。

  • Compare 是键的比较规则。

  • Alloc 是空间配置器。

map 具有以下特点:

  1. 每个键都是唯一的。

  2. 数据按照键自动排序。

  3. 键不能直接修改。

  4. 键对应的值可以修改。

  5. 插入、删除和查找的时间复杂度通常为 O(log N)。

3.2 pair 键值对

map 使用 pair 保存键值对。

复制代码
pair<string, string> data(
    "sort",
    "排序"
);

pair 中包含两个成员:

复制代码
data.first;   // 第一个数据
data.second;  // 第二个数据

对于 map 而言:

  • first 表示键。

  • second 表示键对应的值。

也可以使用 make_pair() 创建键值对:

复制代码
auto data = make_pair(
    "insert",
    "插入"
);

map 中实际保存的元素类型可以表示为:

复制代码
pair<const Key, T>

键使用 const 修饰,因此不能直接修改键。

3.3 map 的构造

无参构造

复制代码
map<string, string> dictionary;

初始化列表构造

复制代码
map<string, string> dictionary = {
    {"left", "左边"},
    {"right", "右边"},
    {"insert", "插入"},
    {"string", "字符串"}
};

迭代器区间构造

复制代码
map<string, int> first = {
    {"apple", 3},
    {"banana", 2}
};

map<string, int> second(
    first.begin(),
    first.end()
);

拷贝构造

复制代码
map<string, int> third(first);

3.4 map 的遍历

使用普通迭代器遍历:

复制代码
for (auto iterator = dictionary.begin();
     iterator != dictionary.end();
     ++iterator)
{
    cout << iterator->first
         << ":"
         << iterator->second
         << endl;
}

使用范围 for 遍历:

复制代码
for (const auto& data : dictionary)
{
    cout << data.first
         << ":"
         << data.second
         << endl;
}

map 默认按照键的升序遍历,而不是按照值排序。

map 同样支持反向迭代器:

复制代码
dictionary.rbegin();
dictionary.rend();

3.5 insert:插入键值对

方式一:先创建 pair 对象

复制代码
pair<string, string> data(
    "first",
    "第一个"
);

dictionary.insert(data);

方式二:构造临时 pair

复制代码
dictionary.insert(
    pair<string, string>(
        "second",
        "第二个"
    )
);

方式三:使用 make_pair

复制代码
dictionary.insert(
    make_pair("sort", "排序")
);

方式四:使用初始化列表

复制代码
dictionary.insert({
    "auto",
    "自动的"
});

初始化列表的写法通常更加简洁。

判断插入是否成功

复制代码
auto result = dictionary.insert({
    "left",
    "左边"
});

if (result.second)
{
    cout << "插入成功" << endl;
}
else
{
    cout << "键已经存在" << endl;
}

map 判断数据是否重复时,只比较键。

即使键对应的值不同,只要键已经存在,插入仍然会失败。

复制代码
dictionary.insert({
    "left",
    "左边"
});

dictionary.insert({
    "left",
    "剩余"
});

第二次插入不会覆盖第一次保存的值。

3.6 find:根据键查找数据

复制代码
string word;
cin >> word;

auto position = dictionary.find(word);

if (position != dictionary.end())
{
    cout << position->second << endl;
}
else
{
    cout << "没有找到对应单词" << endl;
}

如果查找成功,迭代器可以同时访问键和值:

复制代码
position->first;
position->second;

3.7 使用迭代器修改值

map 不允许修改键,但是允许修改键所映射的值:

复制代码
auto position = dictionary.find("left");

if (position != dictionary.end())
{
    position->second = "左边、剩余";
}

下面的写法不允许:

复制代码
// position->first = "right";

因为修改键会破坏底层搜索树的结构。

3.8 count:判断键是否存在

复制代码
if (dictionary.count("left"))
{
    cout << "left存在" << endl;
}
else
{
    cout << "left不存在" << endl;
}

普通 map 中的键不能重复,所以 count() 的返回值只可能是 0 或 1。

3.9 erase:删除键值对

根据键删除

复制代码
size_t count = dictionary.erase("left");

if (count == 0)
{
    cout << "键不存在" << endl;
}

根据迭代器删除

复制代码
auto position = dictionary.find("right");

if (position != dictionary.end())
{
    dictionary.erase(position);
}

删除一段范围

复制代码
map<int, string> values = {
    {10, "A"},
    {20, "B"},
    {30, "C"},
    {40, "D"},
    {50, "E"}
};

auto first = values.lower_bound(20);
auto last = values.upper_bound(40);

values.erase(first, last);

这段代码会删除键位于 [20, 40] 范围内的键值对。

3.10 map 的 lower_bound 和 upper_bound

map 的范围查找比较的是键。

复制代码
map<int, string> values = {
    {10, "A"},
    {20, "B"},
    {30, "C"},
    {40, "D"}
};

查找第一个大于或等于 25 的键:

复制代码
auto position = values.lower_bound(25);

if (position != values.end())
{
    cout << position->first << endl;
}

输出:

复制代码
30

查找第一个严格大于 30 的键:

复制代码
auto position = values.upper_bound(30);

if (position != values.end())
{
    cout << position->first << endl;
}

输出:

复制代码
40

3.11 operator\[\] 接口

operator[] 是 map 中非常重要的接口,它同时具有插入、查找和修改功能。

基本形式:

复制代码
mapped_type& operator[](
    const key_type& key
);

键不存在时

如果键不存在,operator[] 会插入一个新的键值对,并对值进行默认初始化。

复制代码
map<string, string> dictionary;

dictionary["insert"];

插入的键值对相当于:

复制代码
{"insert", string()}

插入并修改

复制代码
dictionary["left"] = "左边";

当 "left" 不存在时,会先插入,再修改对应的值。

修改已有数据

复制代码
dictionary["left"] = "左边、剩余";

当 "left" 已经存在时,会直接修改对应的值。

查找并访问

复制代码
cout << dictionary["left"] << endl;

当键存在时,operator[] 会返回该键所对应值的引用。

因此,operator[] 可以总结为:

|---------|---------------|
| 键的状态 | 实际功能 |
| 键不存在 | 插入默认值,并返回值的引用 |
| 键不存在并赋值 | 插入并修改 |
| 键已经存在 | 查找并修改或访问 |

如果只想判断某个键是否存在,应使用 find() 或 count(),防止插入不需要的数据。

3.12 使用 map 统计次数

使用 find 和迭代器统计

复制代码
#include <iostream>
#include <map>
#include <string>
using namespace std;

int main()
{
    string fruits[] = {
        "苹果", "西瓜", "苹果",
        "西瓜", "苹果", "苹果",
        "西瓜", "苹果", "香蕉",
        "苹果", "香蕉"
    };

    map<string, int> counts;

    for (const auto& fruit : fruits)
    {
        auto position = counts.find(fruit);

        if (position == counts.end())
        {
            counts.insert({fruit, 1});
        }
        else
        {
            ++position->second;
        }
    }

    for (const auto& data : counts)
    {
        cout << data.first
             << ":"
             << data.second
             << endl;
    }

    return 0;
}

使用 operator\[\] 统计

利用 operator[] 可以简化代码:

复制代码
map<string, int> counts;

for (const auto& fruit : fruits)
{
    ++counts[fruit];
}

当水果第一次出现时,会先插入:

复制代码
{fruit, 0}

然后执行自增,数量变成 1。再次出现时,直接将原来的数量加一。

3.13 multimap 和 map 的区别

multimap 和 map 的使用方式基本相同,主要区别是 multimap 允许键重复。

复制代码
multimap<string, int> scores;

scores.insert({"Tom", 90});
scores.insert({"Tom", 95});
scores.insert({"Jack", 80});

统计某个键的数量:

复制代码
cout << scores.count("Tom") << endl;

输出:

复制代码
2

获取一个键对应的所有数据:

复制代码
auto range = scores.equal_range("Tom");

for (auto iterator = range.first;
     iterator != range.second;
     ++iterator)
{
    cout << iterator->first
         << ":"
         << iterator->second
         << endl;
}

删除指定键时,会删除该键对应的所有键值对:

复制代码
scores.erase("Tom");

multimap 不支持 operator[]。因为一个键可能对应多个值,无法通过一个键确定应该访问或修改哪一个值。

|------------|---------|-------------------|
| 容器 | 键是否允许重复 | 是否支持 operator[] |
| map | 否 | 是 |
| multimap | 是 | 否 |


4. set 和 map 的典型应用

4.1 set 的常见应用

set 适合解决:

  • 数据去重。

  • 数据自动排序。

  • 快速判断元素是否出现。

  • 求两个数组的交集。

  • 记录链表节点,判断链表是否存在环。

  • 查找某个范围内的数据。

例如,将数组放入 set,可以同时完成去重和排序:

复制代码
vector<int> numbers = {
    4, 2, 7, 2, 4, 8
};

set<int> values(
    numbers.begin(),
    numbers.end()
);

4.2 map 的常见应用

map 适合解决:

  • 统计元素出现次数。

  • 建立字典映射关系。

  • 建立原对象和新对象之间的映射。

  • 根据关键字快速查找数据。

  • 统计高频单词。

  • 保存编号和对象之间的对应关系。

例如:

复制代码
map<string, int> counts;

counts["apple"]++;
counts["banana"]++;
counts["apple"]++;

最终结果为:

复制代码
apple: 2
banana: 1

5. 常用接口总结

set 常用接口

|-----------------|-----------------|
| 接口 | 作用 |
| insert() | 插入元素 |
| find() | 查找元素 |
| count() | 统计元素数量 |
| erase() | 删除元素 |
| lower_bound() | 查找第一个大于或等于目标的元素 |
| upper_bound() | 查找第一个大于目标的元素 |
| begin() | 获取第一个元素的迭代器 |
| end() | 获取尾后迭代器 |
| rbegin() | 获取反向遍历的起始迭代器 |
| rend() | 获取反向遍历的尾后迭代器 |
| size() | 获取元素数量 |
| empty() | 判断容器是否为空 |
| clear() | 清空容器 |

map 常用接口

|-----------------|----------------|
| 接口 | 作用 |
| insert() | 插入键值对 |
| find() | 根据键查找数据 |
| count() | 统计键的数量 |
| erase() | 删除键值对 |
| lower_bound() | 查找第一个键不小于目标的位置 |
| upper_bound() | 查找第一个键大于目标的位置 |
| operator[] | 插入、访问或修改值 |
| begin() | 获取第一个键值对的迭代器 |
| end() | 获取尾后迭代器 |
| rbegin() | 获取反向遍历的起始迭代器 |
| rend() | 获取反向遍历的尾后迭代器 |
| size() | 获取键值对数量 |
| empty() | 判断容器是否为空 |
| clear() | 清空容器 |

6. 总结

set 和 map 都是基于关键字组织数据的关联式容器,底层通常使用红黑树实现。

set 的核心特点是:

  • 元素不能重复。

  • 自动排序。

  • 适合搜索和去重。

  • 不能直接修改元素。

map 的核心特点是:

  • 保存键值对。

  • 键不能重复。

  • 按照键自动排序。

  • 键不能修改,但对应的值可以修改。

  • operator[] 同时具有插入、访问和修改功能。

set 和 map 的插入、删除和查找效率通常为 O(log N)。如果需要保存重复元素,可以分别使用 multiset 和 multimap。

相关推荐
Random Glow1 小时前
聚簇索引 vs 二级索引:回表、覆盖索引、索引下推
数据库·mysql·性能优化·innodb·1024程序员节·索引优化
怪我冷i1 小时前
Rust语言——关联类型
1024程序员节
深圳老胡1 小时前
STM32 开发中 Trae 内置模型怎么选:Seed、MiniMax、GLM、DeepSeek、Kimi、Qwen 全对比
笔记·stm32·单片机·嵌入式硬件·代码规范·1024程序员节
喵了几个咪1 小时前
GoWind Admin 拆解:一个 Sender 接口接 39 个通知渠道
微服务·golang·go·1024程序员节·admin·gowind
.Hypocritical.1 小时前
JUC + 三大设计模式(单例、工厂、代理)
设计模式·juc·1024程序员节
bksczm1 小时前
Redis数据结构之hash
数据结构·redis·哈希算法·1024程序员节
2601_967987371 小时前
SvelteKit Superforms 中 SPA 模式下页面重定向问题的分析与解决
spa·1024程序员节·sveltekit·superforms·重定向问题·jsonschema
ERP管理软件1 小时前
【致远】致远协同提示当前安装版本已过期。
1024程序员节·当前版本已过期
传感器与混合集成电路2 小时前
多计算节点怎么融合?全国产通用融合计算机三子卡架构与任务配置方法
嵌入式硬件·1024程序员节