Re:从零开始的 C++ STL篇(十)map/set使用精讲:常见问题与典型用法(上)


◆ 博主名称: 晓此方-CSDN博客 大家好,欢迎来到晓此方的博客。
⭐️C++系列个人专栏: 主题曲:C++程序设计
⭐️ 踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰


目录


概要&序論

    这里是此方,好久不见。 在上两篇中,我们看到了AVL树和红黑树这两种高效的二叉搜索树,在C++的库里面,也有这样一对关联式容器,他们的底层是红黑树,本文将从实际使用角度出发,系统梳理其基本接口、常见操作以及典型应用场景。好的,让我们现在开始吧。

本文代码示例及测试所需要的头文件:

cpp 复制代码
#pragma once
#include<iostream>
#include<map>
#include<vector>
#include<set>
using namespace std;

本文参考文档:

C++Referance-set
C++Referance-map


一,关联式容器和序列式容器

    前面我们已经接触过STL中的部分容器如:string、vector、list、deque、array、forward_list等,这些容器统称为序列式容器,因为逻辑结构为线性序列(物理结构不一定线性 )的数据结构,两个位置存储的值之间一般没有紧密的关联关系 ,比如交换一下,他依旧是序列式容器。顺序容器中的元素是按他们在容器中的存储位置来顺序保存和访问的。

    关联式容器也是用来存储数据的,与序列式容器不同的是,关联式容器逻辑结构通常是非线性结构两个位置有紧密的关联关系,交换一下,他的存储结构就被破坏了。顺序容器中的元素是按关键字来保存和访问的。关联式容器有map/set系列和unordered_map/unordered_set系列。

    本章节讲解的map和set底层是红黑树(文档中没有说明),红黑树我们前面讲过是一颗平衡二叉搜索树。set是key搜索场景的结构,map是key/value搜索场景的结构。

二,set原型/接口

2.1set原型

cpp 复制代码
1  template < class T,                     // set::key_type/value_type
2          class Compare = less<T>,     //set::key_compare/value_compare
3          class Alloc = allocator<T>      // set::allocator_type
4          class set>
  1. set的声明如上,T就是set底层关键字key的类型。
  2. set默认要求T支持小于比较(底层红黑树要进行小于比较),如果不支持或者想按自己的需求走可以自行实现仿函数传给第二个模版参数。
  3. 默认的less支持小于比较,也就是正常二叉搜索树的规则(左小右大),还可以传greater,就是完全和二叉搜索树的规则反着来。
  4. set底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给第三个参数。
  5. 一般情况下,我们都不需要传后两个模版参数。

Tips:为什么传递一个less仿函数就足够支持内部比较了?

    比较的时候调换一下要比较的两个数就可以了。

2.2set的增删查效率

    set底层是用红黑树实现,增删查效率是 O(logN),迭代器遍历是走的搜索树的中序,所以是有序的

2.3set的接口介绍

   前面部分我们已经学习了vector/list等容器的使用,STL容器接口设计,高度相似,所以这里我们就不再一个接口一个接口的介绍,而是直接带着大家看文档,挑比较重要的接口进行介绍。

2.3.1构造函数

   set的构造我们关注以下几个接口即可。

2.3.1.1 无参数默认构造

原型:

cpp 复制代码
explicit set (const key_compare& comp = key_compare(),
              const allocator_type& alloc = allocator_type());

案例:

cpp 复制代码
set<int> s;
s.insert(1);
2.3.1.2 迭代器区间构造

原型:

cpp 复制代码
template <class InputIterator>
set (InputIterator first, InputIterator last,
     const key_compare& comp = key_compare(),
     const allocator_type& = allocator_type());

案例:

cpp 复制代码
vector<int> v = {1, 2, 2, 3};
set<int> s(v.begin(), v.end());
2.3.1.3 拷贝构造

原型:

cpp 复制代码
set (const set& x);

案例:

