跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

C++ STL 中 map 与 set 容器的核心用法与实战

深入讲解了 C++ STL 中 map 与 set 容器的核心用法。首先区分了序列式与关联式容器的特性,重点剖析了 set、multiset、map 及 multimap 的底层原理(红黑树)、构造函数、迭代器操作及增删查改接口。针对 map 的 operator[] 隐式插入机制、pair 类型使用等易错点进行了详细说明,并结合 LeetCode 经典例题(如数组交集、环形链表检测、前 K 个高频单词)展示了容器在实际算法题中的应用技巧。内容涵盖从基础语法到实战优化的完整流程,适合希望提升 C++ 容器运用能力的开发者阅读。

星落发布于 2026/3/29更新于 2026/10/2386 浏览
C++ STL 中 map 与 set 容器的核心用法与实战

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

1.1 序列式容器
  • 核心特征
    • 按插入顺序存储:元素的物理存储顺序与插入顺序一致。
    • 允许重复元素:可以存储多个相同的值。
    • 通过位置访问:通过索引(如数组)或迭代器直接访问元素。
  • 常见容器
    • vector: 动态数组,支持快速随机访问,尾部插入高效。
    • list: 双向链表,支持快速插入/删除,但随机访问效率低。
    • deque: 双端队列,支持头尾高效插入/删除。
    • array: 固定大小数组,编译时确定大小。
    • forward_list: 单向链表,内存占用更小。
  • 适应场景
    • 需要保持插入顺序(如日志记录,操作历史)。
    • 频繁通过索引随机访问(如 vector 的 [] 操作)。
    • 需要高效头尾操作(如 deque)。
1.2 关联式容器
  • 核心特征
    • 按键存储:元素基于键值对(key-value)键本身存储。
    • 自动排序:元素按键的排序规则(默认升序)组织。
    • 唯一性:set和map要求键唯一;multiset和multimap允许重复键。
  • 常见容器
    • set: 存储唯一键的有序集合。
    • map: 存储键值对的有序映射。
    • multiset: 允许重复键的有序集合。
    • multimap: 允许重复键的有序映射。
  • 适用场景
    • 需要快速查找(如字典,数据库索引)。
    • 需自动排序(如排行榜,有序事件调度)。
    • 需要唯一性约束(如用户 ID 管理)。

2. set 系列的使用

2.1 set 类的介绍

set底层是用红黑树实现的,增删查的效率是 O(log n),迭代器遍历走的是搜索树的中序遍历,所以遍历后的元素是有序的。声明如下,T 就是 set底层关键字的类型。

template <class T,          // set::key_type/value_type
            class Compare = less<T>, // set::key_compare/value_compare
            class Alloc = allocator<T>> // set::allocator_type
class set;

默认要求 T 支持小于比较,如果不支持,可以手动实现一个仿函数传给第二个模板参数。一般情况下我们都不需要传后面的两个模板参数。

2.2 set 的构造函数和迭代器

构造方面主要关注无参、区间、拷贝和初始化列表这几种。迭代器是双向迭代器,支持正向和反向遍历。

// empty (1) 无参默认构造
explicit set(const key_compare& comp = key_compare(), const allocator_type& alloc = allocator_type());
// range (2) 迭代器区间构造
template<class InputIterator>
set(InputIterator first, InputIterator last, const key_compare& comp = key_compare(), const allocator_type& alloc = allocator_type());
// copy (3) 拷贝构造
set(const set& x);
// initializer list (5) 初始化列表构造
set(initializer_list<value_type> il, const key_compare& comp = key_compare(), const allocator_type& alloc = allocator_type());

// 正向迭代器
iterator begin(); iterator end();
// 反向迭代器
reverse_iterator rbegin(); reverse_iterator rend();
2.3 set 的增删查实战

这里我们通过代码看看实际怎么用。注意 set里的元素是不可修改的,因为修改会破坏红黑树的平衡结构。

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

