C++ | map&set的使用

🦌云深麋鹿
专栏C++ | 用C语言学数据结构 | Java

回顾:上一篇我们结束了 二叉搜索树,接下来这篇文章让我们进入到 map&set 的使用学习,体会新的设计思路吧~

放个目录

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

1.1 介绍

1.1.1 序列式容器

  1. 逻辑结构为线性。
  2. 两个存储位置之间没有紧密的关联关系。

1.1.2 关联式容器

  1. 逻辑结构为非线性。
  2. 两个存储位置之间有紧密的关联关系。

1.2 给各容器分类

  • 序列式容器:string、vector、list、deque、array、forward_list。
  • 关联式容器:map/set系列和unordered_map/unordered_set系列。

1.3 前言

  • 我们接下来介绍的map/set底层是红黑树,红黑树是⼀颗平衡二叉搜索树。
  • set是key搜索场景的结构,map是key/value搜索场景的结构。

二 set系列的使用

该容器适用场景:去重+排序。

  1. T就是set底层关键字的类型。
  2. Compare(一般保持默认即可):默认要求T⽀持小于比较。
  3. Alloc(一般保持默认即可):底层存储数据的内存是从空间配置器申请的。

2.1 构造

我们这里研究C++11的构造。

2.1.1 无参默认构造

  • key_compare 是比较函数对象的类型,默认为 std::less< Key >。
  • allocator_type 是内存分配器类型,默认为 std::allocator< Key >。
cpp 复制代码
set<int> s;

调试:

  • 迭代器是双向迭代器。

2.1.2 迭代器区间构造

  • InputIterator 为输入迭代器类型。

    遍历迭代区间中的元素,把非重复元素插入set中。
(1)用一个 vector对象 初始化
cpp 复制代码
vector<int> v = { 10, 20, 30, 20, 10 };
set<int> s(v.begin(), v.end());

调试:

(2)用一个 istream_iterator对象 初始化

从输入中读取值初始化:

cpp 复制代码
istream_iterator<int> eos;
istream_iterator<int> iit(cin);
set<int> s5(iit, eos);

运行:

调试:

2.1.3 initializer_list 初始化

  • initializer_list 是C++11 新增的特性,接受 花括号 {} 包围的元素列表。
cpp 复制代码
set<int> s1 = { 1, 5, 5, 4 };
set<int> s2{ 1, 5, 5, 4 };

调试:

2.2 增删查

set不支持修改数据,这会破坏底层红黑树结构。

2.2.1 insert

(1)插入单个元素
  1. 返回值类型为一个 pair<iterator, bool>。
    pair类的定义如下:

    Ⅰ pair 中 iterator成员 指向已存在元素或新插入元素的迭代器。
    Ⅱ pair 中 bool成员 存储是否成功插入信息。
  2. const value_type& val:接受左值(有名字的变量)。
  3. value_type&& val:接受右值(临时对象、std::move 转换的值)。
①常见用法
cpp 复制代码
set<int> s{ 1, 2, 3 };
auto result = s.insert(4);
cout << "bool: " << result.second;
cout << ",element: " << *result.first << endl;

运行:

②插入 std::move 转换的值
cpp 复制代码
set<string> mySet;
string another = "Temp";
auto result = mySet.insert(move(another));
cout << "bool: " << result.second;

运行:

调试:

  1. mySet 中的元素现在拥有 "Temp" 的内存。
  2. another 不再拥有任何字符串内存(通常变为空)。

std::move 不是移动数据本身,而是授予"窃取资源"的权限,让目标对象可以高效地接管资源所有权。

(2)指定位置插入

position: 一个提示位置的迭代器,将在此位置附近插入。

cpp 复制代码
set<int> mySet;
vector<int> data = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto hint = mySet.end();
for (int x : data) {
	hint = mySet.insert(hint, x);  
}
  • 批量插入,插入序列有序,使用提示插入。
  • 插入操作时间复杂度 O(1)。

调试:

(3)插入一段区间

函数效果等价于:

cpp 复制代码
while (first != last) {
    insert(*first);
    ++first;
}

上代码测试:

cpp 复制代码
set<int> mySet;
vector<int> vec = { 10, 20, 30, 40, 50, 10 };
mySet.insert(vec.begin(), vec.end());

调试:

(4)插入一段 initializer_list