cpp 复制代码
set<int> s1 = {1, 2, 3};
set<int> s2(s1);
2.3.1.4 initializer_list 列表构造

原型:

cpp 复制代码
set (initializer_list<value_type> il,
     const key_compare& comp = key_compare(),
     const allocator_type& alloc = allocator_type());

案例:

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

2.3.2迭代器

   set的支持正向和反向迭代遍历 ,遍历默认按升序顺序,因为底层是二叉搜索树,迭代器遍历走的中序 ;支持迭代器就意味着支持范围for ,set的iterator和const_iterator都不支持迭代器修改数据,修改关键字数据,破坏了底层搜索树的结构。

cpp 复制代码
iterator   -> a bidirectional iterator to const value_type
 //正向迭代器 
iterator begin();
iterator end();
//反向迭代器
reverse_iterator rbegin();
reverse_iterator rend();

2.3.3插入接口

   value_type 是什么类型:实际上就是你插入的值的类型。 为了和map做对称,这里设计的key_type和value_type实际上是一回事。

关注以下几个接口

cpp 复制代码
// 单个数据插入,如果已经存在则插入失败
pair<iterator,bool> insert (const value_type& val);
// 列表插入,已经在容器中存在的值不会插入
void insert (initializer_list<value_type> il);
// 迭代器区间插入,已经在容器中存在的值不会插入
template <class InputIterator>
void insert (InputIterator first, InputIterator last);
// 查找 val,返回 val 所在的迭代器,没有找到返回 end()

2.3.4查找接口

   找到了返回迭代器(中序第一个 ,你要找的值在multiset中可能会出现很多个),找不到返回end()。

cpp 复制代码
iterator find (const value_type& val);
// 查找 val,返回 Val 的个数
const_iterator find (const value_type& val);

   为什么库里面看似 支持了一对只有返回值相同的接口。实际上这并不是一对只有返回值不同的接口重载。

   编译器内部会把他们解析为如下两种接口,这样就实现了重载。在调用的时候也是const对象匹配const版本。普通对象匹配普通版本。

cpp 复制代码
iterator       find(set* this, const value_type& val);
const_iterator find(const set* this, const value_type& val);

   查找不要去用标准库里面的那个find,那个find走的是遍历,效率低下,这个find走的是搜索树规则,效率更高。

2.3.5计数接口

   为了和multiset做对称设计而出现的一个接口,在set中用于查找一个元素在不在,在multiset中用于查找一个元素出现的次数。

cpp 复制代码
size_type count (const value_type& val) const;

   可以利用count 间接实现快速查找。

cpp 复制代码
if (s.count(x))
{
    cout << x << "存在!" << endl;
}

2.3.6删除接口

   和vector一样,删除后的迭代器会失效,vector已经详细讲过,这里不多讲。

  1. 给迭代器:删除你给他的位置,返回下一个位置的迭代器。
  2. 给值:相等的全部删完。
cpp 复制代码
// 删除一个迭代器位置的值
iterator  erase (const_iterator position);
// 删除 val,val 不存在返回 0,存在返回 1
// 删除几个数据就返回删除了几个数据,没有就返回0
// 为什么不采用bool?为了和multiset做对称
size_type erase (const value_type& val);
// 删除一段迭代器区间的值
iterator  erase (const_iterator first, const_iterator last);

2.3.7返回大于/小于/的关于val位置的迭代器

   这对接口的用途在容器中是找到第一个大于等于/小于等于你输入的那个val大小的数据。

cpp 复制代码
// 返回大于等于 val 位置的迭代器
iterator lower_bound (const value_type& val) const;
// 返回大于 val 位置的迭代器
iterator upper_bound (const value_type& val) const;

   给你个例子就明白了下面的代码中,删除的应该是30和60。

