C++ STL 关联式容器万字详解:set、multiset、map、multimap|接口用法 + 易错点 + 力扣真题实战

目录

引言:

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

2.set系列的使用

2.1.set和multiset参考文档

2.2.set类的介绍

2.3.set的构造

2.4.set的迭代器

2.5.set的增删查

2.6.insert和迭代器遍历使用样例

2.7.find和erase使用样例

2.8.multiset和set的差异

2.9.set系列练习题

[2.9.1.349. 两个数组的交集 - 力扣(LeetCode)](#2.9.1.349. 两个数组的交集 - 力扣(LeetCode))

[2.9.2.142. 环形链表 II - 力扣(LeetCode)](#2.9.2.142. 环形链表 II - 力扣(LeetCode))

2.10.set和multiset总结

set

multiset

3.map系列的使用

3.1.map和multimap参考文档

3.2.map类的介绍

3.3.pair类型介绍

3.4.map的构造和迭代器

3.5.map的增删查

3.6.构造遍历以及增删查使用样例

3.7.map的数据修改(重点)

[3.8.map的迭代器和 \[\] 功能样例](#3.8.map的迭代器和 [] 功能样例)

3.9.multimap和map的差异

3.10.map系列练习题

[3.10.1.138. 随机链表的复制 - 力扣(LeetCode)](#3.10.1.138. 随机链表的复制 - 力扣(LeetCode))

[3.10.2.692. 前K个高频单词 - 力扣(LeetCode)](#3.10.2.692. 前K个高频单词 - 力扣(LeetCode))

结语:


引言:

STL 容器分为序列式容器与关联式容器两大类。

前文已经学习过 vector、list 等序列式容器,本篇我们聚焦set/map这一组关联式容器,它们底层基于红黑树,也就是平衡二叉搜索树,依靠 key 完成存储与查找,增删查效率稳定 O (logN)。

如果你阅读过我之前二叉搜索树的文章C++ 高阶数据结构:二叉搜索树(BST)万字详解 | 原理、增删查、key 与 key‑value 双版本实现、看懂 set/map 底层-CSDN博客理解本篇会事半功倍。本文会结合核心接口、易错点以及力扣真题实战,带大家吃透 set、multiset、map、multimap 的使用,同时理清容器的性能取舍。

那么话不多说,直接进入正文------------>


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

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

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

本章节讲解的 map 和 set 底层是红黑树,红黑树是一颗平衡二叉搜索树(左右均衡程度接近完全二叉树)。set 是 key 搜索场景的结构,map 是 key/value 搜索场景的结构。

看过我上篇文章的话,这边的概念是很通俗易懂的

C++ 高阶数据结构:二叉搜索树(BST)万字详解 | 原理、增删查、key 与 key‑value 双版本实现、看懂 set/map 底层-CSDN博客


2.set系列的使用

2.1.set和multiset参考文档

<set> - C++ Reference

set不允许冗余(没有重复数据),multiiset允许冗余(可以有重复数据)

2.2.set类的介绍

  • set 的声明如下,T 就是 set 底层关键字的类型
  • set 默认要求 T 支持小于比较,如果不支持或者想按自己的需求走可以自行实现仿函数传给第二个模版参数
  • set 底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给第三个参数。
  • 一般情况下,我们都不需要传后两个模版参数。
  • set 底层是用红黑树实现,增删查效率是O(logN),迭代器遍历是走的搜索树的中序,所以是有序的。
  • 前面部分我们已经学习了 vector/list 等容器的使用,STL 容器接口设计,高度相似,所以这里我们就不再一个接口一个接口的介绍,而是直接带着大家看文档,挑比较重要的接口进行介绍。
cpp 复制代码
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;

这结构我们在二叉搜索树那演示过,key搜索版本,只是他这底层是红黑树

2.3.set的构造

这里我们需要关注的接口其实就是我大写的那几个,即,这里就不再多说了,这和前面几个容器没什么区别

cpp 复制代码
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& = 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());

2.4.set的迭代器

set的迭代器是一个双向迭代器,如下图

这里有个小技巧,在明显知道这个容器不是随机迭代器的情况下,只有看库函数有没有rbegin和rend就可以了,因为反向迭代器是正向迭代器封装的,有的话就是双向迭代器,没有的话就是单向迭代器

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

2.5.set的增删查

set的增删查我们只需要关注下面这些接口就可以了

cpp 复制代码
Member types
key_type -> The first template parameter (T)
value_type -> The first template parameter (T)

// 单个数据插⼊,如果已经存在则插⼊失败 
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() 
iterator find (const value_type& val);
// 查找val,返回Val的个数 
size_type count (const value_type& val) const;
// 删除⼀个迭代器位置的值 
iterator erase (const_iterator position);
// 删除val,val不存在返回0,存在返回1 
size_type erase (const value_type& val);
// 删除⼀段迭代器区间的值 
iterator erase (const_iterator first, const_iterator last);
// 返回⼤于等于val位置的迭代器 
iterator lower_bound (const value_type& val) const;
// 返回⼤于val位置的迭代器 
iterator upper_bound (const value_type& val) const;

2.6.insert和迭代器遍历使用样例

我们先看一个用上上面所有知识的样例,随后我们进行拓展

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())
	{
		// 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⽐较ascll码⼤⼩顺序遍历的 
	for (auto& e : strset)
	{
		cout << e << " ";
	}
	cout << endl;
}

思考一只传一个比较器怎么实现插入和删除?

我们在上篇实现二叉搜索树的时候,不支持冗余的情况下,对于插入时的判断,我们有三种,一种是大于的情况,一种是小于的情况,还有一种等于的情况

这其实就是仿函数的妙用了,我们可以这么用,比如如下

cpp 复制代码
...
if(cmp(key,cur->_key))
...
else if(cmp(cur->_key,key))
...
else
...

set的底层用的也是这个思路

2.7.find和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);
	if (num == 0)
	{
		cout << x << "不存在!" << endl;
	}
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;
	// 直接查找在利⽤迭代器删除x 
	cin >> 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;
}

lower_bound和upper_bound应用实例

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 40 50 60 70 80 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.8.multiset和set的差异

multiset 和 set 的使用基本完全类似(也是包含在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;
}

2.9.set系列练习题

2.9.1.349. 两个数组的交集 - 力扣(LeetCode)

AC码

cpp 复制代码
class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        set<int> st(nums1.begin(),nums1.end());
        vector<int> ans;
        for(auto x : nums2)
        {
            if(st.count(x))
            {    
                ans.push_back(x);
                st.erase(x);
            }
        }
        return ans;
    }
};