函数效果等价于:

cpp 复制代码
for (const auto& elem : il) {
    insert(elem);
}

上代码测试:

cpp 复制代码
set<int> mySet;
mySet.insert({ 1, 2, 3, 4, 5 });

调试:

2.2.2 erase

(1)删除指定位置元素

① position: 一个 const_iterator(只能读取元素,不能修改),指向要删除的元素的位置。

② 该迭代器必须指向容器内的有效元素。

cpp 复制代码
set<int> s{ 1, 2, 3 };
s.erase(s.end());
复制代码
否则程序奔溃:

③ 返回值类型为 iterator,指向被删除元素的下一个元素。

  1. 若删除的是最后一个元素,则返回 end()。
  2. 为什么返回值类型不是 const_iterator ?
cpp 复制代码
typedef const int* iterator;
typedef const int* const_iterator;
复制代码
Ⅰ iterator 和 const_iterator 都不能修改元素本身,底层类型可能都是一样的:
Ⅱ 可以把 const_iterator 理解为只读容器(不能erase)的迭代器。
Ⅲ erase 返回 iterator(可变容器的迭代器) 保持了它原来的特性,可继续借助返回值(赋值给定义的it遍历迭代器)连续erase。
cpp 复制代码
set<int> s{ 1, 2, 3 };
for (auto it = s.begin(); it != s.end(); ) {
	it = s.erase(it);
}
  1. 为什么参数类型不是 iterator 而是 const_iterator?

    Ⅰ 遵循最小权限原则:对于参数,函数只要求它真正的权限。参数类型为 const_iterator 就是函数 erase 要 求的最小权限。

    Ⅱ 最大灵活性:函数接受更广泛的参数类型。我们传参 iterator (iterator 可以隐式转换成 const_iterator, 反之不可),erase 函数也接受。

删除set中的min值。

根据 set 底层是红黑树的特性:

cpp 复制代码
set<int> s{ 1, 2, 3 };
s.erase(s.begin());

直接传begin。

调试:

(2)删除指定值元素

1.返回值类型为 size_t ,表示已删除元素个数。

2.由于 set 元素唯一,可通过返回值判断删除是否成功。

  • 返回值为0,元素不存在。
  • 返回值为1,删除成功。
    测试:
cpp 复制代码
set<int> s{ 1, 2, 3 };
size_t n1 = s.erase(2);
cout << "test1: " << n1 << endl;
size_t n2 = s.erase(2);
cout << "test2: " << n2 << endl;

运行:

(3)删除一个区间
  1. 返回值指向最后一个被删除元素之后的迭代器。
  2. 删除范围是 [first, last) :参数 first 指向要删除的第一个元素的迭代器,last 指向要删除的最后一个元素之后的迭代器。

测试:

cpp 复制代码
set<int> s{ 1, 2, 3 };
auto it = s.erase(s.begin(),--s.end());
cout << "remaining number: " << *it << endl;

运行:

2.2.3 find

  1. 俩函数返回值不同,可以看出来,一个是const容器的 find,一个是可变容器的 find。
  2. 没找到返回set::end(如下面的代码)。
cpp 复制代码
set<int> s{ 1, 2, 3 };
cout << (s.find(4) == s.end()) << endl;

运行:

另外:

  • 算法库的find是暴力查找,时间复杂度是O(N)。
  • set的find效率更高,时间复杂度是O(logN)。

2.3 其他接口

2.3.1 count

返回元素在容器里的个数。

也可以用来判断元素是否在容器里:

cpp 复制代码
set<int> s{ 1, 2, 3 };
cout << s.count(4) << endl;

运行:

2.3.2 lower_bound/upper_bound

  1. lower_bound 返回大于等于val的元素的迭代器。
  2. upper_bound 返回大于val的元素的迭代器。
  3. 这俩配合起来,方便找左闭右开的区间。
    删除这段区间的元素:
cpp 复制代码
set<int> s = { 10, 20, 30, 40, 50, 60 };
auto first = s.lower_bound(25);
auto last = s.upper_bound(55);
s.erase(first, last);

调试:

算法库里也有这俩函数,但是要求容器内元素有序。

2.4 multiset和set的差异

包在里了:

2.4.1 不去重

  1. 插入的值可以相同,所以insert操作永远不会失败。
  2. 这里第一个 insert 插入单个元素,返回值就不是一个pair了。