cpp 复制代码
#include <iostream>
#include <set>
using namespace std;
int main()
{
    set<int> s;
    for(int i=1;i<10;i++) s.insert(i*10); // 10~90
    for(auto e:s) cout<<e<<" ";
    cout<<endl;
    auto itlow=s.lower_bound(30); // <=30
    auto itup=s.upper_bound(50);  // >50
    s.erase(itlow,itup); // 删除[30,60)
    for(auto e:s) cout<<e<<" ";
    cout<<endl;
    return 0;
}

2.3.4insert和迭代器遍历使用样例

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);
    //set<int>::iterator it = s.begin();
    auto it = s.begin();
    while (it != s.end())
    {
        cout << *it << " ";
        ++it;
    }
    cout << endl;
    // error C3892: "it": 不能给常量赋值
    // *it = 1;
    cout << *it << " ";
    ++it;
    }
    cout << endl;
    // 插入一段initializer_list列表值,已经存在的值插入失败
    s.insert({ 2,8,3,9 });
    for (auto e : s)
    {
        cout << e << " ";
    }
    cout << endl;
    set<string> strset = { "sort", "insert", "add" };
    // 遍历string比较ascii码大小顺序遍历的
    for (auto& e : strset)
    {
        cout << e << " ";
    }
    cout << endl;
    return 0;
}

2.3.5find和erase使用样例

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;
    // 直接删除x
    int x;
    cin >> x;
    int num = s.erase(x); // 返回删除元素个数(0或1)
    if (num == 0)
    {
        cout << x << "不存在!" << endl;
    }
    for (auto e : s)
    {
        cout << e << " ";
    }
    cout << endl;
    // 直接查找后利用迭代器删除x
    auto pos = s.find(x);
    if (pos != s.end())
    {
        s.erase(pos);
    }
    else
    {
        cout << x << "不存在!" << endl;
    }
    for (auto e : s)
    {
        cout << e << " ";
    }
    cout << endl;
    // 算法库的查找 O(N)
    auto pos1 = find(s.begin(), s.end(), x);
    // set自身实现的查找 O(logN)
    auto pos2 = s.find(x);
    // 利用count间接实现快速查找
    cin >> x;
    if (s.count(x))
    {
        cout << x << "在!" << endl;
    }
    else
    {
        cout << x << "不存在!" << endl;
    }
    return 0;
}
cpp 复制代码
#include<iostream>
#include<set>
using namespace std;
int main()
{
    std::set<int> myset;
    for (int i = 1; i < 10; i++)
        myset.insert(i * 10); // 插入 10 20 30 ... 90
    for (auto e : myset)
    {
        cout << e << " ";
    }
    cout << endl;
    // 实现查找到的 [itlow, itup) 包含 [30, 60] 区间
    // 返回 >= 30
    auto itlow = myset.lower_bound(30);
    // 返回 > 60
    auto itup = myset.upper_bound(60);
    // 删除这段区间的值
    myset.erase(itlow, itup);
    for (auto e : myset)
    {
        cout << e << " ";
    }
    cout << endl;
    return 0;
}

2.4mulitset与set的比较

   multiset和set的使用基本完全类似 ,主要区别点在于multiset 支持值冗余,那么insert/find/count/erase都围绕着支持值冗余有所差异,具体参看下面的样例代码理解。