2.9.2.142. 环形链表 II - 力扣(LeetCode)

AC码(C++)

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

我们也可以通过快慢指针来解决这个问题(一个走一步,一个走俩步),但需要证明同时出发会在环的入口相遇,理解证明会比较麻烦。但是空间复杂度是优于用set的,感兴趣的可以自己了解下,这里直接放AC码(那时候用C语言实现的,用C++的话会很便捷)

AC码(C语言)

cpp 复制代码
typedef struct ListNode LN;

struct ListNode *detectCycle(struct ListNode *head) {
    LN*fast = head;
    LN*slow = head;
    while(fast&&fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        
        if(slow==fast)
        {
            while(head!=fast)
            {
                head = head->next;
                fast = fast->next;
            }
            return fast;
        }
    }
    return NULL;
}

注意:set的方式在思维上是降维打击,但在算法性能上是降级,set方式的时间复杂度是O(NlogN),空间复杂度是O(N),但是双指针的方式时间复杂度是O(N),空间复杂度是O(1)

2.10.set和multiset总结

这里主要总结下set和mutiset在接口上的差异点

set

insert接口------支持直接插入数据返回一个pair,内存迭代器和bool,迭代器存插入位置,bool存插入是否成功、支持列表插入、支持迭代器区间插入

find接口------查val,返回val所在迭代器

count接口------查val,返回val个数

