【C++】 认识STL set与map(基础接口+题目OJ运用)


C++ STL set 详解

一、set 简介

set 是 C++ STL 提供的有序、不重复关联容器,底层基于红黑树实现。

特点:元素自动排序、容器内无重复值、查找删除效率极高。

复制代码
#include <set>
using namespace std;

二、核心特性

  1. 插入元素自动升序排列
  2. 不允许存放重复数据
  3. 底层红黑树,增删查时间复杂度 O(logn)
  4. 迭代器只读,无法直接修改元素值

三、常用构造方式

cpp 复制代码
// 空集合
set<int> s1;

// 数组初始化
int arr[] = {1,3,2,2,5};
set<int> s2(arr, arr+5);

// 拷贝构造
set<int> s3(s2);

四、常用 API 用法

1. 插入 insert

复制代码
s.insert(10);
s.insert({2,5,8});

template <class InputIterator>
11 void insert (InputIterator first, InputIterator last);

重复元素插入会自动失效。

insert 返回一个 pair:

  • first:迭代器,指向插入的元素

  • second:bool,表示是否插入成功(true = 新插入,false = 已存在)

    auto res = s.insert(5);
    if (res.second)
    cout << "插入成功";
    else
    cout << "元素已存在";

2. 遍历容器

复制代码
// 迭代器遍历
for(set<int>::iterator it = s.begin(); it != s.end(); ++it)
{
    cout << *it << " ";
}

// C++11 范围for
for(auto x : s) 
cout << x << " ";

3. 查找 find

找到返回元素迭代器,没找到返回end()

复制代码
auto it = s.find(5);
if(it != s.end()) cout << "存在";

// 算法库的查找 O(N)
 auto pos1 = find(s.begin(), s.end(), x);
// set⾃⾝实现的查找 O(logN)
 auto pos2 = s.find(x);

比较算法库的find,效率更高!优先用自身的接口函数。

4. 删除 erase

复制代码
// 删除⼀个迭代器位置的值
 iterator erase (const_iterator position);
// 删除val,val不存在返回0,存在返回1
 size_type erase (const value_type& val);
// 删除⼀段迭代器区间的值
 iterator erase (const_iterator first, const_iterator last);
s.erase(3);        // 按值删除
s.erase(it);       // 按迭代器删除
s.clear();         // 清空所有元素

5. 大小与判空

复制代码
s.size();    // 元素个数
s.empty();   // 为空返回true

6. 边界查找

复制代码
s.lower_bound(x);  // 第一个>=x的元素
s.upper_bound(x); // 第一个>x的元素

7.查找数据

复制代码
// 查找val,返回Val的个数
size_type count (const value_type& val) const;

五、自定义排序规则

默认从小到大,可重载比较器实现降序

复制代码
// 降序set
set<int,greater<int>> s;

自定义结构体排序:重载<运算符即可。

六、常见使用场景

  1. 数组 / 数据快速去重
  2. 有序数据存储与维护
  3. 高频查找、快速判重业务
  4. 区间最值、边界元素检索

C++ multiset

容器 重复元素 删除数值 count 返回值 适用场景
set 不允许 仅删单个 0 或 1 数据去重、唯一值存储
multiset 允许 删除全部匹配 实际个数 重复数据排序、频次统计

oj题目运用

两个数组的交集

349. 两个数组的交集https://leetcode.cn/problems/intersection-of-two-arrays/

给定两个数组 nums1nums2 ,返回 它们的 交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序

复制代码
class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        set<int> a1(nums1.begin(),nums1.end());
        set<int> a2(nums2.begin(),nums2.end());
        vector<int> vv;
        for(auto e:a1)
        {
            if(a2.count(e)==1)
            {
                vv.push_back(e);
            }
        }
        return vv;
    }
};

环形链表ll

142. 环形链表 IIhttps://leetcode.cn/problems/linked-list-cycle-ii/

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos-1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