cpp 复制代码
#include<iostream>
#include<set>
using namespace std;
int main()
{
    // 相比set不同的是,multiset是排序,但是不去重
    multiset<int> s = { 4,2,7,2,4,8,4,5,4,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;
    // 相比set不同的是,count会返回x的实际个数
    cout << s.count(x) << endl;
    // 相比set不同的是,erase给值时会删除所有的x
    s.erase(x);
    for (auto e : s)
    {
        cout << e << " ";
    }
    cout << endl;
    return 0;
}

三,set实战

3.1实战演示01------两个数组的交集-力扣

   题目传送门两个数组的交集-力扣

3.1.1题目思路分析

   看到题目最先想到的一定是暴力解法:排序+双指针遍历,相等则输出。但是排序+去重+遍历的消耗是NlogN+N,并且去重麻烦 。有没有效率更高的方法?有点兄弟有的。这个时候就该set登场了。

   将数据直接放入两个set中,自动实现有序化+去重。第二步,如何遍历:首先我们用迭代器。

  • 两个迭代器指向的值相等:两个迭代器都向前进。
  • 两个迭代器指向的值不相等,指向的数比较小的那个迭代器向前走。

   为什么这么设计: ,如下图,用一个数组模拟迭代器遍历set的实际情况,这里it1和it2指向的值不同,如果这个时候让it2(指向的数更大)往前走,就会错过8这种情况。

   当两个指针指向的值不相等时,由于两个序列都是有序的,较小的那个值在对方序列的当前位置之后不可能再出现(因为后面的元素只会更大 )。因此,这个较小值已经不可能参与后续的匹配,继续保留只会产生无效比较,所以必须让指向较小值的指针前进,从而保证既不漏解,又提高效率。

3.1.2完整代码展示

cpp 复制代码
class Solution 
{
public:
    vector<int> intersection(vector<int>& num1, vector<int>& num2) 
    {
        set <int> se1(num1.begin(),num1.end());
        set <int> se2(num2.begin(),num2.end());
        vector<int> v;
        set<int>::iterator it1=se1.begin();
        set<int>::iterator it2=se2.begin();
        while(it1!=se1.end()&&it2!=se2.end())
        {
            if(*it1==*it2)
            {
                v.push_back(*it1);
                it1++;
                it2++;
            }
            else
            {
                if(*it1>*it2)
                {
                    it2++;
                }
                else it1++;
            }
        }
        return v;
    }
};

3.1.3题目背后的思想

   交集/差集思想。 这里讲一讲交集差集思想(集合论)在工程中的运用。

   如下,从手机中加入一张图片,云相册会比对云相册和手机中的交集和差集 ,将/d上传到云相册。云相册再和pc和pad比对交集和差集 ,将图片/d下载到pad和pc。

   手机上的图片修改了,和云相册尝试同步:首先调用两张图片的"最后修改时间"接口,如果不一致就会将更新的那张图覆盖给旧的那张图。更新最后修改时间。这就是工业同步算法的一个基本框架。

3.2实战演示02------环形链表II-力扣

题目传送门: 环形链表II-力扣

   数据结构初阶阶段,我们通过证明一个指针从头开始走一个指针从相遇点开始走,会在入口点相遇,理解证明都会很麻烦 。这里我们使用set查找记录解决非常简单方便,这里体现了set在解决一些问题时的价值,完全是降维打击。

   直接遍历插入环形链表的结点指针,当插入失败的时候就说明找到环了,这个时候直接返回。

cpp 复制代码
class Solution 
{
public:
    ListNode *detectCycle(ListNode *head) 
    {
        set<ListNode *> se;
        ListNode* node=head;
        ListNode* ret=0;
        while(node!=nullptr)
        {
            auto judge=se.insert(node);
            if(judge.second==false)
            {
                ret=node;
                break;
            }
            node=node->next;
        }
        return ret;
    }
};

好了,本期内容到此结束,我是此方,我们下期再见。バイバイ!

相关推荐
地平线开发者2 小时前
profiler debug 工具用法与高一致性策略
算法·自动驾驶
编程大师哥2 小时前
匿名函数 lambda + 高阶函数
java·python·算法
isyangli_blog2 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb2008112 小时前
FastAPI APIRouter
开发语言·python
Benszen2 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆2 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木2 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
我叫袁小陌2 小时前
算法解题思路指南
算法
MC皮蛋侠客3 小时前
C++17 多线程系列(五):C++17 并行算法——从串行到并行的零成本迁移
c++·多线程
地平线开发者3 小时前
Conv+BN+Add+ReLU 融合机制简介
算法·自动驾驶