int main() {
    // 去重 + 升序排序
    set<int> s;
    
    // 插入单个元素
    s.insert(2); s.insert(1); s.insert(5); s.insert(6);
    
    // 遍历输出
    auto it = s.begin();
    while(it != s.end()) {
        cout << *it << " ";
        ++it;
    }
    cout << endl;
    
    // 插入一段 initializer_list 的值,已经存在则插入失败
    s.insert({1, 5, 3, 2, 7, 9});
    for(auto e : s) {
        cout << e << " ";
    }
    cout << endl;
    
    // string 类对象比较是按照 ascii 码来比较大小的
    set<string> strset = {"sort", "add", "insert"};
    for(auto &e : strset) {
        cout << e << " ";
    }
    cout << endl;
    return 0;
}
2.4 find 和 erase 的使用样例

erase和find是日常最常用的接口。count可以用来判断元素是否存在,返回值为 0 或 1。

int main() {
    set<int> s = {2, 7, 4, 3, 1, 9, 5, 0};
    
    // 删除最小值
    s.erase(s.begin());
    
    // 删除输入的值,这个值有可能在,也有可能不在
    int x; cin >> x;
    int num = s.erase(x); // 返回值是被删除的元素个数
    if(num == 0) {
        cout << x << "不存在!" << endl;
    }
    
    // 直接查找,再利用迭代器删除
    cin >> x;
    auto pos = s.find(x);
    if(pos != s.end()) {
        s.erase(pos);
    } else {
        cout << x << "不存在" << endl;
    }
    
    // count 快速判断
    cin >> x;
    if(s.count(x)) {
        cout << x << "在!" << endl;
    } else {
        cout << x << "不存在!" << endl;
    }
    return 0;
}

删除一段区间的值可以用 lower_bound 和 upper_bound 配合 erase 完成。

int main() {
    set<int> myset;
    for(int i = 1; i < 10; i++) {
        myset.insert(i * 10); // 10 20 30 ... 90
    }
    
    // 实现查找到的 [itlow, itup] 包含 [30, 60] 区间
    auto itlow = myset.lower_bound(30); // 返回 >=30
    auto itup = myset.upper_bound(60);  // 返回 >60
    
    // 删除这段区间的值
    myset.erase(itlow, itup);
    
    for(auto e : myset) {
        cout << e << " ";
    }
    return 0;
}
2.5 multiset 和 set 的差异

multiset和set基本完全相似,主要的区别点在于 multiset支持键值冗余。那么 insert/find/count/erase都围绕着支持键值冗余有所差异。

int main() {
    multiset<int> s = {4, 6, 4, 3, 6, 7, 8, 9, 2, 5, 3, 7, 8, 9};
    auto it = s.begin();
    while(it != s.end()) {
        cout << *it << " ";
        ++it;
    }
    cout << endl;
    
    // 相比较 set 不同的是,x 可能会存在多个,find 查找的是中序的第一个
    int x; cin >> x;
    auto pos = s.find(x);
    while(pos != s.end() && *pos == x) {
        cout << *pos << " ";
        ++pos;
    }
    cout << endl;
    
    // count 会返回 x 的实际个数
    cout << s.count(x) << endl;
    
    // erase 会删掉里面的所有的 x
    s.erase(x);
    for(auto e : s) {
        cout << e << " ";
    }
    return 0;
}
2.6 实战案例:两个数组的交集

利用 set 的特性可以快速去重并排序,非常适合处理这类问题。

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        // 这里用 set 实现了对数组的排序 + 去重
        set<int>s1(nums1.begin(), nums1.end());
        set<int>s2(nums2.begin(), nums2.end());
        
        vector<int> ret;
        auto it1 = s1.begin();
        auto it2 = s2.begin();
        
        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;
    }
};
2.7 实战案例:环形链表 II

遍历链表,如果节点已存在于 set 中,说明找到了环的入口。

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        set<ListNode*> s;
        ListNode* cur = head;
        while(cur) {
            if(s.count(cur)) return cur; // 等于 1 即为入口点
            else s.insert(cur);
            cur = cur->next;
        }
        return nullptr;
    }
};