set 里元素唯一,可能会insert失败,所以返回值为一个pair(带有是否insert成功判断)。

测试:

cpp 复制代码
multiset<int> ms{1,1,3,3};
auto it = ms.insert(2);
cout << *it << endl;

insert 返回值指向新插入元素的迭代器。

运行:

调试:

2.4.2 find

  1. find 找到的是中序遍历到的第一个。
  2. 寻找过程:找左子树有没有val,没有就取前一个找到了的val。
  3. 跟set的一样没找到就返回end。

测试:

cpp 复制代码
multiset<int> ms{ 1,1,2,2,3,3 };
auto it = ms.find(2);
while(it != ms.end()){
    cout << *it << " ";
    ++it;
}
cout << endl;

运行:

2.4.3 count

  • 返回值为 参数val 在容器中出现的个数,可能取值为0~n。

set中元素唯一,所以count的返回值取值只可能为0或1。

测试:

cpp 复制代码
multiset<int> ms{ 1,1,2,2,3,3 };
size_t count1 = ms.count(2);
size_t count2 = ms.count(4);
cout << "count1:" << count1 << endl;
cout << "count2:" << count2 << endl;

运行:

2.4.4 erase

(1)给迭代器删除一个
cpp 复制代码
multiset<int> ms{ 1,1,2,2,3,3 };
ms.erase(ms.begin());

调试:

(2)给值删除所有
cpp 复制代码
multiset<int> ms{ 1,1,2,2,3,3 };
ms.erase(1);

调试:

(3)迭代区间

就不上代码了。

2.5 题目

来两道题练练手。

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

cpp 复制代码
ListNode *detectCycle(ListNode *head) {
    set<ListNode*> s;
    ListNode* node = head;
    while(node){
        if(!s.insert(node).second){
            return node;
        }
        node = node -> next;
    }
    return nullptr;
}

胜在代码简单。

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

(1)先去重
①算法库

算法库里有个unique函数,但是我们不用这个。

②用set存储数据

迭代器区间构造。

(2)再插入

把重叠数据插入vector,该vector作为返回值。

(3)代码
cpp 复制代码
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
    vector<int> v;
    set<int> s1(nums1.begin(),nums1.end());
    set<int> s2(nums2.begin(),nums2.end());
    for(auto e:s2){
        if(!s1.insert(e).second){
            v.push_back(e);
        }
    }
    return v;
}

2.4.3(拓展题型)

(1)同样用set存储数据
(2)找 交/差 集

应用场景:同步算法。

①交集
  1. 依次比较。
  2. 让值小的++。
  3. 值相等的就是交集,后同时++。
  4. 其中一个结束就结束。

代码:

cpp 复制代码
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
	vector<int> v;
	set<int> s1(nums1.begin(), nums1.end());
	set<int> s2(nums2.begin(), nums2.end());
    auto it1 = s1.begin();
    auto it2 = s2.begin();
    while(it1 != s1.end() && it2 != s2.end()){
		if (*it1 == *it2) {
			v.push_back(*it1);
            ++it1;
            ++it2;
		}else if(*it1 < *it2) {
			++it1;
		}else {
			++it2;
		}
    }
    return v;
}
②差集
  1. 依次比较。
  2. 值小的就是差集,小的++。
  3. 值相等就同时++。
  4. 其中一个结束就结束。
  5. 另一个剩下的都是差集。

代码:

cpp 复制代码
vector<int> difference(vector<int>& nums1, vector<int>& nums2) {
	vector<int> v;
	set<int> s1(nums1.begin(), nums1.end());
	set<int> s2(nums2.begin(), nums2.end());
    auto it1 = s1.begin();
    auto it2 = s2.begin();
    while (it1 != s1.end() && it2 != s2.end()) {
		if (*it1 == *it2) {
			++it1;
			++it2;
		}
		else if (*it1 < *it2) {
            v.push_back(*it1);
			++it1;
		}
		else {
			v.push_back(*it2);
			++it2;
		}
    }
    if (!s1.empty()) {
		while (it1 != s1.end()) {
			v.push_back(*it1);
			++it1;
		}
    }else if (!s2.empty()) {
		while (it2 != s2.end()) {
			v.push_back(*it2);
			++it2;
		}
    }
    return v;
}

