前言:本篇博客只介绍这两个容器比较难的接口,其余接口可以通过官方文档去查询了解
文档入口:cplusplus.com - The C++ Resources Networkhttp://m.cplusplus.com/
目录
multiset中面对重复的数据它先找到的是那一个重复的数据呢?
我们发现set/multiset是没有修改接口的,我们可以借助迭代器进行修改吗?
一、关联式容器
STL中将数据结构叫做容器。我们已经接触过STL中的部分容器,比如:vector、list、deque、forward_list(C++11)等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面存储的是元素本身。那什么是关联式容器?它与序列式容器有什么区别?
ps:栈和队列是适配器
二、键值对
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。
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)
{}
};
树形结构的关联式容器
set
set的介绍
set的官方文档介绍:set - C++ Reference (cplusplus.com)
- 1. set是按照一定次序存储元素的容器
- 2. 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
- 3. 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序。
- 4. set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对子集进行直接迭代。
- 5. set在底层是用二叉搜索树(红黑树)实现的。
注意:
- 1. 与map/multimap不同,map/multimap中存储的是真正的键值对<key, value>,set中只放value,但在底层实际存放的是由<value, value>构成的键值对。
- 2. set中插入元素时,只需要插入value即可,不需要构造键值对。
- 3. set中的元素不可以重复(因此可以使用set进行去重)。
- 4. 使用set的迭代器遍历set中的元素,可以得到有序序列
- 5. set中的元素默认按照小于来比较
- 6. set中查找某个元素,时间复杂度为:
- 7. set中的元素不允许修改(为什么?)因为修改了以后它就不是一颗搜索树了
- 8. set中的底层使用二叉搜索树(红黑树)来实现
set的使用
1. set的模板参数列表
T: set 中存放元素的类型,实际在底层存储 <value, value> 的键值对。Compare : set 中元素默认按照小于来比较(就是个仿函数)Alloc : set 中元素空间的管理方式,使用 STL 提供的空间配置器管理
2.set的使用
void test_set1()
{
//排序+去重
set<int> s;
s.insert(3);
s.insert(1);
s.insert(8);
s.insert(2);
s.insert(5);
s.insert(5);
s.insert(5);
//set<int>::iterator it = s.begin();
auto it = s.begin();
while (it != s.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
排序加去重,排序也就是进行中序遍历。
3.erase
删除一个位置必须保证删除的位置的迭代器是有效的。
删除一个值
这个地方为什么不用bool值呢?这就与multiset有关。
multiset
multiset的介绍
multiset的官方文档介绍:multiset - C++ Reference (cplusplus.com)
- 1. multiset是按照特定顺序存储元素的容器,其中元素是可以重复的。
- 2. 在multiset中,元素的value也会识别它(因为multiset中本身存储的就是<value, value>组成的键值对,因此value本身就是key,key就是value,类型为T). multiset元素的值不能在容器中进行修改(因为元素总是const的),但可以从容器中插入或删除。
- 3. 在内部,multiset中的元素总是按照其内部比较规则(类型比较)所指示的特定严格弱排序准则进行排序。
- 4. multiset容器通过key访问单个元素的速度通常比unordered_multiset容器慢,但当使用迭代器遍历时会得到一个有序序列。
- 5. multiset底层结构为二叉搜索树(红黑树)。
- 1. multiset中在底层中存储的是<value, value>的键值对
- 2. mtltiset的插入接口中只需要插入即可
- 3. 与set的区别是,multiset中的元素可以重复,set是中value是唯一的
- 4. 使用迭代器对multiset中的元素进行遍历,可以得到有序的序列
- 5. multiset中的元素不能修改
- 6. 在multiset中找某个元素,时间复杂度为O(logN)
- 7. multiset的作用:可以对元素进行排序
multiset的使用
此处只简单演示set与multiset的不同,其他接口接口与set相同,大家可参考set。
如果我们不想去重仅仅就想排序该怎么办呢?
multi的意思是多样的,多种的。它依然是在我们set里面的。
我们发现multiset是不进行去重操作的,multi就是多种多样,允许我们的数据有重复,冗余。
multiset中面对重复的数据它先找到的是那一个重复的数据呢?
我们用find进行验证。find的val有多个值的时候,返回中序第一个val值所在节点的迭代器。
我们将1改成8再次验证。
答案就是返回中序遍历的第一个重复值。
如何删除所有的重复数据?
在这里我们以删除1为例
因为找到的1是从中序的第一个位置开始,所以就可以进行遍历,只要判断出pos是1就将它删除,知道遍历结束。但是我们目前的写法是有问题的。
程序崩溃了。
它是二叉树,它的迭代器类似于节点的指针,当第一个1这个节点被干掉了,它里面的pos就成了野指针,++pos是找不到下一个位置的。
方法1:
解决方法:定义一个next,将pos赋值给next,对next++,然后删除pos,再将next赋值给pos,也就是提前保存好pos的值。
方法2:用值去删
所以这时候就明白为什么erase的返回值是返回被删除值的个数,对于set没意义,对于multiset就有意义了。
有几个就去删几个。
可以认为这个erase的实现就是依据方法1实现二来的,在方法一中加个计数器,进行封装,本质都是一样的。
我们发现set/multiset是没有修改接口的,我们可以借助迭代器进行修改吗?
经过验证是不可以进行修改的。*pos是个常量。如何做到的呢?
这里的迭代器要去调用operator*和operator->,他俩返回const T的引用就可以了。也就是set的底层普通迭代器和const迭代器在这个地方的实现是一样的,都不允许修改。因为修改了以后就不能保证你还是一个搜索树了。
map
map的介绍
1. map的模板参数说明
- key: 键值对中key的类型
- T: 键值对中value的类型
- Compare: 比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比较,一般情况下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户自己显式传递比较规则 (一般情况下按照函数指针或者仿函数来传递)
- Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器
- 注意:在使用map时,需要包含头文件。
2.Pair
map这我们首先得了解pair,因为map是标准的key,value结构,也就是每个节点的位置除了存key,还存了value,那map的key,value是怎么存的呢?它和我们之前实现的不太一样,并不是给一个key给一个value,而是将keyvalue封装到了一个类结构里面去,这个结构叫做pair,pair也叫做键值对。
pair本身的结构也是一个类
里面有两个成员,
first就是带一个模板参数也就是key,second就是第二个模板参数也就是value。
- 1. map中的的元素是键值对
- 2. map中的key是唯一的,并且不能修改
- 3. 默认按照小于的方式对key进行比较
- 4. map中的元素如果用迭代器去遍历,可以得到一个有序的序列
- 5. map的底层为平衡搜索树(红黑树),查找效率比较高
- 6. 支持[]操作符,operator[]中实际进行插入查找。
map的使用
insert
这里我们主要讨论插入的方法1
insert的是一个value_type,value_type就是一个pair。pair第一个参数就是key,第二个参数是mapped_type也就是T,也就是value。
第一种插入方法:
第二种插入方法:
第三种插入方法:借助make_pair
make_pair
是一个函数模板,返回值是pair,返回pair的匿名对象。
map的遍历
像往常一样写时会报错的
解引用返回节点里面的数据,节点里面是数据不是一个值,而是把key和value放到了一个结构里去,也就是pair,所以它的返回值是pair,这里不支持输出pair,因为pair没有重载流提取和流插入
正确方法:将它的两个值都打印出来
或者使用->两者更常用下面这种
同时可以用范围for
这里的打印出来还是有排序的意思的,按照key去排序,这里我们的key是string类型,所以按照ascll码去排序。
[ ]的使用
我们假设现在要统计水果的次数
map的key是不支持修改的,value是支持修改的。
因为key修改会影响树的结构,但value修改是不影响的。
传统写法:
void test2_map()
{
string arr[] = { "苹果","苹果","香蕉","苹果","香蕉","苹果","樱桃" };
//key是水果的名称,value是次数
map<string, int> countMap;
for (auto& str : arr)
{
//看下水果有没有出现过,如果没有出现过我们就插入,如果已经出现过,就进行修改
auto ret = countMap.find(str);
if (ret == countMap.end()) //表示没有找到
{
countMap.insert(make_pair(str, 1));
}
else
{
//find返回的是迭代器
ret->second++;
}
}
for (auto& kv : countMap)
{
cout << kv.first << ":" << kv.second << endl;
}
}
缺陷:如果key不在的情况下,查找会走一次这个搜索树,插入的时候也会走一次这个搜索树。
优化:
首先明白insert的返回值
insert的第一个重载有一个返回值,它返回的不是单纯的真假,返回一个pair,pair里的bool很好理解如果key已经有了返回false,没有返回true,那这个迭代器该如何理解呢?
也就是说如果key没有,插入后返回true,迭代器指向新插入的这个节点。
如果key已将存在,则返回false,迭代器还是指向原来的这个节点。
ps:迭代器含义
void test2_map()
{
string arr[] = { "苹果","苹果","香蕉","苹果","香蕉","苹果","樱桃" };
//key是水果的名称,value是次数
map<string, int> countMap;
for (auto& str : arr)
{
auto kv = countMap.insert(make_pair(str, 1)); //先不管你在不在,直接插入
//一种情况是这个水果不在,插入成功,给1次 二、这个水果已经出现过,插入失败应该对次数++
if (kv.second == false)
{
kv.first->second++;
}
}
for (auto& kv : countMap)
{
cout << kv.first << ":" << kv.second << endl;
}
}
这个方法最大有点就是insert既充当了插入,又充当了查找。
方法二:
for (auto& str : arr) { countMap[str]++; }
原理解释:
之前我们的[ ] 支持的是随机访问,但是这里的[ ]肯定不是随机访问,因为它是一颗树。
同时它还等价于这个,大概认为它是这一样实现的
mapped_type& operator[] (const key_type& k) { return (*((this->insert(make_pair(k,mapped_type()))).first)).second; }
我们简化下这句代码:
我们结合我们统计水果次数的例子深入理解一下:
这里我们的key_type就是string ,mapped_type就是int ,他去调用insert的时候,我也不知道水果到底出现过了没,所以我们先插入,这里的k就给的是水果,value给的是mapped_type的匿名对象,这个匿名对象对于int来说就是0(C++对内置类型也进行了升级,使得他们也有默认的构造函数),插入的时候可能插入成功或者插入失败,如果这个水果没有就插入成功了,插入成功后就返回pair,pair的迭代器就是新插入水果节点的迭代器,再对这个迭代器解引用就是(我们的简化版就是直接用的箭头)这个节点里面所在水果的key,value,key是刚插入的水果,value是0。而且这里返回的是这个次数的引用。刚开始返回的是0次,countMap[str]++就变成了1次,这个++是作用在函数调用的返回值上面的;第二次苹果再来了以后,insert的时候,k就给的是水果,value给的是mapped_type的匿名对象,还是0。但此时苹果已经有了,会插入失败,迭代器就返回之前那个苹果的迭代器,上次苹果中的次数已经变成1次了,然后返回第一次苹果的节点里面的second的别名,再进行countMap[str]++,就变成了2次。以此类推...
[ ]的功能
1、插入 2、查找(key对应value) 3、修改(key对应value)
如果水果第一次出现,对应的是插入+修改,水果不是第一次出现,对应查找+修改
对应用法举例
但是建议查找最好还是用find,因为如果没有key,[ ]就成了插入。
multimap
multiset的介绍
multiset的官方文档介绍:multimap - C++ Reference (cplusplus.com)
- 1. multimaps是关联式容器,它按照特定的顺序,存储由key和value映射成的键值对<key, value>,其中多个键值对之间的key是可以重复的。
- 2. 在multimap中,通常按照key排序和惟一地标识元素,而映射的value存储与key关联的内容。key和value的类型可能不同,通过multimap内部的成员类型value_type组合在一起,value_type是组合key和value的键值对: typedef pair<const Key, T> value_type;
- 3. 在内部,multimap中的元素总是通过其内部比较对象,按照指定的特定严格弱排序标准对key进行排序的。
- 4. multimap通过key访问单个元素的速度通常比unordered_multimap容器慢,但是使用迭代器直接遍历multimap中的元素可以得到关于key有序的序列。
- 5. multimap在底层用二叉搜索树(红黑树)来实现。
multimap的使用
- 1. multimap中的key是可以重复的。
- 2. multimap中的元素默认将key按照小于来比较
- 3. multimap中没有重载operator[]操作
- 4. 使用时与map包含的头文件相同:
同样是允许数据冗余
multimap就没有[ ]了,因为我现在有很多的key,到时候返回的时候我就不知道到底该返回那个key对应的value。其他操作都一样
cout
cout的作用也是查找,但更大的意义是用来计数,适合multimap使用
erase
multimap中的erase就是把所有的key都删除调
练习题
一、统计前k种水果
以该组数据为例,k值传3.
方法一:
统计次数我们已轻车熟路,重要的就是进行排序,我们首先使用sort进行排序,但是sort要求的是随机迭代器,因为sort底层是快排,需要三数取中,也就是需要迭代器可以做减法操作,显然这里传map的迭代器是不行的。
但是我们可以将countMap中的数据放到vector中,sort是支持vector的迭代器的。
但此时我们排序的结果是这样的,那么pair支持排序吗?
pair是支持比较大小的,但是它比较大小是先比first,first相等再去比second
但是我们不想去比first,只需要去比较second,我们写个仿函数就可以解决了。
仿函数:
struct CountVal
{
//仿函数最大的优势就是你可以控制它排序的方式
bool operator()(const pair<string, int>& l, const pair<string, int>& r)
{
return l.second > r.second; //>就是降序 ,<就是升序
}
};
完整代码演示:
struct CountVal
{
//仿函数最大的优势就是你可以控制它排序的方式
bool operator()(const pair<string, int>& l, const pair<string, int>& r)
{
return l.second > r.second; //>就是降序 ,<就是升序
}
};
void GetFavoriteFruit(const vector<string>& fruits, size_t k)
{
//统计次数
map<string, int> countMap;
for (auto& str : fruits)
{
countMap[str]++;
}
//排序
vector<pair<string, int>> sortV;
for (auto& kv : countMap)
{
sortV.push_back(kv);
}
sort(sortV.begin(), sortV.end(), CountVal());//这里的sort是不知道如何排pair,pair支持比较吗?
for (auto&kv : sortV)
{
cout <<kv.first<<":"<<kv.second << endl;
}
cout << endl;
//取出前k个
for (int i = 0; i < k; i++)
{
cout << sortV[i].first << ":" << sortV[i].second << endl;
}
}
方法一的优化:
目前我们方法一存在的问题:sortV插入数据的时候得开很大的空间,如果pair里面存的很大的话。同时存在深拷贝问题。
优化方法:pair很大,但是迭代器很小,里面只存一个节点的指针,直接在vector里面存map的迭代器,只有4或者8个字节。所以我们要对仿函数进行修改,vector中存的是迭代器则访问成员时使用->即可。
完整代码:
struct CountValiterator
{
//仿函数最大的优势就是你可以控制它排序的方式
bool operator()(const map<string, int>::iterator& l, const map<string, int>::iterator& r)
{
return l->second > r->second; //>就是降序 ,<就是升序
}
};
void GetFavoriteFruit2(const vector<string>& fruits, size_t k)
{
//统计次数
map<string, int> countMap;
for (auto& str : fruits)
{
countMap[str]++;
}
//上面方法存在的问题:sortV插入数据的时候得开很大的空间,如果pair里面存的很大的话
vector<map<string,int>::iterator> sortV; //pair很大,但是迭代器很小,里面只存一个节点的指针,直接在vector里面存map的迭代器,只有4或者8个字节
auto it = countMap.begin();
while (it != countMap.end())
{
sortV.push_back(it);
++it;
}
sort(sortV.begin(), sortV.end(), CountValiterator());//这里的sort是不知道如何排pair,pair支持比较吗?
for (int i = 0; i < k; i++)
{
cout << sortV[i]->first << ":" << sortV[i]->second << endl;
}
}
方法二:
利用multimap进行排序,因为它不会去重,但这里我们是要对数字进行排序,所以要让map中的value,做multimap中的key。但multimap默认排序时升序,因为它compare的缺省值是less<key>
取出前三种水果可以用反向迭代器倒着取遍历。
如果不使用反向迭代器,我们就可以使用仿函数:great<key>官方提供的降序排序
完整代码(使用反向迭代器)
void GetFavoriteFruit3(const vector<string>& fruits, size_t k)
{
//统计次数
map<string, int> countMap;
for (auto& str : fruits)
{
countMap[str]++;
}
//排序,它不会去重
multimap<int, string> sortMap;
for (auto& kv : countMap)
{
sortMap.insert(make_pair(kv.second, kv.first));
}
//取出前三种水果可以用反向迭代器倒着取遍历
auto it = sortMap.rbegin();
while (it != sortMap.rend() && k != 0)
{
cout << it->second << ":" << it->first << endl;
++it;
k--;
}
}
完整代码(不使用反向迭代器)
void GetFavoriteFruit3(const vector<string>& fruits, size_t k)
{
//统计次数
map<string, int> countMap;
for (auto& str : fruits)
{
countMap[str]++;
}
//取出前三种水果,不使用反向迭代器,就使用仿函数
multimap<int, string, greater<int>> sortMap;
for (auto& kv : countMap)
{
sortMap.insert(make_pair(kv.second, kv.first));
}
for (auto& kv : sortMap)
{
if (k)
{
cout << kv.second << ":" << kv.first << endl;
k--;
}
}
}
方法三:
利用优先级队列也就是堆
struct CountValpq
{
//仿函数最大的优势就是你可以控制它排序的方式
bool operator()(const pair<string, int>& l, const pair<string, int>& r)
{
return l.second < r.second; //>建大堆 ,<就是建小堆
}
};
void GetFavoriteFruit4(const vector<string>& fruits, size_t k)
{
//统计次数
map<string, int> countMap;
for (auto& str : fruits)
{
countMap[str]++;
}
//利用优先级队列--堆
priority_queue < pair<string, int>, vector<pair<string, int>>, CountValpq> pq; //类模板传类型,函数模板传对象
for (auto& kv : countMap)
{
pq.push(kv);
}
while (k--)
{
cout << pq.top().first << ":" << pq.top().second << endl;
pq.pop();
}
cout << endl;
}
方法三的优化:
类似于方法一的优化我们改为传迭代器
struct CountValiteratorpq
{
//仿函数最大的优势就是你可以控制它排序的方式
bool operator()(const map<string, int>::iterator& l, const map<string, int>::iterator& r)
{
return l->second < r->second; //>建大堆 ,<就是建小堆
}
};
void GetFavoriteFruit5(const vector<string>& fruits, size_t k)
{
//统计次数
map<string, int> countMap;
for (auto& str : fruits)
{
countMap[str]++;
}
//利用优先级队列--堆
priority_queue < map<string, int>::iterator, vector<map<string, int>::iterator>, CountValiteratorpq> pq; //类模板传类型,函数模板传对象
auto it = countMap.begin();
while (it != countMap.end())
{
pq.push(it);
++it;
}
while (k--)
{
cout << pq.top()->first << ":" << pq.top()->second << endl;
pq.pop();
}
cout << endl;
}
二、前K个高频单词
给定一个单词列表 words 和一个整数 k ,返回前 k 个出现次数最多的单词。返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序排序
示例 1:
输入: words = ["i", "love", "leetcode", "i", "love", "coding"], k = 2
输出: ["i", "love"]
解析: "i" 和 "love" 为出现次数最多的两个单词,均为2次。
注意,按字母顺序 "i" 在 "love" 之前。
示例 2:输入: ["the", "day", "is", "sunny", "the", "the", "the", "sunny", "is", "is"], k = 4
输出: ["the", "is", "sunny", "day"]
解析: "the", "is", "sunny" 和 "day" 是出现次数最多的四个单词,
出现次数依次为 4, 3, 2 和 1 次。来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/top-k-frequent-words
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/top-k-frequent-words
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
在讲过上道题以后,这个题就显得比较简单了,但是不同的是这道题的难度在于排完序后,如果k值相同,还要将它们按照字符顺序进行输出。也就是说如果k值相同还要将他们按照ascii码值进行排序。这里我们用sort就不行了,因为sort的底层是快排,所以它是一个不稳定的排序。(稳定性博主在讲排序的时候介绍过)。
解决方法:博主在这采用的是multimap进行排序,因为起初用map统计的时候,map会对key进行排序,对应到这道题map就默认会将这些字符串按照ASCII码进行排序。之后将map中的数据放到multimap中的时候是会按照这些字符串的顺序进行插入的,然后根据int值进行比较。所以就解决了稳定性的问题。
完整代码演示:
class Solution {
public:
vector<string> topKFrequent(vector<string>& words, int k) {
//统计次数
map<string, int> countMap;
for(auto&str : words)
{
countMap[str]++;
}
//排序
multimap<int, string, greater<int>> sortMap;
for(auto&kv : countMap)
{
sortMap.insert( make_pair(kv.second, kv.first) );
}
//排好序后放到vector中
vector<string> v;
for(auto&kv : sortMap)
{
if(k)
{
v.push_back(kv.second);
k--;
}
}
return v;
}
};
其他解决方法:既然sort不能用,但是我们可以使用stable_sort,这个算法就可以保持稳定性