erase接口------支持删除迭代器指向的数据、删除一个数据val(不存在返回0,存在返回1),删除一段迭代器区间的数据

multiset

insert接口------支持直接插入数据返回迭代器存插入位置、支持列表插入、支持迭代器区间插入

find接口------查val,返回中序遍历第一个val所在迭代器

count接口------查val,返回val个数

erase接口------支持删除迭代器指向的数据、删除一个数据val(会把multiset中所有val全删除,返回删除个数),删除一段迭代器区间的数据


3.map系列的使用

有了set的基础,map学起来会更快

3.1.map和multimap参考文档

<map> - C++ Reference

set不允许冗余(没有重复key),multiiset允许冗余(可以有重复key)

3.2.map类的介绍

  • map 的声明如下,Key 就是 map 底层关键字的类型,T 是 map 底层value的类型

  • map 默认要求 Key 支持小于比较,如果不支持或者想按自己的需求走可以自行实现仿函数传给第三个模版参数

  • map 底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给第四个参数。

  • 一般情况下,我们都不需要传后两个模版参数。

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

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

  • map内部成员指向的结构体的内部成员其实是pair类型的

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

    key加const修饰是为了预防误修改破坏树的结构

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_value搜索版本,只是他这底层是红黑树

map的大框架跟set其实是差不多的,唯一多出来的就是map是支持改,所以多了\[\]运算符重载和at

3.3.pair类型介绍

map和set的insert都会返回一个pair的东西,这里我来详细讲讲pair

**pair其实就是个类模板,**他里面有俩个成员first和second,first就是T1类型,second就是T2类型

我这里把pair的底层扒出来了,我们来研究下,最上面一行是传过去的类型(set的key_type和value_type都是T类型(key),map的key_type是Key类型,value_type是pair类型),这也能看出map内部的成员指向的结构体的内部成员其实是pair类型的

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

T1就是const Key,T2就是T,然后first就是const Key类型,second就是T类型,其实这就相当于是------我们先前在二叉搜索树部分实现的时候是给了俩个成员变量,他这里将俩个成员变量封装到了一个结构体里

就这么简单,然后make_pair就相当于是通过调用这个函数自己构造一个对应的pair对象返回

pair是没有重载流插入流提取的

3.4.map的构造和迭代器

上面的set懂了这里就是一眼过,因为和set十分类似

map 的构造接口和 STL 其他容器风格保持一致,我们重点掌握常用的几个构造接口即可。

map 支持正向迭代遍历与反向迭代遍历。因为底层是红黑树,迭代器执行的是二叉搜索树的中序遍历,默认按照 key 的升序进行输出。 只要支持迭代器,map 就支持范围 for 语法。map 允许修改 value 数据,但是不允许修改 key 数据;如果修改 key,会直接破坏底层搜索树的有序结构。

cpp 复制代码
// 1.无参默认构造
explicit map (const key_compare& comp = key_compare(),
 const allocator_type& alloc = allocator_type());

// 2.迭代器区间构造
template <class InputIterator>
 map (InputIterator first, InputIterator last,
 const key_compare& comp = key_compare(),
 const allocator_type& = allocator_type());

// 3.拷贝构造
map (const map& x);

// 4.initializer_list初始化列表构造
map (initializer_list<value_type> il,
 const key_compare& comp = key_compare(),
 const allocator_type& alloc = allocator_type());

map 的迭代器属于双向迭代器。 迭代器指向的是节点的结构体,然后因为 * 重载,在这个函数的操作下,返回的是节点的结构体内部的 pair 成员

cpp 复制代码
//正向迭代器
iterator begin();
iterator end();

//反向迭代器
reverse_iterator rbegin();
reverse_iterator rend();

3.5.map的增删查

map 的增删查我们重点掌握下面这几组接口即可。

map 插入的是 pair 键值对,这一点和 set 有明显区别;但是查找、删除接口只需要传入关键字 key,接口用法和 set 非常相似。 find接口返回迭代器,除了可以判断 key 是否存在以外,还可以拿到 key 对应的 value,并且可以通过迭代器修改 value(key 不允许修改)。