3. map 系列的使用

3.1 map 类的介绍

map底层使用红黑树实现的,增删查改的效率是 O(log n)。Key 的值是有序的。声明如下,Key 就是 map 底层关键字的类型,T 是 map 底层 Value 的类型。

template <class Key,           // map::key_type
            class T,            // map::mapped_type
            class Compare = less<Key>, // map::key_compare
            class Alloc = allocator<pair<const Key, T>>> // map::allocator_type
class map;
3.2 pair 类型介绍

map底层红黑树节点中的数据,使用 pair<Key,T>存储键值对数据。pair是一个类模板,里面有两个成员变量,一个是 first,一个是 second。

template<class T1, class T2>
struct pair {
    typedef T1 first_type;
    typedef T2 second_type;
    T1 first;
    T2 second;
    // ... 构造函数略
};

// make_pair 辅助构造
inline pair<T1, T2> make_pair(T1 x, T2 y) {
    return pair<T1, T2>(x, y);
}
3.3 map 的构造与遍历

map支持正向迭代器遍历和反向迭代器遍历,遍历默认按 Key 的升序。支持 value数据的修改,但不支持 Key的修改。修改关键字的数据破坏了底层搜索树的结构。

#include <map>
int main() {
    // initializer_list 构造以及迭代遍历
    map<string, string> dict = {{"left","左边"},{"right","右边"},{"insert","插入"}};
    
    // 原生写法
    map<string, string>::iterator it = dict.begin();
    while(it != dict.end()) {
        cout << it->first << ":" << it->second << endl;
        ++it;
    }
    
    // 范围 for 遍历
    for(auto& e : dict) {
        cout << e.first << ":" << e.second << endl;
    }
    
    // C++17 结构化绑定
    // for (const auto& [k,v] : dict)
    //     cout << k << ":" << v << endl;
    
    return 0;
}
3.4 map 的增删查改

map的增接口,插入的是 pair 的键值对数据。查和删的接口只用关键字 Key。find返回的是 iterator,不仅仅可以找到 Key 在不在,还能找到映射的 Value,同时通过迭代还能修改 Value。

// insert 返回 pair<iterator, bool>
pair<iterator, bool> insert(const value_type& val);
// 查找 k,返回 k 所在的迭代器
iterator find(const key_type& k);
// 删除 k,k 存在返回 1
size_type erase(const key_type& k);
3.5 map 的数据修改

map 第一个支持修改的方式是通过迭代器,迭代器遍历时或者 find 返回 Key 所在的 iterator 修改。map 还有一个非常重要的修改接口 operator[],但是 operator[]不仅仅支持数据的修改,还支持插入数据和查找数据,所以它是一个多功能复合接口。

// operator[] 的内部逻辑示意
mapped_type& operator[](const key_type& k) {
    // 1、如果 k 不在 map 中,insert 会插入 k 和 mapped_type 默认值
    // 2、如果 k 在 map 中,insert 会插入失败,但返回指向 key 结点的迭代器
    pair<iterator, bool> ret = insert({k, mapped_type()});
    iterator it = ret.first;
    return it->second;
}

注意:operator[]在查找不到时会隐式插入一个默认值的元素,这在某些场景下可能不是预期的行为,此时应优先使用 find。

3.6 实战案例:统计水果出现次数
#include <iostream>
#include <string>
#include <map>
using namespace std;

int main() {
    string arr[] = {"苹果","西瓜","苹果","西瓜","苹果","苹果","西瓜","苹果","香蕉","苹果","香蕉"};
    map<string, int> countMap;
    
    // 方法一:先查找再决定
    for(const auto& str : arr) {
        auto ret = countMap.find(str);
        if(ret == countMap.end()) {
            countMap.insert({str, 1});
        } else {
            ret->second++;
        }
    }
    
    // 方法二:直接用 [] (注意它会隐式创建)
    // for(const auto& str : arr) countMap[str]++;
    
    for(const auto& e : countMap) {
        cout << e.first << ":" << e.second << endl;
    }
    return 0;
}
3.7 实战案例:前 K 个高频单词