复制代码
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        set<ListNode*> ai;
        ListNode* phead=head;
        while(phead!=nullptr)
        {
            int sizep=ai.size();
            ai.insert(phead);
            if(sizep==ai.size())
            return phead;
            phead=phead->next;            
        }
        return nullptr;
        
    }
};

C++ STL map 深度解析

1. 定义与本质

std::map 是定义在 <map> 头文件中的有序关联容器,存储结构为键值对(std::pair<const Key, T>),其中:

cpp 复制代码
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;
  • Key(键):唯一标识元素,不可重复,不可修改(const 修饰)
  • T(值):与键绑定的数据,可任意修改;
  • 容器会自动根据键的升序排序(默认规则)。

2. 核心特性

  1. 键唯一性:同一个键只能存在一个元素,重复插入会覆盖旧值
  2. 自动排序:默认按键的小于(<)运算符升序排列,支持自定义排序规则;
  3. 双向迭代器:仅支持正向 / 反向遍历,不支持随机访问(不能用 [] 下标随机跳转);
  4. 动态大小:无需预先分配内存,自动扩容 / 缩容;
  5. 底层实现:红黑树(Red-Black Tree)(平衡二叉搜索树)。

3. 适用场景

  • 需要有序存储键值对的场景;
  • 频繁查找、插入、删除元素,且对稳定性要求高;
  • 键需要唯一映射的业务(如用户 ID→用户信息、单词→释义)。

4. pair 介绍

map底层的红⿊树节点中的数据,使⽤ pair<Key, T> 存储键值对数据。

cpp 复制代码
 typedef pair<const Key, T> value_type;

 template <class T1, class T2>
 struct pair
 {
 typedef T1 first_type;
 typedef T2 second_type;

 T1 first;
 T2 second;

 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)
 {}
 };

 template <class T1,class T2>
 inline pair<T1,T2> make_pair (T1 x, T2 y)
 {
     return ( pair<T1,T2>(x,y) );
 }

map 基础用法

1. 初始化方式

map 支持多种初始化方式,覆盖开发中所有常用场景:

cpp 复制代码
// 1. 空初始化
map<int, string> map1;

// 2. 列表初始化(C++11 及以上)
map<int, string> map2 = {{1, "张三"}, {2, "李四"}, {3, "王五"}};

// 3. 拷贝初始化
map<int, string> map3(map2);

// 4. 范围初始化(从其他容器拷贝)
map<int, string> map4(map2.begin(), map2.end());

2. 插入元素

cpp 复制代码
// 方式1:[] 运算符(最简单,若键不存在则创建,存在则覆盖值)
map1[1] = "苹果";
map1[2] = "香蕉";
map1[1] = "红苹果"; // 键1已存在,覆盖旧值

// 方式2:insert() 插入 pair(不覆盖,键存在则插入失败)
map1.insert(pair<int, string>(3, "橙子"));
map1.insert(make_pair(4, "葡萄")); // 更简洁的 pair 创建方式

// 方式3:insert() 插入初始化列表(C++11)
map1.insert({5, "芒果"});
map1.insert({5, "xxxx"});// 插入失败,插入只看key

// 方式4:emplace() 直接构造(效率更高,避免临时对象)
map1.emplace(6, "西瓜");

关键区别:

  • []:会覆盖旧值,且能直接访问值;
  • insert()/emplace():不覆盖旧值,键存在时插入操作无效。

3. 访问元素

两种安全 / 便捷的访问方式,推荐优先使用 at()

cpp 复制代码
map<int, string> fruitMap = {{1, "苹果"}, {2, "香蕉"}, {3, "橙子"}};

// 方式1:[] 运算符(键不存在会自动创建空值,有风险)
cout << fruitMap[1] << endl; // 输出:苹果
// cout << fruitMap[10] << endl; // 危险:键10不存在,自动插入键10,值为空字符串

// 方式2:at() 成员函数(键不存在抛出 out_of_range 异常,更安全)
cout << fruitMap.at(2) << endl; // 输出:香蕉

