【C++进阶】map和set

目录

本节学习目标

[1. 序列式容器 和 关联式容器](#1. 序列式容器 和 关联式容器)

[1.1 序列式容器](#1.1 序列式容器)

[1.2 关联式容器](#1.2 关联式容器)

[2 set 系列使用(头文件 )](#2 set 系列使用(头文件 ))

[2.1 set 模板声明](#2.1 set 模板声明)

[2.2 set 常用构造](#2.2 set 常用构造)

[2.3 迭代器](#2.3 迭代器)

[2.4 set 核心成员接口](#2.4 set 核心成员接口)

[2.5 set 基础代码示例](#2.5 set 基础代码示例)

[2.6 find、erase、lower_bound /upper_bound 示例](#2.6 find、erase、lower_bound /upper_bound 示例)

[2.7 multiset(允许重复元素)](#2.7 multiset(允许重复元素))

[2.8 LeetCode 习题](#2.8 LeetCode 习题)

[习题 1:349. 两个数组的交集](#习题 1:349. 两个数组的交集)

[习题 2:142. 环形链表 II,检测环入口](#习题 2:142. 环形链表 II,检测环入口)

[3 map 系列使用(头文件)](#3 map 系列使用(头文件))

[3.1 map 模板](#3.1 map 模板)

[3.2 pair 键值对](#3.2 pair 键值对)

[3.3 map 构造、迭代器](#3.3 map 构造、迭代器)

[3.4 map 核心接口](#3.4 map 核心接口)

[⭐重中之重 operator \[\]](#⭐重中之重 operator [])

[3.5 map 完整示例](#3.5 map 完整示例)

示例:单词词频统计,两种写法

[3.6 multimap](#3.6 multimap)

[3.7 LeetCode 习题](#3.7 LeetCode 习题)

[习题 1:138 复制随机链表](#习题 1:138 复制随机链表)

[习题 2:692. 前 K 个高频单词](#习题 2:692. 前 K 个高频单词)

📔本篇核心考点总结


本节学习目标

  1. 分清序列式容器 与关联式容器,了解底层红黑树
  2. 熟练掌握set / multiset接口:构造、迭代器、insert、find、erase、lower_bound、upper_bound,分清 set 去重、multiset 允许重复
  3. 掌握pair键值对结构,理解 map 存储的pair<const Key,T>
  4. 熟练掌握map / multimap,吃透operator[]底层逻辑(插入、查找、修改三合一)
  5. 区分 map 和 multimap 差异,multimap 不支持operator[]
  6. 看懂力扣实战习题:两个数组交集、环形链表检测、复制随机链表、前 K 个高频单词

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

1.1 序列式容器

代表:string、vector、list、deque、array、forward_list

  • 逻辑是线性序列;元素之间没有强制大小关系。
  • 元素依靠存储位置保存、访问;交换两个元素,容器逻辑不会被破坏。

1.2 关联式容器

代表:set / multiset、map / multimap(底层红黑树,平衡二叉搜索树 );还有unordered_set/unordered_map哈希版本。

  • 逻辑是非线性树形结构;元素之间依靠关键字 key 维持大小关系;随意交换元素会直接破坏容器结构。
  • 元素依靠关键字 key保存和访问;增删查时间复杂度 \(O(logN)\)。

对应前面二叉搜索树知识:

  • set:对应 key 搜索场景,只存 key,自动排序 + 默认去重
  • map:对应 key‑value 搜索场景,存键值对 pair,key 唯一

2 set 系列使用(头文件 <set>)

2.1 set 模板声明

cpp 复制代码
template < class T,
          class Compare = less<T>,   //比较仿函数,默认less<T>升序
          class Alloc = allocator<T> >
class set;
  • T:存放元素类型;
  • Compare:仿函数,默认less<T>,从小到大升序;传入greater<T>实现降序;
  • Alloc:空间配置器,内存分配,一般不用管。
  • 底层红黑树;迭代器遍历是中序遍历,输出有序;
  • set 迭代器是双向迭代器,迭代器指向的内容是 const,不能修改元素!修改 key 会破坏红黑树结构。

2.2 set 常用构造

  1. 无参构造:set<int> s;
  2. 迭代器区间构造:set<int> s(v.begin(),v.end()); 把容器区间元素传入构造,自动排序去重
  3. 拷贝构造:set<int> s2(s1);
  4. 初始化列表构造:set<int> s{4,2,7,2,8};

2.3 迭代器

  • begin():返回第一个元素迭代器(最小)
  • end():末尾后一位迭代器,代表不存在元素,不能解引用
  • rbegin() / rend():反向迭代器,逆序遍历

set 支持范围 for,底层等价于正向迭代器遍历。 ❗ *it = xxx; 编译报错,set 元素不允许修改。

2.4 set 核心成员接口

表格

接口 功能说明
pair<iterator,bool> insert(val) 插入元素;key 存在则插入失败;返回 pair:first 迭代器,second 插入是否成功 true/false
insert(init_list) 初始化列表批量插入,重复值忽略
insert(first,last) 迭代器区间批量插入,重复值忽略
iterator find(val) 查找 val,找到返回对应迭代器;找不到返回end(),\(O(logN)\)
size_type count(val) 统计 val 的个数;set 要么 0 要么 1;multiset 返回真实出现次数
iterator erase(pos) 删除迭代器位置元素,返回下一个元素迭代器
size_type erase(val) 按值删除;set 存在就删返回 1,不存在返回 0
iterator erase(first,last) 删除 [first,last) 区间元素
lower_bound(val) 返回大于等于 val的第一个元素迭代器
upper_bound(val) 返回大于 val的第一个元素迭代器

⚠️注意:std::find是算法库函数,遍历查找\(O(N)\);优先使用容器自带find(),红黑树查找\(O(logN)\)。

2.5 set 基础代码示例

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

int main()
{
    set<int> s;
    //set<int, greater<int>> s; //降序版本
    s.insert(5);
    s.insert(2);
    s.insert(7);
    s.insert(5); //重复,插入失败

    auto it = s.begin();
    while(it != s.end())
    {
        //*it = 10; //编译报错,不能修改set元素
        cout << *it << " ";
        ++it;
    }
    cout << endl;

    //初始化列表批量插入,重复自动丢弃
    s.insert({2,8,3,9});
    for(auto e : s)
    {
        cout << e << " ";
    }
    cout << endl;

    set<string> strset = {"sort", "insert", "add"};
    for(auto& e : strset)
    {
        cout << e << " ";
    }
    return 0;
}

2.6 find、erase、lower_bound /upper_bound 示例

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

int main()
{
    set<int> s = {4,2,7,2,8,5,9};
    for(auto e : s) cout << e << " ";
    cout << endl;

    s.erase(s.begin()); //删除最小值
    for(auto e : s) cout << e << " ";
    cout << endl;

    int x;
    cin >> x;
    size_type num = s.erase(x);
    if(num == 0)
    {
        cout << x << "不存在\n";
    }

    //find查找删除
    cin >> x;
    auto pos = s.find(x);
    if(pos != s.end())
    {
        s.erase(pos);
    }
    else
    {
        cout << x << "不存在\n";
    }

    //count间接判断元素是否存在
    cin >> x;
    if(s.count(x))
        cout << x << "在集合中\n";
    else
        cout << x << "不存在\n";

    // lower_bound upper_bound
    set<int> myset;
    for(int i = 1; i < 10; i++)
        myset.insert(i*10); //10,20,30...90
    //取区间 [30,60]
    auto itlow = myset.lower_bound(30); //>=30
    auto itup = myset.upper_bound(60);  //>60
    myset.erase(itlow, itup); //删除30、40、50、60
    for(auto e : myset) cout << e << " ";
    return 0;
}

2.7 multiset(允许重复元素)

multiset接口几乎和 set 完全一样,唯一区别:允许插入重复值,不去重。 重点差异:

  1. insert:重复值可以多次插入成功;
  2. find(x):返回中序遍历第一个等于 x的迭代器;
  3. count(x):返回 x 实际出现总个数;
  4. erase(x):传入值,会删除容器中全部等于 x 的元素!;如果只想删一个,先用 find 拿到迭代器,erase (迭代器)。

multiset 示例:

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

int main()
{
    multiset<int> s = {4,2,7,2,4,8,4,5,4,9};
    for(auto e : s) cout << e << " ";
    cout << endl;

    int x = 4;
    auto pos = s.find(x);
    while(pos != s.end() && *pos == x)
    {
        cout << *pos << " ";
        ++pos;
    }
    cout << "\ncount=" << s.count(x) << endl;

    //s.erase(x); //⚠️删除全部4!!
    //只删除一个4,传迭代器
    auto delPos = s.find(x);
    if(delPos != s.end())
        s.erase(delPos);

    for(auto e : s) cout << e << " ";
    return 0;
}

2.8 LeetCode 习题

习题 1:349. 两个数组的交集349. 两个数组的交集 - 力扣(LeetCode)

思路:两个 set,有序,双指针遍历求交集。

cpp 复制代码
#include<vector>
#include<set>
using namespace std;
class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        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:142. 环形链表 II,检测环入口142. 环形链表 II - 力扣(LeetCode)

set 存储链表结点指针;遍历,如果 insert 返回 false 说明结点已经出现过,就是环入口。

cpp 复制代码
struct ListNode {
    int val;
    ListNode *next;
};
#include<set>
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        set<ListNode*> s;
        ListNode* cur = head;
        while(cur)
        {
            auto ret = s.insert(cur);
            if(ret.second == false)
            {
                return cur;
            }
            cur = cur->next;
        }
        return nullptr;
    }
};

3 map 系列使用(头文件<map>)

3.1 map 模板

cpp 复制代码
template <class Key, class T,
          class Compare = less<Key>,
          class Alloc = allocator<pair<const Key,T>>>
class map;
  • Key:关键字 key;T:映射的 value;
  • 底层红黑树,存储pair<const Key, T>;key 是 const,不允许修改!value 可以修改;
  • key 唯一,自动按 key 升序排序;
  • 迭代器双向迭代器。

3.2 pair 键值对

map 结点存储 pair,first存 key,second存 value。

cpp 复制代码
template<class T1,class T2>
struct pair
{
    T1 first;
    T2 second;
    pair(){}
    pair(const T1& a, const T2& b):first(a),second(b){}
};
//辅助函数,构造pair
template<class T1,class T2>
pair<T1,T2> make_pair(T1 x,T2 y)
{
    return pair<T1,T2>(x,y);
}

3.3 map 构造、迭代器

  • 无参构造、迭代器区间构造、拷贝构造、初始化列表构造;
  • begin() end() rbegin() rend()双向迭代器;
  • 遍历:it->first取 key,it->second取 value;it->first 不能修改,it->second 可以修改。

3.4 map 核心接口

find、count、erase、lower_bound、upper_bound 接口和 set 用法几乎一致,只是操作对象是 key。

insert返回:pair<iterator,bool>

  • 插入成功:first 是新元素迭代器,second=true;
  • key 已经存在插入失败:first 指向原有 key 结点迭代器,second=false。
⭐重中之重 operator \[\]
cpp 复制代码
mapped_type& operator[] (const key_type& k);

operator \[\] 伪代码内部逻辑:

cpp 复制代码
mapped_type& operator[] (const key_type& k)
{
    //key不存在:插入k,value为默认构造值;key存在,insert失败拿到原有迭代器
    auto ret = insert({k,mapped_type()});
    iterator it = ret.first;
    return it->second; //返回value的引用
}

`operator \[\] 三合一功能:

  1. key 不存在:插入新键值对(value 默认值),返回 value 引用;可以用来插入、赋值;
  2. key 存在:不插入,直接返回已有 value 引用;可以读取、修改 value;

⚠️注意:只做查找不要直接用[]!key 不存在会偷偷插入一条数据,污染 map;单纯查找优先用find()。

3.5 map 完整示例

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

int main()
{
    //初始化列表构造
    map<string,string> dict = {{"left","左边"},{"right","右边"},{"insert","插入"}};
    auto it = dict.begin();
    while(it != dict.end())
    {
        cout << it->first << ":" << it->second << endl;
        ++it;
    }

    //四种insert插入pair
    pair<string,string> kv1("first","第一个");
    dict.insert(kv1);
    dict.insert(pair<string,string>("second","第二个"));
    dict.insert(make_pair("sort","排序"));
    dict.insert({"auto","自动的"});

    dict.insert({"left","左边剩余"}); //key已经存在,插入失败!

    //范围for遍历
    for(const auto& e : dict)
    {
        cout << e.first << "->" << e.second << endl;
    }

    //find查找
    string str;
    while(cin >> str)
    {
        auto ret = dict.find(str);
        if(ret != dict.end())
        {
            cout << ret->second << endl;
        }
        else
        {
            cout << "没有该单词\n";
        }
    }
    return 0;
}
示例:单词词频统计,两种写法

写法 1:使用 find 判断

cpp 复制代码
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(auto& e : countMap)
    {
        cout << e.first << " : " << e.second << endl;
    }
    return 0;
}

写法 2:利用operator[]简洁统计(业务最常用)

cpp 复制代码
int main()
{
    string arr[] = {"苹果","西瓜","苹果","西瓜","苹果","香蕉","苹果","香蕉"};
    map<string,int> countMap;
    for(const auto& str : arr)
    {
        countMap[str]++;
    }
    for(auto& e : countMap)
    {
        cout << e.first << " : " << e.second << endl;
    }
    return 0;
}

operator \[\] 多种行为演示

cpp 复制代码
int main()
{
    map<string,string> dict;
    dict.insert(make_pair("sort","排序"));

    dict["insert"]; //key不存在,插入insert,value为空字符串
    dict["left"] = "左边"; //插入+赋值
    dict["left"] = "左边、剩余"; //修改已存在key的value
    cout << dict["left"] << endl; //读取value
    return 0;
}

3.6 multimap

multimap允许 key 重复;

  • 接口和 map 基本一致;
  • ❗multimap 不支持 operator \[\],因为 key 可以重复,不知道该取哪一个 key 对应的 value;
  • find(key)返回中序第一个匹配 key 的迭代器;
  • erase(key)删除全部等于 key 的键值对;只想删一个要传迭代器。

3.7 LeetCode 习题

习题 1:138 复制随机链表138. 随机链表的复制 - 力扣(LeetCode)

利用 map 保存原结点→拷贝结点映射,快速处理 random 指针。

cpp 复制代码
class Node {
public:
    int val;
    Node* next;
    Node* random;
    Node(int _val) {
        val = _val;
        next = nullptr;
        random = nullptr;
    }
};
#include<map>
class Solution {
public:
    Node* copyRandomList(Node* head) {
        map<Node*,Node*> nodeMap;
        Node* copyhead = nullptr;
        Node* copytail = nullptr;
        Node* cur = head;
        //第一步:拷贝val,建立原结点和拷贝结点映射
        while(cur != nullptr)
        {
            if(copytail == nullptr)
            {
                copyhead = copytail = new Node(cur->val);
            }
            else
            {
                copytail->next = new Node(cur->val);
                copytail = copytail->next;
            }
            nodeMap[cur] = copytail;
            cur = cur->next;
        }
        //第二步处理random指针
        cur = head;
        Node* copy = copyhead;
        while(cur != nullptr)
        {
            if(cur->random == nullptr)
            {
                copy->random = nullptr;
            }
            else
            {
                copy->random = nodeMap[cur->random];
            }
            cur = cur->next;
            copy = copy->next;
        }
        return copyhead;
    }
};
习题 2:692. 前 K 个高频单词692. 前K个高频单词 - 力扣(LeetCode)

map 统计单词频次;再排序,要求:频次高优先;频次相同字典序小优先。

cpp 复制代码
#include<vector>
#include<map>
#include<algorithm>
#include<string>
using namespace std;

struct Compare
{
    bool operator()(const pair<string,int>& x,const pair<string,int>& y) const
    {
        //次数大靠前;次数相等,字典序小靠前
        return x.second > y.second || (x.second == y.second && x.first < y.first);
    }
};

class Solution {
public:
    vector<string> topKFrequent(vector<string>& words, int k) {
        map<string,int> countMap;
        for(auto& e : words)
        {
            countMap[e]++;
        }
        vector<pair<string,int>> v(countMap.begin(),countMap.end());
        sort(v.begin(),v.end(),Compare());
        vector<string> res;
        for(int i = 0; i < k; i++)
        {
            res.push_back(v[i].first);
        }
        return res;
    }
};

📔本篇核心考点总结

  1. 序列式容器 :依靠位置存储;关联式容器 (set/map 底层红黑树) 依靠 key 关键字存储,O (logN)。
  2. set/multiset:存 key;set 去重;multiset 允许重复;迭代器元素 const,不能修改元素;find()容器内查找优于算法库 std::find。
  3. lower_bound >=val;upper_bound >val;erase 传值 multiset 会删除全部匹配元素,只想删一个要传迭代器。
  4. map/multimap存储pair<const Key,T>;key 不可修改,value 可以修改。
  5. map::operator[]三合一:key 不存在就插入;key 存在读取 / 修改 value;只查询不要用 \[\],会插入垃圾数据,优先 find。
  6. multimap支持重复 key,没有 operator \[\]。
  7. insert 返回pair<iterator,bool>,first 是迭代器,second 标记插入成功与否。

面试简答

Q:set 的迭代器为什么不能修改元素?

A:set 底层红黑树,修改 key 会破坏红黑树有序规则,所以迭代器指向内容是 const。

Q:map 的 operator \[\] 做了什么?

A:调用 insert,key 不存在插入默认 value;key 存在拿到原有结点;返回 value 的引用,用于读取或者修改。

Q:set::erase 传迭代器 和传值对于 multiset 有什么区别?

A:erase (迭代器):只删除这一个结点;erase (值) 会删除容器中所有等于该值的结点。

相关推荐
lie..2 小时前
30天从零开始学AI应用开发(Day 12):像产品一样稳:流式输出、报错重试、花钱控制
开发语言·人工智能·php
傻啦嘿哟2 小时前
Python的默认参数把我坑惨了,原来写[]和写None的区别这么大
开发语言·python·机器学习
需要8262 小时前
分布式事务:Seata AT/TCC/SAGA 的取舍与踩坑
java·spring boot·分布式·spring·spring cloud
晨小安2 小时前
ThreadLocal及其内存泄漏问题深度解析
java·jvm·tomcat·哈希表
DeepAgent2 小时前
AI Agent 工程实践(49):一次真实优化——从 Agent v1 到 v2
开发语言·人工智能·agent
知识分享小能手2 小时前
C++ 学习教程,从入门到精通,C++对象和类 — 详细知识点总结(10)
开发语言·c++·学习
m0_587383002 小时前
深圳24小时自助健身房解决方案实战:从系统架构到部署指南
java·人工智能·spring boot·spring·系统架构·需求分析
顺顺 尼2 小时前
linux 进程信号(上)
linux·c++·算法
郑州光合科技余经理3 小时前
同城电商系统:库存变更怎么同步到订单
java·开发语言·前端·后端·uni-app·php·ai编程