三 map系列的使用

3.1 pair的介绍

前面有提到过:

pair有键值对的意思,map中的一个元素就是一个pair。

在容器map中:

3.2 insert

3.2.1 写一个pair有名对象再插入

cpp 复制代码
map<string, int> scores;
pair<string,int> pair("zhangsan", 100);
scores.insert(pair);

调试:

3.2.2 插入一个pair匿名对象

方便一些。

cpp 复制代码
map<string, int> scores;
scores.insert(pair<string,int>("zhangsan", 100));

3.2.3 隐式类型转换:直接插入值

最方便。

cpp 复制代码
map<string, int> scores;
scores.insert({ "zhangsan", 100 });

3.2.4 make_pair构造pair

上代码:

cpp 复制代码
map<string, int> scores;
scores.insert(make_pair("zhangsan", 100));

3.2.5 返回值

cpp 复制代码
pair<iterator,bool>

若返回pair第二个成员为true,则没有相同值;

若为false,则有相同值。

cpp 复制代码
map<string, int> scores;
scores.insert({ "zhangsan", 100 });
auto result = scores.insert({ "zhangsan", 80 });
if (result.second) {
	cout << "success" << endl;
}
else {
	cout << "failed" << endl;
}

键相同则插入失败。

运行:

3.3 遍历

插入代码放这里:

cpp 复制代码
map<string, int> scores;
scores.insert({ "zhangsan", 100 });
scores.insert({ "lisi", 80 });

3.3.1 迭代器遍历

  • pair不支持直接输入输出,要取first/second。
cpp 复制代码
auto it = scores.begin();
while (it != scores.end()) {
	cout << it->first << ":" << it->second << endl;
	++it;
}

运行:

3.3.2 范围for

支持迭代器,就能用范围for。

记得加引用。

cpp 复制代码
for (auto& e:scores) {
	cout << e.first << ":" << e.second << endl;
}

运行:

3.4 删查改

初始map:

cpp 复制代码
map<string, int> scores = { { "zhangsan", 100 }, { "lisi", 80 } };

调试如图:

3.4.1 erase

跟set类似。

(1)用迭代器删除
cpp 复制代码
scores.insert({ "a", 60 });
scores.erase(scores.begin());

调试:

继续:

(2)用键删除
cpp 复制代码
scores.insert({ "a", 60 });
scores.erase("a");

调试:

继续:

(3)用迭代区间删除
cpp 复制代码
scores.erase(scores.begin(), scores.end());

调试:

继续:

3.4.2 find

也是跟set类似。

cpp 复制代码
auto it = scores.find("lisi");
cout << it->first << ":" << it->second << endl;

运行:

3.4.3 通过迭代器修改

借助find返回的迭代器修改值:

cpp 复制代码
auto it = scores.find("lisi");
cout << "before" << endl;
cout << it->first << ":" << it->second << endl;
it->second = 90;
cout << "after" << endl;
cout << it->first << ":" << it->second << endl;

运行:

3.5 应用实例

实现一个简单字典。

输出字典所有内容:

cpp 复制代码
map<string, string> dict = { {"left", "左边"}, {"right", "右边"}
    , {"insert", "插入"}, {"integer","整数"}};
auto it = dict.begin();
while (it != dict.end()){
	cout << it->first << ":" << it->second << endl;
    ++it;
} 
cout << endl;

运行:

简单输入,匹配输出:

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

运行:

3.6 迭代器和[ ]

场景:统计水果出现的次数。

cpp 复制代码
string arr[] = { "苹果","西瓜","苹果","西瓜"
    ,"苹果","苹果","西瓜","苹果","香蕉","苹果","香蕉" };
map<string, int> countMap;

3.6.1 迭代器代码

cpp 复制代码
for (const auto& str : arr){
    auto ret = countMap.find(str);
    if (ret == countMap.end()){
        countMap.insert({ str, 1 });
    }else{
        ret->second++;
    }
}
  1. 遍历arr。
  2. 如果ret在countMap里,它的值(即second)就++。
  3. ret不在countMap里,就新增这个键。

输出:

cpp 复制代码
for (const auto& e : countMap){
	cout << e.first << ":" << e.second << endl;
}

运行:

3.6.2 [ ]代码

(1)使用场景1

更为方便就能替代上一串代码。