// 方式3:通过迭代器访问
auto it = fruitMap.find(3);
if (it != fruitMap.end()) {
    cout << it->first << ": " << it->second << endl; // 输出:3: 橙子
}

4. 查找元素

find()map 最核心的查找方法,时间复杂度 O(logn):

cpp 复制代码
// 查找键2
auto it = fruitMap.find(2);
if (it != fruitMap.end()) {
    cout << "找到元素:" << it->second << endl;
} else {
    cout << "未找到元素" << endl;
}

补充**:count(key) 函数,返回键的数量(map 中只能是 0 或 1),常用于判断键是否存在:**

5. 删除元素

cpp 复制代码
map<int, string> fruitMap = {{1, "苹果"}, {2, "香蕉"}, {3, "橙子"}, {4, "葡萄"}};

// 方式1:按迭代器删除
auto it = fruitMap.find(2);
if (it != fruitMap.end()) {
    fruitMap.erase(it); // 删除键2
}

// 方式2:按键删除(直接传键)
fruitMap.erase(3); // 删除键3

// 方式3:范围删除
fruitMap.erase(fruitMap.begin(), fruitMap.find(4)); // 删除键1,2, 3

// 清空整个map
fruitMap.clear();

6. 常用成员函数

函数 作用
size() 返回元素个数
empty() 判断是否为空(空返回 true)
max_size() 返回容器最大可存储元素数
swap(map) 交换两个 map 的内容
lower_bound(key) 返回第一个 ≥ key 的迭代器
upper_bound(key) 返回第一个 > key 的迭代器

operator[] 底层实现原理

🔍****核心代码拆解

复制代码
mapped_type& operator[] (const key_type& k)
{
    return (*(this->insert(make_pair(k, mapped_type()))).first).second;
}

它的执行过程,正好对应图里从内到外的层层嵌套:

make_pair(k, mapped_type())

  • 先构造一个键值对:pair<key_type, mapped_type>
  • 这里的 mapped_type() 是关键:如果是 int 就是 0,如果是自定义类型就是默认构造函数初始化的对象。

insert(...)

  • 调用 mapinsert 方法,尝试把这个键值对插入容器。
  • 它的返回值是 pair<iterator, bool>
    • bool 表示是否插入成功(true 表示插入了新元素,false 表示键已存在)
    • iterator 指向容器中该键对应的元素(无论新插入还是已存在)

insert(...).first

  • 取出返回的 pair 里的第一个成员,也就是指向元素的 iterator

*(...).first

  • 对迭代器解引用,得到 map 里的元素,类型是 pair<key_type, mapped_type>

(...).second

  • 取出这个 pair 里的第二个成员,也就是 mapped_type 类型的值,然后返回它的引用

💡****示例代码的本质

复制代码
for (auto& str : arr)
{
    countMap[str]++;
}
  • str 第一次出现时:operator[] 会自动插入 (str, 0),然后返回 0 的引用,接着执行 ++,变成 1
  • str 再次出现时:insert 会发现键已存在,直接返回指向已有元素的迭代器,然后对值执行 ++

⚠️****两个关键注意点

  1. **operator[] 会强制插入元素即使你只是想读取 countMap["不存在的键"],它也会默认插入一个值为 0(或默认构造值)的键值对,这会改变容器的大小。**👉 安全读取应该用 find()

复制代码
   auto it = countMap.find(str);
   if (it != countMap.end()) {
       // 存在,用 it->second
   } else {
       // 不存在,不插入
   }
  1. const map 不能用 operator[]因为 operator[] 有修改容器的副作用,所以 const std::map 不支持 [] 运算符,只能用 find()

C++ multimap

multimap和map的使⽤基本完全类似,主要区别点在于multimap⽀持关键值key冗余,那么 insert/find/count/erase都围绕着⽀持关键值key冗余有所差异,这⾥跟set和multiset完全⼀样,⽐如find时,有多个key,返回中序第⼀个。
其次就是multimap 不⽀持[] ,因为⽀持key冗余,[]就只能⽀持插⼊了,不能⽀持修改。


