C++ -- STL【set/map和multiset/multimap的使用】

目录

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

2、set的介绍

3、set的功能

[3.1 set的初始化](#3.1 set的初始化)

[3.2 set的迭代器](#3.2 set的迭代器)

[3.3 set的常见成员函数](#3.3 set的常见成员函数)

[3.4 两道例题](#3.4 两道例题)

4、mutiset

5、map的介绍

6、map的功能

[6.1 map的初始化](#6.1 map的初始化)

[6.2 map的迭代器](#6.2 map的迭代器)

[6.3 map的常见成员函数](#6.3 map的常见成员函数)

[6.3.1 insert](#6.3.1 insert)

[6.3.2 [ ] 运算符重载](#6.3.2 [ ] 运算符重载)

[6.4 两道例题](#6.4 两道例题)

7、pair

8、multimap


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

  • 序列式容器

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

  • 关联式容器

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

本章节讲解的 map 和 set 底层是红黑树,红黑树是⼀颗平衡二叉搜索树。set 是 key 搜索场景的结构,map 是 key/value 搜索场景的结构。

2、set的介绍

在 C++ 中,set 是一种关联容器,用于存储唯一的元素。其具有以下这几个特点:

具体可参考官方文档 ------ set

3、set的功能

首先 set 与 STL 其他大多数容器一样,为了支持所有类型,所以是一个类模版。

3.1 set的初始化

set 初始化会调用构造函数,其构造函数分别重载了以下几种方式:

cpp 复制代码
void Test1()
{
	//1.默认无参构造
	set<int> s1;
	//2.迭代器区间初始化
	string str("tata");
	set<int> s2(str.begin(),str.end());
	//3.拷贝构造
	set<int> s3(s1);
	//4.指定比较方式
	set<int, greater<int>> s4;
}

3.2 set的迭代器

成员函数 功能
begin 获取容器中第一个元素的正向迭代器
end 获取容器中最后一个元素下一个位置的正向迭代器
rbegin 获取容器中最后一个元素的反向迭代器
rend 获取容器中第一个元素前一个位置的反向迭代器

这些函数与其他容器的迭代器成员函数无论是功能还是用法都是一模一样的.

cpp 复制代码
void Test2()
{
	vector<int> arr = { 1,2,3,4,5,6,7 };
	//迭代器区间初始化
	set<int> s1(arr.begin(), arr.end());
	//正向迭代器
	set<int>::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	//反向迭代器
	set<int>::reverse_iterator rit = s1.rbegin();
	while (rit != s1.rend())
	{
		cout << *rit << " ";
		++rit;
	}
	cout << endl;
}

3.3 set的常见成员函数

以下是 set 常见的成员函数:

成员函数 功能
insert 插入指定元素
erase 删除指定元素
find 查找指定元素
size 获取容器中元素的个数
empty 判断容器是否为空
clear 清空容器
swap 交换两个容器中的数据
count 获取容器中指定元素值的元素个数
cpp 复制代码
void Test3()
{
	set<int> s1;
	s1.insert(1);
	s1.insert(1);
	s1.insert(2);
	s1.insert(2);
	s1.insert(3);
	s1.insert(4);
	//元素个数
	cout << "元素个数:";
	cout << s1.size() << endl;
	//正向迭代器
	set<int>::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	set<int>::iterator pos = s1.find(2);
	//如果找不到会返回end()迭代器
	if (pos != s1.end())
	{
		s1.erase(pos);
	}
	it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}
cpp 复制代码
void Test4()
{
	set<int> s1;
	s1.insert(1);
	s1.insert(2);
	s1.insert(3);
	s1.insert(4);
	set<int>::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	//判断是否存在
	if (s1.count(2))
	{
		cout << "元素存在" << endl;
	}
	//清空
	s1.clear();
	//判断是否为空
	if (s1.empty())
	{
		cout << "容器为空" << endl;
	}
}

注意 :因为删除后迭代器失效 ,所以 erase 返回迭代器的下一个位置。

所以我们删除迭代器后就不要访问了。

3.4 两道例题

题目1:两个数组的交集

思路分析:

cpp 复制代码
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)
            {
                it2++;
            }
            else if(*it1<*it2)
            {
                it1++;
            }
            else
            {
                ret.push_back(*it1);
                it1++;
                it2++;
            }
        }
        return ret;
    }
};
  • 同步算法

题目2:环形链表2

思路分析:

cpp 复制代码
class Solution {
public:
    ListNode *detectCycle(ListNode *head) 
    {
        set<ListNode*> tmp;
        ListNode *cur=head;
        while(cur!=nullptr&&!tmp.count(cur))
        {
            tmp.insert(cur);
            cur=cur->next;
        }
        return cur;
    }
};

4、mutiset

multiset 的使用方式与 set 基本一致,唯一的区别就在于 multiset 允许键值冗余,即可存储重复元素。

cpp 复制代码
void Test5()
{
	multiset<int> s1;
	//支持键值冗余
	s1.insert(1);
	s1.insert(1);
	s1.insert(2);
	s1.insert(2);
	s1.insert(3);
	s1.insert(4);
	multiset<int>::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		++it;
	}
}

值得注意的是:multiset 的 find 返回底层搜索树中序的第一个值为 val 的元素的迭代器,而 set 返回的是 val 元素的迭代器。

5、map的介绍

map是 C++ 中的关联式容器,具有以下特性:

6、map的功能

首先 map 同样与 STL 其他大多数容器一样,为了支持所有类型,所以是一个类模版。

6.1 map的初始化

map 初始化会调用构造函数,其构造函数分别重载了以下几种方式:

cpp 复制代码
void Test6()
{
	//1.默认无参构造
	map<int,int> m1;
	//2.迭代器区间初始化
	map<int, int> m2(m1.begin(), m1.end());
	//3.拷贝构造
	map<int, int> m3(m1);
	//4.指定比较方式
	map<int, int, greater<int>> m4;
}

6.2 map的迭代器

成员函数 功能
begin 获取容器中第一个元素的正向迭代器
end 获取容器中最后一个元素下一个位置的正向迭代器
rbegin 获取容器中最后一个元素的反向迭代器
rend 获取容器中第一个元素前一个位置的反向迭代器

这些函数也与其他容器的迭代器成员函数无论是功能还是用法都是一模一样的。

cpp 复制代码
void Test7()
{
	map<int, string> m;
	m.insert(pair<int, string>(1, "one"));
	m.insert(pair<int, string>(2, "two"));
	m.insert(pair<int, string>(3, "three"));
	//正向迭代器
	map<int, string>::iterator it = m.begin();
	while(it != m.end())
	{
		cout << "<" << it->first << "," << it->second << ">" << " ";
		++it;
	}
	cout << endl;
	//反向迭代器
	map<int, string>::reverse_iterator rit = m.rbegin();
	while (rit != m.rend())
	{
		cout << "<" << rit->first << "," << rit->second << ">" << " ";
		++rit;
	}
	cout << endl;
}

6.3 map的常见成员函数

map 与 set 的成员函数类似,只不过多了一个[]运算符重载。

成员函数 功能
insert 插入指定元素
erase 删除指定元素
find 查找指定元素
size 获取容器中元素的个数
empty 判断容器是否为空
clear 清空容器
swap 交换两个容器中的数据
count 获取容器中指定元素值的元素个数
[ ] 运算符重载 根据对应的 key 获取其 val 的值

相似的函数用法也是相同的,我们不在重复介绍。我们重点介绍以下几个函数:

6.3.1 insert

map 的插入函数的函数原型如下:

cpp 复制代码
pair<iterator,bool> insert (const value_type& val);

其中 insert 函数的参数为是 value_type 类型的,实际上 value_type 只是被 typedef 之后的类型,其真实类似为:

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

因此,我们在向 map 容器插入元素时,需要用 key 和 value 构造一个 pair 对象,再插入。

而构造 pair 对象常见的有两种方式,第一种就是构造匿名对象插入:

cpp 复制代码
//匿名对象插入
map<int, string> m;
m.insert(pair<int, string>(1, "one"));

但是这种方式过于繁琐,所以一般我们常用的是以下这种:

通过这个 make_pair 函数的返回值进行构造。

cpp 复制代码
//make_pair函数构造
map<int, string> m;
m.insert(make_pair(1,"one"));

最后 insert 的返回值也是一个 pair 类型,其含义为:

  • 若待插入元素的键值 key 在 map 当中不存在,则 insert 函数插入成功,并返回插入后元素的迭代器和 true。
  • 若待插入元素的键值 key 在 map 当中已经存在,则 insert 函数插入失败,并返回 map 当中键值为 key 的元素的迭代器和 false。

6.3.2 [ ] 运算符重载

map 的 [ ] 运算符重载的函数原型如下:

cpp 复制代码
mapped_type& operator[] (const key_type& k);

其中 mapped_type 就是 value 的类型,并且这个重载实现方式是这样的:

cpp 复制代码
(*((this->insert(make_pair(k, mapped_type()))).first)).second

这样咋一看其实并不好理解,我们可以先将其拆分为三步:

  1. 第一步调用 insert 函数插入键值对。
  2. 第二步接收 insert 函数的返回值,并获取相应的迭代器。
  3. 第三步通过该迭代器得到 value 的值返回。
cpp 复制代码
mapped_type& operator[] (const key_type& k)
{
	//第一步调用insert函数插入键值对。
	pair<iterator, bool> ret = insert(make_pair(k, mapped_type()));
	//第二步接收insert函数的返回值,并获取相应的迭代器。
	iterator it = ret.first;
	//第三步通过该迭代器得到value的值返回。
	return it->second;
}

如果 [ ] 调用的值不存在,则 [ ] 就相当于插入;如果 [ ] 调用的值存在,则 [ ] 就相当于得到对应的 value 。所以通过该函数,我们可以简化 map 的插入与修改操作。

cpp 复制代码
void Test8()
{
	map<int, string> m;
	//插入
	m[1] = "one";
	m[2] = "two";
	m[3] = "three";
	map<int, string>::iterator it = m.begin();
	while (it != m.end())
	{
		cout << "<" << it->first << "," << it->second << ">" << " ";
		++it;
	}
	cout << endl;
	//修改
	m[1] = "1";
	m[2] = "2";
	m[3] = "3";
	it = m.begin();
	while (it != m.end())
	{
		cout << "<" << it->first << "," << it->second << ">" << " ";
		++it;
	}
	cout << endl;
}

6.4 两道例题

题目1:随机链表的复制

思路分析:

cpp 复制代码
class Solution {
public:
    Node* copyRandomList(Node* head) 
    {
        // 哈希表:key=原链表节点地址,value=对应拷贝的新节点地址
        // 作用:快速映射原节点与新节点,解决random指针指向问题
        std::map<Node*, Node*> hash;
        
        Node* phead = nullptr; // 拷贝链表的头指针(最终返回值)
        Node* tail = nullptr;  // 拷贝链表的尾指针(用于构建next链)
        Node* pur = head;      // 遍历原链表的指针("pur"=original,原节点)

        // 第一步:拷贝链表的next指针链,同时建立原节点→新节点的映射
        while (pur != nullptr) {
            // 1. 为当前原节点创建值相同的新节点
            Node* node = new Node(pur->val);

            // 2. 构建拷贝链表的next关系(保持与原链表顺序一致)
            if (phead != nullptr) { // 拷贝链表已存在节点,尾插新节点
                tail->next = node;  // 尾节点的next指向新节点
                tail = node;        // 尾指针后移到新节点
            } else { // 拷贝链表为空,新节点作为头节点
                phead = node;       // 头指针指向第一个新节点
                tail = node;        // 尾指针也指向第一个新节点
            }

            // 3. 记录原节点与新节点的映射关系,供后续处理random指针
            hash.insert({pur, node});

            // 原链表指针后移,继续处理下一个节点
            pur = pur->next;
        }

        // 第二步:拷贝链表的random指针(依赖第一步建立的哈希映射)
        pur = head; // 重置原链表遍历指针,重新从头开始遍历
        while (pur != nullptr) {
            // 核心逻辑:新节点的random = 原节点random对应的新节点
            // - 若原节点random为nullptr,hash[nullptr]返回nullptr(符合要求)
            // - 若原节点random指向某原节点,hash会找到对应的新节点
            hash[pur]->random = hash[pur->random];

            // 原链表指针后移,继续处理下一个节点
            pur = pur->next;
        }

        // 返回拷贝链表的头指针
        return phead;
    }
};

题目2:前k个高频单词

思路分析:

也可以用仿函数控制稳定性。

也可以用 top-k,找出最大的前 k 个 string。注意:仿函数的升序提供大于比较,降序提供小余比较,但是堆的逻辑是反过来的。

  • stable_sort
cpp 复制代码
class Solution {
public:
    // 定义别名:PSI = 字符串(单词)+ 整数(频次)的键值对
    using PSI = pair<string, int>;

    // 自定义比较器:用于排序时的规则
    struct cmp {
        // 排序规则:
        // 1. 频次高的排在前面;
        // 2. 若频次相同,字典序小的排在前面
        bool operator()(const PSI& a, const PSI& b) {
            // 先比较频次:a的频次 > b的频次 → a排在b前面
            if (a.second != b.second) {
                return a.second > b.second;
            }
            // 频次相同时,比较字典序:a的字符串 < b的字符串 → a排在b前面
            else {
                return a.first < b.first;
            }
        }
    };

    vector<string> topKFrequent(vector<string>& words, int k) 
    {
        // 1. 统计每个单词的出现频次:key=单词,value=出现次数
        map<string, int> countmap;
        for (auto& x : words) {
            countmap[x]++; // 遍历单词列表,累加每个单词的频次
        }

        // 2. 将频次统计结果(map)转存为vector(便于排序)
        vector<PSI> v(countmap.begin(), countmap.end());

        // 3. 按自定义规则排序:频次降序,频次相同则字典序升序
        stable_sort(v.begin(), v.end(), cmp());
        // 注:此处用sort也可以,因为cmp规则已覆盖所有情况,stable_sort是稳定排序(本题无必要,仅为兼容)

        // 4. 提取前k个高频单词,存入结果数组
        vector<string> ret;
        for (int i = 0; i < k; i++) {
            ret.push_back(v[i].first); // 取排序后前k个元素的"单词"部分
        }

        // 返回最终结果
        return ret;
    }
};
  • 仿函数控制稳定
cpp 复制代码
class Solution {
public:
    // 定义别名 PSI:存储 <单词, 出现频次> 的键值对
    // 简化代码写法,避免重复写 pair<string, int>,提升可读性
    using PSI = pair<string, int>;

    // 自定义比较器结构体:用于sort排序时的规则定义
    // 核心需求:1. 频次高的单词排前面;2. 频次相同时,字典序小的单词排前面
    struct cmp {
        // 重载()运算符,使cmp成为可调用对象(满足sort的比较函数要求)
        // 参数:两个PSI类型的常量引用(避免拷贝,提高效率)
        bool operator()(const PSI& a, const PSI& b) {
            // 排序规则:
            // 1. 若a的频次 > b的频次 → a排在b前面(频次降序)
            // 2. 若频次相等 → 比较单词字典序,a的字典序小则a排在b前面(字典序升序)
            return a.second > b.second || (a.second == b.second && a.first < b.first);
        }
    };

    // 主函数:输入单词列表和k,返回前k个高频单词(按规则排序)
    vector<string> topKFrequent(vector<string>& words, int k) 
    {
        // 1. 统计每个单词的出现频次:key=单词(string),value=出现次数(int)
        // map默认按key(单词字典序)排序,但此处仅用于统计,最终排序靠自定义规则
        map<string, int> countmap;
        for (auto& x : words) { // 遍历单词列表
            countmap[x]++; // 单词x的频次+1(map不存在该key时会自动初始化value为0)
        }

        // 2. 将map中的<单词,频次>键值对转存到vector中
        // 原因:map是关联容器,不支持直接按自定义规则排序,vector支持sort算法
        vector<PSI> v(countmap.begin(), countmap.end());

        // 3. 按自定义比较器cmp排序vector
        // 排序后:vector前端是高频单词,频次相同则字典序小的在前
        sort(v.begin(), v.end(), cmp());

        // 4. 提取排序后前k个单词,存入结果数组
        vector<string> ret;
        for (int i = 0; i < k; i++) {
            ret.push_back(v[i].first); // 取第i个元素的"单词"部分(first是pair的第一个元素)
        }

        // 返回最终结果
        return ret;
    }
};
  • 堆top-k
cpp 复制代码
class Solution {
public:
    // 定义别名 PSI:存储 <单词, 出现频次> 的键值对
    // 简化代码写法,避免重复书写 pair<string, int>,提升可读性
    using PSI = pair<string, int>;

    // 自定义比较器结构体:用于优先队列(堆)的排序规则定义
    // 核心目的:构建「小顶堆」,堆顶始终是当前堆中"最该被淘汰"的元素
    struct cmp {
        // 重载()运算符,使cmp成为可调用对象(满足优先队列的比较要求)
        // 参数:两个PSI类型的常量引用(避免拷贝,提高效率)
        bool operator()(const PSI& a, const PSI& b) {
            // 堆的排序规则(优先队列是"大的元素下沉,小的元素上浮",cmp返回true时a会被放到堆底):
            // 1. 频次低的元素优先级更高(先被弹出):a频次 < b频次 → a排在堆顶附近(小顶堆核心)
            // 2. 若频次相同:字典序大的元素优先级更高(先被弹出):a字典序 > b字典序 → a排在堆顶附近
            // 最终堆中保留的是「前k个高频且字典序小」的元素,堆顶是这k个中"最弱势"的(便于淘汰)
            return a.second < b.second || (a.second == b.second && a.first > b.first);
        }
    };

    // 主函数:输入单词列表和k,返回前k个高频单词(规则:频次降序,频次相同则字典序升序)
    vector<string> topKFrequent(vector<string>& words, int k) 
    {
        // 结果数组:提前初始化大小为k,避免动态扩容,效率更高
        vector<string> ret(k);

        // 1. 统计每个单词的出现频次:key=单词(string),value=出现次数(int)
        // 选择unordered_map:哈希表实现,插入和查询的时间复杂度O(1),比map(红黑树O(logn))更高效
        unordered_map<string, int> hash;
        for (auto& x : words) { // 遍历单词列表
            hash[x]++; // 单词x的频次+1(unordered_map不存在该key时自动初始化value为0)
        }

        // 2. 构建优先队列(小顶堆):存储<单词,频次>,排序规则由cmp定义
        // 直接用hash的迭代器初始化堆,堆会自动按cmp规则调整结构
        priority_queue<PSI, vector<PSI>, cmp> heap(hash.begin(), hash.end());

        // 3. 提取堆顶元素(共k次):堆顶是当前堆中"频次最高/字典序最小"的元素
        // 因为堆已按规则维护,前k次弹出的就是最终需要的结果
        for (int i = 0; i < k; i++) {
            ret[i] = heap.top().first; // 取堆顶元素的"单词"部分(pair的first成员)存入结果
            heap.pop(); // 弹出堆顶元素,下一个"最优"元素自动上浮到堆顶
        }

        // 返回最终结果:结果数组已按「频次降序、频次相同字典序升序」排列
        return ret;
    }
};

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

pair 是一个类模板。主要包含 first 和 second 两个成员。可以理解为 pair 就是个结构体,所以就可以理解为之前是分散存放 key 和 value,现在放在一个结构体里面。

pair 的使用写起来就比较方便:

cpp 复制代码
#include<iostream>
#include<map>
using namespace std;
int main()
{
	// insert插⼊pair对象的4种⽅式,对⽐之下,最后⼀种最⽅便
	map<string, string> dict;
	pair<string, string> kv1("first", "第一个");
	dict.insert(kv1);
	dict.insert(pair<string, string>("second", "第二个"));
	dict.insert(make_pair("sort", "排序"));
	dict.insert({ "auto", "自动的" });
	// "left"已经存在,插⼊失败
	dict.insert({ "left", "左边,剩余" });
	while (it != dict.end())
	{
		//cout << (*it).first <<":"<<(*it).second << endl;
		// map的迭代基本都使⽤operator->,这⾥省略了⼀个->
		// 第⼀个->是迭代器运算符重载,返回pair*,第⼆个箭头是结构指针解引⽤取pair数据
		//cout << it.operator->()->first << ":" << it.operator->()-> second << endl;
	    cout << it->first << ":" << it->second << endl;
	    ++it;
	}
	cout << endl;
	return 0;
}

同时这里遍历的时候,我们不能这样写,因为pair 不支持流插入和流提取。

所以要显示的写出 first 和 second,本质是通过运算符重载找到 pair*,在通过pair* 访问 first 和 second,但是为了好看就省略了一个 ->。

8、multimap

multimap 的使用方式与 map 基本一致,唯一的区别就在于 multimap 允许键值冗余,即可存储重复元素。

cpp 复制代码
void Test9()
{
	//允许键值冗余
	multimap<int, string> m;
	m.insert(make_pair(1, "one"));
	m.insert(make_pair(1, "1"));
	m.insert(make_pair(2, "two"));
	m.insert(make_pair(2, "2"));
	m.insert(make_pair(3, "three"));
	for (auto e : m)
	{
		cout << "<" << e.first << "," << e.second << ">" << " ";
	}
	cout << endl; 
}

值得注意的是:multimap 的 find 返回底层搜索树中序的第一个值为 key 的元素的迭代器,而 map 返回的是 key 元素的迭代器。并且由于multimap 支持键值冗余,所以其成员函数没有 [ ] 运算符重载,因为一旦键值冗余,根本不知道该返回哪个键值的value 。

相关推荐
m0_692457103 小时前
C++面向过程编程
c++·面向过程编程
waves浪游3 小时前
进程控制(中)
linux·运维·服务器·开发语言·c++
0 0 03 小时前
CCF-CSP 36-3 缓存模拟(cache)【C++】
开发语言·c++·算法
满天星83035774 小时前
【Linux】信号(上)
linux·运维·服务器·开发语言·c++
李日灐4 小时前
C++STL: list(双链表) 简单介绍,了解迭代器类型,list sort 的弊端
开发语言·c++·list
打不了嗝 ᥬ᭄4 小时前
【Linux】多路转接 Select , Poll和Epoll
linux·网络·c++·网络协议·http
啊森要自信4 小时前
【C++的奇迹之旅】map与set应用
c语言·开发语言·c++
pu_taoc4 小时前
ffmpeg实战4-将PCM与YUV封装成MP4
c++·ffmpeg·pcm
2301_803554525 小时前
Pimpl(Pointer to Implementation)设计模式详解
c++·算法·设计模式