cpp 复制代码
for (const auto& str : arr) {
    ++countMap[str];
}
  1. 虽然循环体里只有一句。
  2. 实际逻辑:没有当前key就插入后++,有就直接++。
(2)使用场景2

修改:

cpp 复制代码
countMap["香蕉"] = 0;

运行输出:

3.7 题目

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

(1)思路
  1. 定义一个map,键为旧结点,值为新结点。
  2. 先把新节点一个一个(根据旧结点的值)创建出来,再跟map里的旧结点一一对应。
  3. 再来个循环,把原结点的 next 和 random 映射到新结点之间。
(2)上代码
cpp 复制代码
Node* copyRandomList(Node* head) {
    map<Node*,Node*> m;
    for(Node* p = head;p != nullptr;p = p->next){
        Node* newNode = new Node(p->val);
        m[p] = newNode;
    }
    for(Node* p = head;p != nullptr;p = p->next){
        m[p]->next = m[p->next];
        m[p]->random = m[p->random];
    }
    return m[head];
}

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

cpp 复制代码
vector<string> topKFrequent(vector<string>& words, int k) {}

(1) 遍历words,用一个map统计每个word出现次数。

cpp 复制代码
map<string, int> countMap;
for (auto& word : words) {
	countMap[word]++;
}

(2) 用一个vector存储map统计结果,便于使用 stable_sort 来排序。

cpp 复制代码
vector<pair<string, int>> v(countMap.begin(),countMap.end());
stable_sort(v.begin(), v.end(), Compare());
  1. stable_sort 是 < algorithm > 里的函数:

    Ⅰ 原来map中是按字典序排的,符合题目条件之一。
    Ⅱ 因此不能破坏原本(针对出现次数相同的string)的相对顺序,要用stable_sort。
  2. 这里的Compare需要自己定义一下:
cpp 复制代码
struct Compare {
    bool operator()(const pair<string, int>& a, const pair<string, int>& b) {
        return a.second > b.second;
    }
};

意思就是次数多的往前排。
(3) 把前k个拎出来放到一个vector里,就是我们要返回的vector。

cpp 复制代码
vector<string> ret;
for (int i = 0;i < k;++i) {
    ret.push_back(v[i].first);
}
return ret;

3.8 multimap

  • 和map相比的区别:支持关键值key重复插入。

multimap的使用跟map差不多,不再赘述,重点介绍特殊的函数。

equal_range函数

  1. 两个版本分别对应 const容器 和 可变容器。
  2. 返回值为一个pair,包含两个迭代器。
  3. 第一个迭代器,指向第一个不小于 k 的元素(相当于lower_bound(k) )。
  4. 第二个迭代器,指向第一个大于 k 的元素(相当于 upper_bound(k) )。
  5. 所以这两个迭代器构造出来的区间,包含所有键等于 k 的元素。

测试一下:

cpp 复制代码
multimap<int, std::string> mm = {
    {1, "apple"}, {2, "banana"}, {2, "blueberry"},
    {2, "blackberry"}, {3, "cherry"}
};

找键为2的元素:

cpp 复制代码
auto range = mm.equal_range(2);
for (auto it = range.first; it != range.second; ++it) {
	std::cout << it->first << ": " << it->second << std::endl;
}

运行:

map&set使用 的学习就到这里,下一篇我们上新的树 AVLTree ,今天会更出来~


相关推荐
Byte不洛1 小时前
深入理解C++智能指针:从RAII到shared_ptr
c++·智能指针·raii·unique_ptr·shared_ptr·auto_ptr
allnlei1 小时前
gRPC C++ Callback API(Reactor 模式)介绍
开发语言·c++
Eiceblue1 小时前
锁定单元格 :C# 控制 Excel 单元格编辑权限
开发语言·c#·excel
lilong(DLC)1 小时前
Qt信号槽在异步连接时需要将参数进行复制吗?
开发语言·qt
沐知全栈开发1 小时前
RSS 参考手册
开发语言
贫民窟的勇敢爷们1 小时前
构建基于Python与机器学习的智能客服
开发语言·python·机器学习
shehuiyuelaiyuehao1 小时前
算法20,x的平方根
开发语言·python·算法
csbysj20201 小时前
.switchClass() 方法详解
开发语言
菜_小_白1 小时前
高性能线程池
linux·c++·设计模式