C++ STL 中的 map 与 set:从使用到实战,掌握高效关联式容器

        在 C++ STL 中,map 和 set 是最常用的关联式容器—— 它们与 vector、list 等序列式容器的核心区别在于:关联式容器通过 “关键字(Key)” 存储和访问数据,而非通过位置。map 和 set 的底层基于红黑树(一种平衡二叉搜索树)实现,保证了增删查操作的时间复杂度为 O (logN),同时支持有序遍历。本文将从 “容器分类→核心使用→实战案例” 的路径,系统讲解 map 和 set 的特性与应用,帮你彻底掌握这两个工业级容器。

一、序列式容器与关联式容器:核心差异

在学习 map 和 set 之前,需先明确 “序列式容器” 与 “关联式容器” 的本质区别 —— 这是理解两者设计思想的关键。

特性序列式容器(如 vector、list)关联式容器(如 map、set)
数据访问方式按存储位置(索引 / 迭代器)访问按关键字(Key)访问
底层逻辑结构线性结构(数组、链表)非线性结构(红黑树)
有序性插入顺序即存储顺序按关键字自动排序
核心优势随机访问(vector)、高效增删(list)高效检索、去重、有序遍历
典型应用场景存储连续数据、需要频繁随机访问数据去重、关键字映射、频率统计

map 和 set 属于 “有序关联式容器”(还有 unordered_map/unordered_set 属于无序关联式容器,底层是哈希表),其有序性和高效检索能力使其成为解决 “去重”“关键字映射” 等问题的首选。

二、set 系列容器:Key 的有序去重容器

set 系列包含setmultiset,核心功能是 “存储关键字(Key),并按 Key 有序排列”,适用于 “仅需判断 Key 存在性” 的场景(如去重、交集计算)。

2.1 set 的核心特性

        去重 + 有序:set 会自动对 Key 去重,并按 Key 的升序(默认)排列;

        不可修改 Key:set 的迭代器是const_iterator,无法通过迭代器修改 Key(修改会破坏红黑树结构);

        底层是红黑树:增删查时间复杂度 O (logN),中序遍历即有序遍历。

2.2 set 的核心接口实战

set 的接口设计简洁,重点掌握 “构造→增删查→遍历” 即可。

2.2.1 构造与遍历

set 支持无参构造、迭代器区间构造、拷贝构造和初始化列表构造,遍历通过迭代器或范围 for 实现(默认升序)。

代码示例:

#include <iostream>
#include <set>
using namespace std;

int main() {
    // 1. 初始化列表构造(自动去重+升序)
    set<int> s1 = {5, 2, 7, 5, 3}; 
    // 2. 迭代器遍历(升序输出:2 3 5 7)
    for (auto it = s1.begin(); it != s1.end(); ++it) {
        // *it = 10; // 错误:迭代器不可修改
        cout << *it << " ";
    }
    cout << endl;

    // 3. 降序set(传入greater<int>仿函数)
    set<int, greater<int>> s2 = {5, 2, 7, 5, 3};
    // 4. 范围for遍历(降序输出:7 5 3 2)
    for (auto e : s2) {
        cout << e << " ";
    }
    cout << endl;

    return 0;
}
2.2.2 增删查操作

set 的增删查接口围绕 Key 展开,核心接口如下:

接口名称功能说明
insert(const Key& val)插入 Key,成功返回pair<iterator, bool>(first 是迭代器,second 表示是否插入成功)
erase(const Key& val)删除 Key,返回删除的元素个数(0 表示不存在)
erase(iterator pos)删除迭代器指向的元素,返回下一个有效迭代器
find(const Key& val)查找 Key,返回对应迭代器(不存在返回end()
count(const Key& val)返回 Key 的个数(set 中仅 0 或 1,用于快速判断存在性)
lower_bound(val)返回第一个≥val 的迭代器
upper_bound(val)返回第一个 > val 的迭代器

代码示例(增删查):

int main() {
    set<int> s = {4, 2, 7, 8, 5};

    // 1. 插入(重复插入失败)
    auto ret1 = s.insert(3); // 成功:ret1.second = true
    auto ret2 = s.insert(4); // 失败:ret1.second = false

    // 2. 查找(存在则返回迭代器,不存在返回end())
    int x = 5;
    auto pos = s.find(x);
    if (pos != s.end()) {
        cout << "找到" << x << endl;
    } else {
        cout << x << "不存在" << endl;
    }

    // 3. 删除(按Key删除)
    int del_num = s.erase(2); // 删除2,返回1(成功)
    if (del_num == 0) {
        cout << "删除失败" << endl;
    }

    // 4. 区间删除(删除[3,6)的元素:3、4、5)
    auto it_low = s.lower_bound(3);  // 第一个≥3的迭代器
    auto it_up = s.upper_bound(6);   // 第一个>6的迭代器
    s.erase(it_low, it_up);

    // 遍历结果:7 8
    for (auto e : s) {
        cout << e << " ";
    }
    return 0;
}

2.3 set 与 multiset 的差异

multiset是 set 的 “允许重复 Key” 版本,核心差异如下:

特性setmultiset
Key 重复性不允许重复允许重复
insert返回值pair<iterator, bool>仅返回插入位置的迭代器
find行为返回任意一个匹配的 Key(仅一个)返回中序遍历的第一个匹配 Key
count行为返回 0 或 1返回匹配 Key 的实际个数
erase(Key)行为删除 1 个匹配 Key(若存在)删除所有匹配 Key

代码示例(multiset):

int main() {
    // 允许重复Key,排序但不去重
    multiset<int> ms = {4, 2, 7, 2, 4, 4};

    // 1. 遍历:2 2 4 4 4 7
    for (auto e : ms) {
        cout << e << " ";
    }
    cout << endl;

    // 2. find返回中序第一个匹配Key(找4,返回第一个4的迭代器)
    auto pos = ms.find(4);
    while (pos != ms.end() && *pos == 4) {
        cout << *pos << " "; // 输出:4 4 4
        ++pos;
    }
    cout << endl;

    // 3. count返回实际个数(4出现3次)
    cout << "4的个数:" << ms.count(4) << endl;

    // 4. erase(Key)删除所有匹配Key(删除所有4)
    ms.erase(4);
    // 遍历结果:2 2 7
    for (auto e : ms) {
        cout << e << " ";
    }
    return 0;
}

2.4 set 实战案例:两个数组的交集(LeetCode 349)

需求:给定两个数组,计算它们的交集(结果需去重)。思路:用 set 对两个数组去重并排序,再用双指针遍历求交集。

代码实现:

#include <vector>
#include <set>
using namespace std;

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        // 1. 用set去重并排序
        set<int> s1(nums1.begin(), nums1.end());
        set<int> s2(nums2.begin(), nums2.end());

        vector<int> ret;
        auto it1 = s1.begin(), it2 = s2.begin();
        // 2. 双指针遍历求交集
        while (it1 != s1.end() && it2 != s2.end()) {
            if (*it1 < *it2) {
                ++it1;
            } else if (*it1 > *it2) {
                ++it2;
            } else {
                ret.push_back(*it1);
                ++it1;
                ++it2;
            }
        }
        return ret;
    }
};

三、map 系列容器:Key-Value 的有序映射容器

map 系列包含mapmultimap,核心功能是 “存储 Key-Value 键值对,按 Key 有序排列”,适用于 “关键字映射” 场景(如字典、频率统计)。

3.1 map 的核心特性

        Key 唯一 + 有序:map 的 Key 唯一且按升序排列,Value 可修改;

        存储键值对:底层红黑树的节点存储pair<const Key, Value>(Key 不可修改,Value 可修改);

  operator[]多功能接口:支持插入、查找、修改 Value(map 的核心特色);

        底层是红黑树:增删查时间复杂度 O (logN),中序遍历按 Key 有序。

3.2 map 的核心接口实战

map 的接口与 set 类似,但需关注 “键值对(pair)” 的处理,重点掌握insertoperator[]

3.2.1 pair 类型:map 的键值对载体

map 存储的是pair<const Key, Value>类型,pair是 STL 中的模板结构体,包含两个成员:

  first:Key(const 类型,不可修改);

  second:Value(可修改)。

创建 pair 的四种方式:

// 1. 直接构造
pair<string, int> kv1("apple", 5);
// 2. 用make_pair函数(推荐,自动推导类型)
auto kv2 = make_pair("banana", 3);
// 3. 初始化列表(C++11后推荐)
pair<string, int> kv3 = {"cherry", 2};
// 4. map的value_type别名
map<string, int>::value_type kv4("date", 4);
3.2.2 构造与遍历

map 的构造方式与 set 类似,遍历需通过迭代器访问pairfirst(Key)和second(Value)。

代码示例:

#include <iostream>
#include <map>
using namespace std;

int main() {
    // 1. 初始化列表构造(按Key升序排列)
    map<string, string> dict = {
        {"left", "左边"},
        {"right", "右边"},
        {"insert", "插入"}
    };

    // 2. 迭代器遍历(输出Key:Value)
    auto it = dict.begin();
    while (it != dict.end()) {
        // 方式1:解引用迭代器访问pair
        cout << (*it).first << ":" << (*it).second << " ";
        // 方式2:用->访问pair(更简洁,推荐)
        // cout << it->first << ":" << it->second << " ";
        ++it;
    }
    // 输出:insert:插入 left:左边 right:右边(按Key字典序)
    cout << endl;

    return 0;
}
3.2.3 增删查与operator[]

map 的增删查接口与 set 类似,但insert插入的是pair,且operator[]是 map 的 “万能接口”—— 支持插入、查找、修改 Value。

1. insert插入键值对

insert返回pair<iterator, bool>

  first:指向 Key 所在节点的迭代器(无论插入成功与否);

  secondtrue表示插入成功(Key 不存在),false表示插入失败(Key 已存在)。

代码示例:

