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