相关成员类型说明:

  • key_type:map 第一个模板参数 Key,关键字的类型
  • mapped_type:map 第二个模板参数 T,映射值的类型
  • value_typepair<const key_type,mapped_type>,map 容器存储的元素类型
cpp 复制代码
// 单个数据插入,key已经存在则插入失败;即便key相同、value不同,同样插入失败
pair<iterator,bool> insert (const value_type& val);

// initializer_list列表插入,容器中已存在的key不会重复插入
void insert (initializer_list<value_type> il);

// 迭代器区间插入,容器中已存在的key不会重复插入
template <class InputIterator>
void insert (InputIterator first, InputIterator last);

// 根据key查找,找到返回对应迭代器,找不到返回end()
iterator find (const key_type& k);

// 统计key的个数,map中结果只能是0或者1
size_type count (const key_type& k) const;

// 删除迭代器指向位置的元素
iterator erase (const_iterator position);

// 根据key删除元素,不存在返回0,存在返回1
size_type erase (const key_type& k);

// 删除迭代器区间 [first, last) 内的所有元素
iterator erase (const_iterator first, const_iterator last);

// 返回第一个大于等于k的元素的迭代器
iterator lower_bound (const key_type& k);
const_iterator lower_bound (const key_type& k) const;

//返回k的迭代器区间(左闭右开)pair------主要用于multi版本
pair<const_iterator,const_iterator> equal_range (const key_type& k) const;
pair<iterator,iterator>             equal_range (const key_type& k);

补充说明:

  1. map 不允许 key 重复,insert 插入重复 key 直接失败,bool 返回 false,迭代器指向原有 key 的位置(说明insert除了充当插入的功能,还充当了查找的功能------为了给\[\]使用做铺垫)
  2. find 拿到迭代器后,可以修改it->second(value),禁止修改it->first(key);
  3. count 对于 map 只有 0 和 1 两种返回值,更多用来判断 key 是否存在;multimap 才会返回大于 1 的数字。

3.6.构造遍历以及增删查使用样例

需要注意的是,迭代器指向的是节点的地址,节点的结构体内部存着pair成员,*和->重载内部是对这个paiir成员的操作

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


int main()
{
	// initializer_list构造及迭代遍历 
	map<string, string> dict = { {"left", "左边"}, {"right", "右边"},
   {"insert", "插入"},{ "string", "字符串" } };

	//map<string, string>::iterator it = dict.begin();
	auto it = dict.begin();
	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;
	// insert插入pair对象的4种方式,对比之下,最后一种最方便 
	pair<string, string> kv1("first", "第一个");
	dict.insert(kv1);
	dict.insert(pair<string, string>("second", "第二个"));
	dict.insert(make_pair("sort", "排序"));//调用make_pair库函数
	dict.insert({ "auto", "自动的" });//C++11新增的,多参数隐式类型转换,C++98支持的是单参数隐式类型转换
	// "left"已经存在,插入失败 
	dict.insert({ "left", "左边,剩余" });

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

	string str;
	while (cin >> str)
	{
		auto ret = dict.find(str);
		if (ret != dict.end())
		{
			cout << "->" << ret->second << endl;
		}
		else
		{
			cout << "无此单词,请重新输入" << endl;
		}
	}

	// erase等接口跟set完全类似,这里就不演示讲解了 
	return 0;
}

我们现在用的最多的insert方式就是这样

3.7.map的数据修改(重点)

前面提到过 map 支持修改value(映射值)的数据,但是不支持修改 key 数据。一旦修改 key,就会破坏底层红黑搜索树的有序结构。

map 第一种修改 value 的方式是借助迭代器:遍历得到迭代器,或者通过 find 拿到 key 对应的迭代器,就可以修改对应的映射值。

map 还有一个非常重要的接口**operator[],它不只是用来做修改,同时兼具插入、查找、修改**多重能力,是一个复合功能接口。

注意区分两个类型概念:

map 模板第二个参数 T 被 typedef 为mapped_type,也就是我们日常说的映射 value;

value_type是红黑树节点内部存储的完整键值对类型:pair<const key_type, mapped_type>。 平时开发习惯依旧把 T 这个映射值叫做 value。

成员类型回顾:

  • key_type:map 第一个模板参数 Key,关键字类型
  • mapped_type:map 第二个模板参数 T,映射值类型
  • value_typepair<const key_type,mapped_type>,容器节点存储的元素类型
cpp 复制代码
// 根据key查找,找到返回对应迭代器,找不到返回end();找到后可通过迭代器修改mapped_type映射值
iterator find (const key_type& k);

// 插入单个键值对,返回pair<iterator,bool>
pair<iterator,bool> insert (const value_type& val);

// []运算符重载
mapped_type& operator[] (const key_type& k);

对 insert 返回值说明:

单个元素版本返回一个 pair 对象。 pair::first是迭代器,指向新插入的元素,或者 map 中已经存在的等价 key 的元素; pair::second为 true 代表成功插入新元素;false 代表 key 已存在,插入失败。

拆解 insert 的返回:

  1. 如果 map 中已经存在该 key:插入失败。返回pair<iterator,bool>first指向原有 key 节点的迭代器secondfalse
  2. 如果 map 中不存在该 key:插入成功。返回pair<iterator,bool>first指向新插入节点的迭代器secondtrue

无论插入成功还是失败,返回 pair 的 first 永远指向这个 key 对应的节点迭代器 。 正是这个特性,让 insert 可以实现operator[]的底层逻辑。

map中的俩种pair

  • 红黑树节点内部存储的数据:pair<const key_type, mapped_type>(容器的 value_type)
  • insert 函数返回值:pair<iterator, bool>,用于反馈插入结果

operator\[\]底层调用其实就是如下图

这样看太难懂了,我把他拆解成了函数段

cpp 复制代码
mapped_type& operator[] (const key_type& k)
{
    // k不存在:insert插入k+mapped_type默认构造对象,返回新节点迭代器,返回value引用,支持后续修改
    // k存在:insert插入失败,但拿到原有key节点迭代器,返回value引用,完成查找修改
    pair<iterator, bool> ret = insert({ k, mapped_type() });
    iterator it = ret.first;
    return it->second;
}

总结:

operator\[\]有俩种场景:

  • key 不存在:执行插入,以 key+value 默认值创建节点,返回 value 的引用,支持后续修改,实现【插入 + 修改】。
  • key 已存在:insert 插入失败,拿到原有节点迭代器,返回已有 value 的引用,实现【查找 + 修改】

operator[]不能用来做单纯查找,key 不存在时它会偷偷插入一条新数据,会改变容器内容,单纯查找优先用 find。

3.8.map的迭代器和 \[\] 功能样例

cpp 复制代码
#include<iostream>
#include<map>
#include<string>
using namespace std;
int main()
{
	// 利用find和iterator修改功能,统计水果出现的次数 
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
   "苹果", "香蕉", "苹果", "香蕉" };
	map<string, int> countMap;
	for (const auto& str : arr)
	{
		// 先查找水果在不在map中 
		// 1、不在,说明水果第一次出现,则插入{水果, 1} 
		// 2、在,则查找到的节点中水果对应的次数++ 
		auto ret = countMap.find(str);
		if (ret == countMap.end())
		{
			countMap.insert({ str, 1 });
		}
		else
		{
			ret->second++;
		}
	}
	for (const auto& e : countMap)
	{
		cout << e.first << ":" << e.second << endl;
	}
	cout << endl;
	return 0;
}
cpp 复制代码
#include<iostream>
#include<map>
#include<string>
using namespace std;
int main()
{
	// 利用[]插入+修改功能,巧妙实现统计水果出现的次数 
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
   "苹果", "香蕉", "苹果", "香蕉" };
	map<string, int> countMap;
	for (const auto& str : arr)
	{
		// []先查找水果在不在map中 
		// 1、不在,说明水果第一次出现,则插入{水果, 0},同时返回次数的引用,++一下就变成1次了
			// 2、在,则返回水果对应的次数++ 
		countMap[str]++;
	}
	for (const auto& e : countMap)
	{
		cout << e.first << ":" << e.second << endl;
	}
	cout << endl;
	return 0;
}
cpp 复制代码
#include<iostream>
#include<map>
#include<string>
using namespace std;
int main()
{
	map<string, string> dict;
	dict.insert(make_pair("sort", "排序"));
	// key不存在->插入 {"insert", string()} 
	dict["insert"];
	// 插入+修改 
	dict["left"] = "左边";
	// 修改 
	dict["left"] = "左边、剩余";
	// key存在->查找 
	cout << dict["left"] << endl;
	return 0;
}