int main() {
    map<string, int> countMap;

    // 插入方式1:直接插入pair
    countMap.insert(pair<string, int>("apple", 1));
    // 插入方式2:make_pair(推荐)
    countMap.insert(make_pair("banana", 2));
    // 插入方式3:初始化列表(推荐)
    countMap.insert({"cherry", 3});

    // 重复插入(Key已存在,插入失败)
    auto ret = countMap.insert({"apple", 5});
    if (!ret.second) {
        cout << "apple已存在,当前计数:" << ret.first->second << endl;
    }

    return 0;
}
2. operator[]:插入、查找、修改三合一

operator[]的底层实现依赖insert,逻辑如下:

Value& operator[](const Key& k) {
    // 1. 尝试插入pair<k, Value()>(Value()是默认构造值,如int()=0)
    auto ret = insert({k, Value()});
    // 2. 返回Value的引用(无论插入成功与否)
    return ret.first->second;
}

operator[]的三种用法:

int main() {
    map<string, int> countMap;

    // 1. 插入:Key不存在时,插入{Key, 0}并返回Value引用
    countMap["apple"] = 1; // 插入{apple, 1}

    // 2. 修改:Key存在时,返回Value引用并修改
    countMap["apple"]++; // apple的Value变为2

    // 3. 查找:Key存在时,返回Value引用
    cout << "apple的计数:" << countMap["apple"] << endl; // 输出2

    // 4. 仅插入:Key不存在时,插入{Key, 默认值}
    countMap["banana"]; // 插入{banana, 0}

    return 0;
}
3. 查找与删除

map 的查找和删除接口与 set 类似,但查找返回的是指向pair的迭代器,可通过second修改 Value。

代码示例:

int main() {
    map<string, int> countMap = {{"apple", 2}, {"banana", 1}, {"cherry", 3}};

    // 1. 查找(返回指向pair的迭代器)
    string key = "apple";
    auto pos = countMap.find(key);
    if (pos != countMap.end()) {
        cout << key << "的计数:" << pos->second << endl;
        // 修改Value
        pos->second = 5;
    }

    // 2. 删除(按Key删除)
    int del_num = countMap.erase("banana"); // 删除成功,返回1
    if (del_num == 0) {
        cout << "删除失败" << endl;
    }

    // 遍历结果:apple:5 cherry:3
    for (auto& e : countMap) {
        cout << e.first << ":" << e.second << " ";
    }
    return 0;
}

3.3 map 与 multimap 的差异

multimap是 map 的 “允许 Key 重复” 版本,核心差异如下:

特性mapmultimap
Key 重复性不允许重复允许重复
insert返回值pair<iterator, bool>仅返回插入位置的迭代器
operator[]支持支持(插入、查找、修改)不支持(Key 重复,无法确定修改哪个)
find行为返回任意一个匹配的 Key返回中序遍历的第一个匹配 Key
count行为返回 0 或 1返回匹配 Key 的实际个数

代码示例(multimap):

int main() {
    // 允许Key重复,按Key有序
    multimap<int, string> mm = {{1, "a"}, {2, "b"}, {1, "c"}, {2, "d"}};

    // 遍历:1:a 1:c 2:b 2:d
    for (auto& e : mm) {
        cout << e.first << ":" << e.second << " ";
    }
    cout << endl;

    // find返回中序第一个匹配Key(找1,返回{1,"a"}的迭代器)
    auto pos = mm.find(1);
    while (pos != mm.end() && pos->first == 1) {
        cout << pos->first << ":" << pos->second << " "; // 输出1:a 1:c
        ++pos;
    }
    return 0;
}

3.4 map 实战案例:单词频率统计

需求:统计数组中各单词的出现次数,并按单词字典序输出。思路:用 map 的operator[]快速统计频率,中序遍历即按字典序输出。

代码实现:

#include <iostream>
#include <map>
#include <string>
using namespace std;

int main() {
    string arr[] = {"apple", "banana", "apple", "cherry", "apple", "banana"};
    map<string, int> countMap;

    // 统计频率(用operator[]简洁实现)
    for (const auto& word : arr) {
        countMap[word]++; // 插入/修改频率
    }

    // 按单词字典序输出频率
    for (const auto& e : countMap) {
        cout << e.first << ":" << e.second << endl;
    }
    // 输出:
    // apple:3
    // banana:2
    // cherry:1
    return 0;
}

四、map 与 set 的核心区别与适用场景

特性setmap
存储内容单个 KeyKey-Value 键值对
核心用途去重、有序遍历、判断存在性关键字映射、频率统计、字典
Key 修改不可修改(迭代器是 const)Key 不可修改,Value 可修改
特有接口无特殊接口operator[](插入 / 查找 / 修改)
典型场景数组交集、环形链表检测单词翻译、随机链表复制

五、总结

map 和 set 是 C++ STL 中最实用的关联式容器,其核心价值在于 “有序性” 和 “高效检索”:

  1. set:专注于 Key 的去重与有序,适用于 “仅需判断存在性” 的场景,如去重、交集计算;
  2. map:专注于 Key-Value 的有序映射,适用于 “关键字关联” 场景,如字典、频率统计;
  3. multiset/multimap:允许 Key 重复,适用于 “Key 不唯一” 的场景,但需注意operator[]的支持差异。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值