利用 map统计频率后,放入 vector排序即可。注意题目要求的特殊排序规则(频率高在前,频率相同字典序小在前)。

class Solution {
public:
    struct Compare {
        bool operator()(const pair<string,int>& kv1, const pair<string,int>& kv2) {
            // 次数降序,次数相同字典序升序
            return kv1.second > kv2.second || (kv1.second == kv2.second && kv1.first < kv2.first);
        }
    };
    
    vector<string> topKFrequent(vector<string>& words, int k) {
        map<string, int> countMap;
        for(auto& str : words) {
            countMap[str]++;
        }
        
        vector<pair<string, int>>v(countMap.begin(), countMap.end());
        sort(v.begin(), v.end(), Compare());
        
        vector<string> strV;
        for(int i = 0; i < k; ++i) {
            strV.push_back(v[i].first);
        }
        return strV;
    }
};

4. 总结

set和map都是基于红黑树实现的关联式容器,提供了高效的查找能力。set用于去重和有序存储,map用于建立键值映射。在实际开发中,要注意 map的 operator[]会隐式插入元素,而 multiset/multimap允许重复键。理解这些细节,能帮你写出更健壮、高效的 C++ 代码。

目录

  1. 1. 序列式容器和关联式容器
  2. 1.1 序列式容器
  3. 1.2 关联式容器
  4. 2. set 系列的使用
  5. 2.1 set 类的介绍
  6. 2.2 set 的构造函数和迭代器
  7. 2.3 set 的增删查实战
  8. 2.4 find 和 erase 的使用样例
  9. 2.5 multiset 和 set 的差异
  10. 2.6 实战案例:两个数组的交集
  11. 2.7 实战案例:环形链表 II
  12. 3. map 系列的使用
  13. 3.1 map 类的介绍
  14. 3.2 pair 类型介绍
  15. 3.3 map 的构造与遍历
  16. 3.4 map 的增删查改
  17. 3.5 map 的数据修改
  18. 3.6 实战案例:统计水果出现次数
  19. 3.7 实战案例:前 K 个高频单词
  20. 4. 总结

更多推荐文章

查看全部
  • OpenClaw 2026.3.7 版本发布:支持 GPT-5.4 与记忆热插拔
  • Linux 信号捕捉与处理机制
  • 基于 Docker 的本地 AI 量化分析平台搭建与波浪理论实战
  • C++多线程编程:测试 Windows 下最大线程数
  • YOLO12 目标检测 WebUI 效果展示:高清图片检测实例
  • CoordinatorLayout 详解:布局协调与滚动效果实现
  • 从人工运维到 ChatOps:大模型驱动运维自动化变革
  • ERNIE-4.5-0.3B 超轻量模型部署与能力评测指南
  • 自动化 B 站直播间搭建与神奇弹幕机器人配置指南
  • 主流 AI 编程工具横向评测:TRAE、Qoder、Cursor 与 Copilot 选型指南
  • 云算力平台集成通义万相 2.1 的 AIGC 创作实战
  • AI Agent 入门:什么是执行式智能体
  • 宝塔面板部署 OpenClaw 云端 AI 助理实战
  • 阿里开源 Page-Agent:一行 JS 代码实现大模型前端 DOM 操控
  • Open-WebUI 管理员面板深度拆解与配置指南
  • Maven 项目中如何将本地依赖库打包进最终 JAR
  • FPGA 摄像头采集处理显示指南:OV5640 至 HDMI 实时显示
  • GitHub 全界面中文化实战:Tampermonkey 插件配置详解
  • 二分查找实战:旋转排序数组最小值与缺失数字求解
  • 鸿蒙金融理财全栈项目:架构、安全与体验

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online