3.9.multimap和map的差异

multimap 和 map 的用法基本完全类似,核心区别在于 multimap 允许 key 冗余,也就是可以存在多个相同的 key

insertfindcounterase这些接口行为,都会因为支持 key 重复而发生变化,这一点和 set 与 multiset 的差异逻辑完全一致。

  • find:容器中存在多个相同 key 时,返回中序遍历遇到的第一个 key 对应的迭代器
  • count:map 中 count 只能返回 0 或 1;multimap 的 count 会返回该 key 的总个数。
  • erase(key):multimap 调用该重载,会把容器中所有该 key 的元素全部删除

重点:multimap 不支持 operator \[\]

原因:multimap 允许 key 重复,同一个 key 可以对应多条记录,operator[]无法确定到底操作哪一个键值对,所以直接把 \[\] 接口删掉,既不能用 \[\] 插入,也不能用 \[\] 修改。
小结对比记忆:

  1. map:key 唯一,支持operator[]
  2. multimap:key 可重复,没有 operator \[\];
  3. 底层同样是红黑树,增删查效率O(logN),迭代器遍历依旧中序有序。

在multimap用equal_range接口就很合适了,可以找出连续的一段区间了

3.10.map系列练习题

3.10.1.138. 随机链表的复制 - 力扣(LeetCode)

用map可以很快解决这道题,只需要开一个key是Node*,value也是Node*的map就可以了,然后key就是原链表的那些节点,全都拷过来,然后再通过映射把value连接起来就可以了

AC码(时间复杂度O(NlogN),空间复杂度O(N))

cpp 复制代码
/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* next;
    Node* random;
    
    Node(int _val) {
        val = _val;
        next = NULL;
        random = NULL;
    }
};
*/

class Solution {
public:
    Node* copyRandomList(Node* head) {
        if(head==nullptr)
            return nullptr;
        map<Node*,Node*> mp;
        Node* cur = head;
        while(cur)
        {
            Node* p = new Node(cur->val);
            mp[cur] = p;
            cur = cur->next;
        }
        cur = head;
        while(cur)
        {
            if(cur->next==nullptr)
                mp[cur]->next = nullptr;
            else
                mp[cur]->next = mp[cur->next];
            if(cur->random==nullptr)
                mp[cur]->random = nullptr;
            else
                mp[cur]->random = mp[cur->random];
            cur = cur->next;
        }
        return mp[head];
    }
};

虽然map很便捷,但是我们用这种容器实现起来快捷的弊端就是时空复杂度都比较高,我们自己实现的话就时空复杂度都可以优化很多,这题不用map的话可以先把一个个节点构建起来,然后接连在原先节点的后面,val就是前一个节点的val,随后通过原链表的random关系,更新接进去的random关系,更新完后切割掉原链表,把 接进去的节点一个个连成新链表即可

AC码(时间复杂度O(N),空间复杂度O(1))

cpp 复制代码
/*
// Definition for a Node.
class Node {
public:
    int val;
    Node* next;
    Node* random;
    
    Node(int _val) {
        val = _val;
        next = NULL;
        random = NULL;
    }
};
*/