OJ题目运用

随机链表的复制

138. 随机链表的复制https://leetcode.cn/problems/copy-list-with-random-pointer/

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。

例如,如果原链表中有 XY 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 xy ,同样有 x.random --> y

返回复制链表的头节点。

cpp 复制代码
class Solution {
public:
    Node* copyRandomList(Node* head) {
        map<Node*,Node*> copymap;
        Node* copyhead=nullptr,*copytail=nullptr;
        Node* cur=head;
        while(cur)
        {
            if(copyhead==nullptr)
            {
                copyhead=copytail=new Node(cur->val);
            }
            else
            {
                copytail->next=new Node(cur->val);
                copytail=copytail->next;
            }
            copymap[cur]=copytail;
            cur=cur->next;
        }
        cur=head;
        Node* copy=copyhead;
        while(copy)
        {
            if(cur->random==nullptr)
            {
                copy->random=nullptr;
            }
            else
            {
                copy->random=copymap[cur->random];
            }
            copy=copy->next;
            cur=cur->next;
        }
        return copyhead;
    }
};

前K个高频单词

692. 前K个高频单词https://leetcode.cn/problems/top-k-frequent-words/

给定一个单词列表 words 和一个整数 k ,返回前 k个出现次数最多的单词。

返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序 排序。

方法一:排序

cpp 复制代码
class Solution {
public:
struct compare
{
    bool operator()(const pair<string,int>& x,const pair<string,int>& y)
    {
        if (x.second != y.second) {
        return x.second > y.second;  // 频率高的在前
         }
        return x.first < y.first; 
    }
};
    vector<string> topKFrequent(vector<string>& words, int k) {
        map<string,int> kv;
        for(auto& e:words)
        {
            kv[e]++;
        }
        vector<pair<string,int>> topk(kv.begin(), kv.end());
        sort(topk.begin(),topk.end(),compare());
        vector<string> topkk;
        for(int i=0;i<k;i++)
        {
            topkk.push_back(topk[i].first);
        }
        return topkk;
    }
};

补充**:这里因为sort库函数的稳定性差,所以用到了仿函数控制逻辑。**

如果我们使用stable_sort就不会打乱map的默认排序,仿函数就不用设计比较字符串大小

方法二:优先队列

cpp 复制代码
class Solution {
public:
    struct kv_pair{
        // 次数大的在前面,次数相等的,字典序小的在前面
        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]++;
        }

        // 大堆
        priority_queue<pair<string,int>,vector<pair<string,int>>,
        kv_pair> pq(countMap.begin(),countMap.end());

        vector<string> ret;
        for(size_t i = 0;i < k;++i)
        {
            ret.push_back(pq.top().first);
            pq.pop();
        }

        return ret;
    }

相关推荐
夕除6 小时前
spring boot 12
java·开发语言·python
05候补工程师6 小时前
【线性代数】核心考点复习笔记:二次型配方法、施密特正交化步骤与特征值经典题型详解
经验分享·笔记·线性代数·考研·算法
Huangjin007_6 小时前
【C++ STL篇(十一)】深入浅出红黑树:从原理到实现,一篇搞定
开发语言·c++
fqbqrr6 小时前
2605C++,C++继承类实现调试器
开发语言·c++
阿里嘎多学长6 小时前
2026-05-21 GitHub 热点项目精选
开发语言·程序员·github·代码托管
wjs20246 小时前
PHP 面向对象编程(OOP)深入解析
开发语言
Deep-w6 小时前
【MATLAB】基于遗传算法的直流电机 PI 控制器参数优化研究
开发语言·算法·matlab
海清河晏1116 小时前
数据结构 | 循环队列
数据结构·c++·visual studio
暴力求解6 小时前
数据结构---二叉树及堆的实现
数据结构·算法·二叉树