class Solution {
public:
    Node* copyRandomList(Node* head) {
        if(head==nullptr)
            return nullptr;
        Node* cur = head;

        while(cur)
        {
            Node* p = new Node(cur->val);
            p->next = cur->next;
            cur->next = p;
            cur = p->next;
        }
        cur = head;
        while(cur)
        {
            if(cur->random)
                cur->next->random = cur->random->next;
            else
                cur->next->random = nullptr;
            cur = cur->next->next;
        }
        cur = head;
        Node* ptr = cur->next;
        while(cur)
        {
            if(cur->next->next==nullptr)
            {
                cur->next = nullptr;
                break;
            }
            Node* p = cur->next->next;
            cur->next->next = cur->next->next->next;
            cur->next = p;
            cur = cur->next;
        }

        return ptr;
    }
};

3.10.2.692. 前K个高频单词 - 力扣(LeetCode)

这题有俩种方式,一种是用map后将数据放入vector排序,另一种是用map后将数据放入堆进行top-K

AC码(map+排序)------时间复杂度O(NlogN),空间复杂度O(N)

cpp 复制代码
class Solution {
public:
    struct Greater
    {
        bool operator()(const pair<string,int>& x,const pair<string,int>& y) const
        {
            if(x.second==y.second)
                return x.first<y.first;
            return x.second > y.second;
        }
    };

    vector<string> topKFrequent(vector<string>& words, int k) {
        map<string,int>mp;
        for(auto& s:words)
            mp[s]++;
        vector<pair<string,int>> v(mp.begin(),mp.end());
        sort(v.begin(),v.end(),Greater());
        vector<string> ans;
        for(int i = 0;i<k;i++)
        {
            ans.push_back(v[i].first);
        }
        return ans;
    }

};

AC码(map+top-K)------时间复杂度O(NlogN) 空间复杂度O(N)

cpp 复制代码
class Solution {
public:
    struct Greater
    {
    public:
        bool operator()(const pair<string,int>& x,const pair<string,int>& y)
        {
            if(x.second==y.second)
                return x.first<y.first;
            return x.second>y.second;
        }
    };

    vector<string> topKFrequent(vector<string>& words, int k) {
        map<string,int>mp;
        for(auto& s:words)
            mp[s]++;
        priority_queue<pair<string,int>,vector<pair<string,int>>,Greater> q;
        for(auto& it : mp)
        {
            if(q.size()==k)
            {
                auto& lin = q.top();
                if(it.second>lin.second)
                {    
                    q.pop();
                    q.push(it);
                }
            }
            else
            {
                q.push(it);
            }
        }
        vector<string> ans;
        while(q.size())
        {
            ans.push_back(q.top().first);
            q.pop();
        }
        reverse(ans.begin(),ans.end());
        return ans;
    }

};

结语:

那么,C++set和map使用部分的内容就全部讲解完毕啦,希望以上内容对你有所帮助,感谢观看,若觉得写的还可以,可以分享给朋友一起来看哦,毕竟一起进步更有动力嘛,当然能关注一下就更好啦。

相关推荐
c238561 小时前
C/C++每日一练21
c语言·开发语言·c++
qq_401700411 小时前
Qt Modbus协议0x00
开发语言·qt
星轨初途1 小时前
LeetCode 热题 100——day7 接雨水
数据结构·c++·笔记·算法·leetcode·职场和发展
yushikong1 小时前
关于rust开发ch32x033f8p6的一些记录
开发语言·后端·rust
qeen871 小时前
【数据结构】搜索二叉树的介绍与算法原理解析及实现
数据结构·c++·学习·算法·模板
吴声子夜歌1 小时前
正则表达式——环视
开发语言·正则表达式
Zenova EdgeOS2 小时前
C++ 工业边缘 Qt 桌面应用实战:从 QWidget 到模型视图、网络与多线程
网络·c++·qt
福大大架构师每日一题2 小时前
agno v2.8.7 发布:顾问模型、精准路线、调度能力全面升级,10项关键修复一次看懂
java·开发语言·数据库
老洋葱Mr_Onion2 小时前
【C++】CSP-J复赛dp问题(含模板题)
开发语言·c++