目录
1. 关联式容器
我们已经接触过STL中的部分容器,比如:vector、list、deque、这些容器统称为序列式容器,
因为其底层为线性序列的数据结构,里面存储的是元素本身,
关联式容器也是用来存储数据的,与序列式容器不同的是,
其里面存储的是<key, value>结构的键值对,在数据检索时比序列式容器效率更高。
总结:
1、容器本身底层采用线性序列存储数据的结构叫做序列式容器,比如vector、list
2、容器本身底层采用键值对存储数据的结构叫做关联式容器,比如map、set
1.1 树形结构的关联式容器
根据应用场景的不同,STL总共实现了两种不同结构的管理式容器:树型结构与哈希结构。
树型结构的关联式容器主要有四种:map、set、multimap、multiset。
这四种容器的共同点是:底层实现为红黑树。
容器中的元素是一个有序的序列。下面一依次介绍每一个容器。
2. set的相关介绍
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中查找某个元素,时间复杂度为:O(logN)。
7. set中的元素不允许修改。
2.1 set的构造和迭代器
2.2 set的容量和操作函数
2.3 set使用代码
#include <iostream>
#include <set>
#include <map>
#include <functional>
using namespace std;
void test_set()
{
//set<int> s;
//s.insert(4);
//s.insert(2);
//s.insert(1);
//set<int> s = { 1, 2, 1, 6, 3, 8, 5 }; C++11
int arr[] = { 1, 2, 1, 6, 3, 8, 5 };
set<int> s(arr, arr + sizeof(arr) / sizeof(arr[0]));
//set<int, greater<int>> s(arr, arr + sizeof(arr) / sizeof(arr[0]));
// 排序 + 去重
set<int>::iterator it = s.begin();
while (it != s.end())
{
//*it = 10; // 和前面二叉搜索树一样不能修改
cout << *it << " ";
++it;
}
cout << endl;
for (const auto& e : s)
{
cout << e << " ";
}
cout << endl;
cout << "size = " << s.size() << endl;
s.erase(2);
s.erase(30); // 直接用erase,没有删除的元素什么事都没
for (const auto& e : s)
{
cout << e << " ";
}
cout << endl;
set<int>::iterator pos = s.find(20); // 用find删除,要判断,不然崩溃
if (pos != s.end())
{
s.erase(pos);
}
pos = s.find(8); // 用find删除,要判断,不然崩溃
if (pos != s.end())
{
s.erase(pos);
}
for (const auto& e : s)
{
cout << e << " ";
}
cout << endl;
cout << "size = " << s.size() << endl;
cout << s.empty() << endl;
}
int main()
{
test_set();
return 0;
}
2.4 multiset使用
set是不允许的键值冗余的,而multiset是允许键值冗余的,也就这个区别而已:
void test_multiset()
{
int arr[] = { 1, 2, 1, 6, 3, 8, 5, 3, 3 };
multiset<int> ms(arr, arr + sizeof(arr) / sizeof(arr[0]));
for (const auto& e : ms)
{
cout << e << " ";
}
cout << endl;
cout << ms.count(1) << endl; // set也有这个接口,是为了和multiset一致,在set中可以用来找元素在不在
// find时,如果有多个值,返回中序的第一个
auto pos = ms.find(3);
while (pos != ms.end())
{
cout << *pos << " ";
++pos; // ++是+到中序的下一个
}
cout << endl;
ms.erase(3); // 删除所有的3
for (const auto& e : ms)
{
cout << e << " ";
}
cout << endl;
pos = ms.find(1); // 删除一个1
if (pos != ms.end())
{
ms.erase(pos);
}
for (const auto& e : ms)
{
cout << e << " ";
}
cout << endl;
}
int main()
{
//test_set();
test_multiset();
return 0;
}
3. map的相关介绍
1. map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。
2. 在map中,键值key通常用于排序和唯一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,为其取别名称为pair:typedef pair<const key, T> value_type;
3. 在内部,map中的元素总是按照键值key进行比较排序的。
4. map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。
5. map支持下标访问符,即在[ ]中放入key,就可以找到与key对应的value。
6. map底层实现为红黑树。
3.1 键值对
map中存储的键值对介绍:
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,
key代表键值,value表示与key对应的信息。比如:现在要建立一个英汉互译的字典,
那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是壹壹
对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。
SGI-STL中关于键值对的定义:
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)
{}
};
常用的:
3.2 map的构造和迭代器
key: 键值对中key的类型。
T: 键值对中value的类型。
Compare : 比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比较,
一般情况下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),
需要用户自己显式传递比较规则(一般情况下按照函数指针或者仿函数来传递)。
Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器。
注意:在使用map时,需要包含头文件#include <map>,键值对的头文件也在里面了。
3.3 map的容量和操作函数
方括号是map的很特别的操作,其它不是连续空间存储的都没有,但是map的方括号和普通的也不一样,它返回的是键值对中key对应的value的引用。
当key不在map中时,通过operator[ ] 获取对应value时会发生什么问题?
在元素访问时,有一个与operator[ ]类似的操作:at()函数(该函数不常用),都是通过key找到与key对应的value然后返回其引用,
不同的是:当key不存在时,operator[ ]用默认value与key构造键值对然后插入,返回该默认value,at()函数直接抛异常。
上面方括号调用的那句代码分成两句就是这样:
3.4 map使用代码
用map来创建字典:
void test_map1()
{
map<string, string> dict;
//pair<string, string> kv1("sort", "排序");
//dict.insert(kv1);
dict.insert(pair<string, string>("sort", "排序")); // 等价于上面注释
dict.insert(pair<string, string>("test", "测试"));
dict.insert(pair<string, string>("string", "字符串"));
typedef pair<string, string> DictKV;
dict.insert(DictKV("string", "xxx"));
dict.insert(make_pair("left", "左边")); // 常用这种插入
dict["right"] = "右边"; // 更常用这种插入
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin(); // 等价于上面注释
while (it != dict.end())
{
//cout << (*it).first << (*it).second <<endl;
cout << it->first << ":" << it->second << endl; // 等价于上面注释
++it;
}
cout << endl;
for (const auto& kv : dict)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
}
int main()
{
//test_set();
//test_multiset();
test_map1();
return 0;
}
用map来计算次数:
void test_map2()
{
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
//map<string, int> countMap;
//for (const auto& str : arr)
//{
// map<string, int>::iterator it = countMap.find(str);
// if (it != countMap.end())
// {
// //(*it).second++;
// it->second++;
// }
// else
// {
// countMap.insert(make_pair(str, 1));
// }
//}
map<string, int> countMap;
for (const auto& str : arr) // 等同于上面一大段注释
{
// 1、str不在countMap中,插入pair(str, int()),然后在对返回次数++
// 2、str在countMap中,返回value(次数)的引用,次数++;
countMap[str]++;
}
map<string, int>::iterator it = countMap.begin();
while (it != countMap.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
}
int main()
{
//test_set();
//test_multiset();
//test_map1();
test_map2();
return 0;
}
3.5 multimap使用
multimap和multiset一样是允许键值冗余的,
1. multimap是关联式容器,它按照特定的顺序,存储由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和map的唯一不同就是:map中的key是唯一的,而multimap中key是可以重复的。
multimap中的接口可以参考map,功能都是类似的。
注意:
1. multimap中的key是可以重复的。
2. multimap中的元素默认将key按照小于来比较
3. multimap中没有重载operator[ ]操作。
4. 使用时与map包含的头文件相同。
void test_multimap()
{
map<string, string> dict;
dict.insert(make_pair("sort", "排序"));
dict["insert"];
dict["insert"] = "插入";
dict["left"] = "左边";
dict["left"] = "左边";
for (const auto& kv : dict)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << dict.size() << endl;
multimap<string, string> mdict;
mdict.insert(make_pair("sort", "排序"));
mdict.insert(make_pair("right", "右边"));
mdict.insert(make_pair("right", "正确"));
mdict.insert(make_pair("right", "右边")); // 正常插入,不管key值
for (const auto& kv : mdict)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << mdict.size() << endl;
}
int main()
{
//test_set();
//test_multiset();
//test_map1();
//test_map2();
test_multimap();
return 0;
}
4. 笔试OJ题
set和map在很多统计次数的OJ中都能用,这里先写两道:
692. 前K个高频单词 - 力扣(LeetCode)
难度中等
给定一个单词列表 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 次。
注意:
1 <= words.length <= 500
1 <= words[i] <= 10
words[i]
由小写英文字母组成。k
的取值范围是[1, 不同 words[i] 的数量]
进阶:尝试以 O(n log k)
时间复杂度和 O(n)
空间复杂度解决。
class Solution {
public:
vector<string> topKFrequent(vector<string>& words, int k) {
}
};
priority_queue解析代码:
这道题是topk问题和统计次数的融合,就是先排次数(val)多的k个,次数一样多的按key排降序,
map里面是不管key的,这样我们就要写一个排序的仿函数了,先用优先级队列(堆)写一写:
优先级队列默认大的优先级高,传的是 less 仿函数,底层是一个大堆;
如果想控制小的优先级高,需手动传 greater 仿函数,其底层是一个小堆。
我们要弄一个大堆,大堆key一样的时候,按val弄一个小堆:
class Solution {
public:
struct Less
{
bool operator()(const pair<string,int>& kv1,const pair<string,int>& kv2) const
{
if(kv1.second < kv2.second) // second(int)升序,小的在前面就ture
{
return true;
}
if(kv1.second == kv2.second && kv1.first > kv2.first) // first(string)降序
{
return true;
}
return false;
}
};
vector<string> topKFrequent(vector<string>& words, int k) {
map<string,int> countMap;
for(const auto& str : words) // 统计次数
{
countMap[str]++;
}
/*priority_queue<pair<string,int>,vector<pair<string,int>>,Less> MaxHeap;
for(const auto& kv : countMap) // 也可以迭代器区间初始化,就是太长了,长就typedef
{
MaxHeap.push(kv);
}*/
typedef priority_queue<pair<string,int>,vector<pair<string,int>>,Less> MaxHeapType;
MaxHeapType MaxHeap(countMap.begin(),countMap.end());
vector<string> v;
while(k--)
{
v.push_back(MaxHeap.top().first);
MaxHeap.pop();
}
return v;
}
};
sort解析代码:
在上面的基础上想想,如果我们用一个稳定的排序来排序string,是不是就能解决?
虽然sort不能排序map,但是可以把map转移到vector里然后在sort,直接sort是不稳定的,
但我们可以控制仿函数来间接控制:
(下面几种方法已经不是为了解题了,只是为了熟悉各个方法的使用,
因为priority的仿函数是反过来的,这里只需改下仿函数的大于小于号,把Less改成Great:)
class Solution {
public:
struct Great
{
bool operator()(const pair<string,int>& kv1,const pair<string,int>& kv2) const
{
if(kv1.second > kv2.second)
{
return true;
}
if(kv1.second == kv2.second && kv1.first < kv2.first)
{
return true;
}
return false;
}
};
vector<string> topKFrequent(vector<string>& words, int k) {
map<string,int> countMap;
for(const auto& str : words) // 统计次数
{
countMap[str]++;
}
vector<pair<string,int>> sortV(countMap.begin(),countMap.end());
sort(sortV.begin(),sortV.end(),Great());
vector<string> v;
for(int i = 0; i < k; ++i)
{
v.push_back(sortV[i].first);
}
return v;
}
};
stable_sort解析代码:
幸运的是algorithm里面有一个stable_sort,它是基于归并排序实现的,是稳定的,也就是仿函数里少写了一段:(下面代码stable_sort换成sort就不行)
class Solution {
public:
struct Great
{
bool operator()(const pair<string,int>& kv1,const pair<string,int>& kv2) const
{
if(kv1.second > kv2.second) // 次数大的在前面
{
return true;
}
/*if(kv1.second == kv2.second && kv1.first < kv2.first)
{
return true;
}*/
return false;
}
};
vector<string> topKFrequent(vector<string>& words, int k) {
map<string,int> countMap;
for(const auto& str : words) // 统计次数
{
countMap[str]++;
}
vector<pair<string,int>> sortV(countMap.begin(),countMap.end());
stable_sort(sortV.begin(),sortV.end(),Great());
vector<string> v;
for(int i = 0; i < k; ++i)
{
v.push_back(sortV[i].first);
}
return v;
}
};
multimap解析代码:
这里可以用multimap 代替stable_sort 以用来排序,可以直接用库里的仿函数:
class Solution {
public:
vector<string> topKFrequent(vector<string>& words, int k) {
map<string, int> countMap;
for(const auto& str : words) // 统计次数
{
countMap[str]++;
}
multimap<int, string, greater<int>> sortMap;
for(const auto& kv : countMap)
{
sortMap.insert(make_pair(kv.second,kv.first));
}
vector<string> v;
multimap<int, string, greater<int>>::iterator it = sortMap.begin();
for(int i = 0; i < k; ++i)
{
v.push_back(it->second);
++it;
}
return v;
}
};
349. 两个数组的交集 - 力扣(LeetCode)
难度简单
给定两个数组 nums1
和 nums2
,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2] 输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4] 输出:[9,4] 解释:[4,9] 也是可通过的
提示:
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 1000
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
}
};
set解析代码:
如果两个数组是有序的,则可以使用双指针的方法得到两个数组的交集。
这里还需要去重,(库里还有一个去重的函数unique,加上sort也可以解决这题)
unique是不改变size的,所以很麻烦,用set(排序+去重)就是很好的选择:
两个指针分别指向两个数组的头部。每次比较两个指针指向的两个数组中的数字,
如果两个数字不相等,则将指向较小数字的指针右移一位,如果两个数字相等,
输出到vector,然后两个指针右移。当至少有一个指针超出数组范围时,遍历结束。
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());
set<int>::iterator it1 = s1.begin();
auto it2 = s2.begin();
vector<int> v;
while(it1 != s1.end() && it2 != s2.end())
{
if(*it1 > *it2)
{
++it2;
}
else if(*it1 < *it2)
{
++it1;
}
else
{
v.push_back(*it1); // 相等,进哪个都行
++it1;
++it2;
}
}
return v;
}
};
单词识别_牛客题霸_牛客网 (nowcoder.com)
中等 通过率:22.37% 时间限制:1秒 空间限制:32M
知识点 字符串 哈希
描述
输入一个英文句子,把句子中的单词(不区分大小写)按出现次数按从多到少把单词和次数在屏幕上输出来,次数一样的按照单词小写的字典序排序输出,要求能识别英文单词和句号。
输入描述:
输入为一行,由若干个单词和句号组成
输出描述:
输出格式参见样例。
示例1
输入:
A blockhouse is a small castle that has four openings through which to shoot.
输出:
a:2 blockhouse:1 castle:1 four:1 has:1 is:1 openings:1 shoot:1 small:1 that:1 through:1 to:1 which:1
#include <iostream>
using namespace std;
int main() {
int a, b;
while (cin >> a >> b) { // 注意 while 处理多个 case
cout << a + b << endl;
}
}
// 64 位输出请用 printf("%lld")
set解析代码:
1. 遍历语句字符串,从语句字符串中分离出单词
2. 每分离出一个单词,就将该单词插入到map中,map会自动统计该单词出现的次数
3. 将统计到的结果按照要求排序
4. 输出
注意:1. 统计单词时,要将单词的大小写统一,因为题目说不区分大小写,注意循环输入
2. 利用set将统计的结果按照次数由大到小排序,如果次数一样按照字典序排序
3. 输出排序之后的结果
#include <iostream>
using namespace std;
#include <map>
#include <set>
class Compare
{
public:
bool operator()(const pair<string,int>& left, const pair<string,int>& right)
{
// 次数从大到小排序,如果次数相同,再按照单词字典序排序
return left.second > right.second || left.first < right.first;
}
};
int main()
{
string s;
while(getline(cin, s))
{
map<string, int> m;
string temp;
// 分割单词,采用map统计每个单词出现的次数
for(size_t i = 0; i < s.size(); ++i)
{
if(s[i] == ' ' || s[i] == '.')
{
if(temp != "")// 一个单词解析结束
{
m[temp]++;
}
temp = "";
}
else
{
temp += tolower(s[i]); // 题目说不区分大小写
}
}
set<pair<string,int>, Compare> s;
// 将map中的<单词,次数>放到set中,并按照次数升序,次数相同按照字典序规则排序
for(auto& e : m)
{
s.insert(e);
}
for(auto& e : s) // 将统计到的结果按照要求输出
{
cout<<e.first<<":"<<e.second<<endl;
}
}
return 0;
}
multimap解析代码:
#include <iostream>
#include <string>
#include <map>
#include <functional>
using namespace std;
int main()
{
string str;
map<string, int> countMap;
while(getline(cin,str))
{
for(int i=0,j=0;i<str.size();i++)
{
if(str[i]==' '||str[i]=='.')
{
string t=str.substr(j,i-j);
if(isupper(t[0]))
{
t[0]=tolower(t[0]);
}
j=i+1;
countMap[t]++;
}
}
}
multimap<int, string, greater<int>> sortMap;
for(const auto& kv : countMap)
{
sortMap.insert(make_pair(kv.second,kv.first));
}
for(const auto& kv : sortMap)
{
cout << kv.second << ":" << kv.first << endl;
}
return 0;
}
5. 笔试选择题
1. 下列说法正确的是()
A.set中的某个元素值不能被直接修改
B.map和unordered_map都是C++11提供的关联式容器
C.因为map和set的底层数据结构相同,因此在实现时set底层实际存储的是<key, key>的键值对
D.map和multimap中都重载了[]运算符
2.下面关于set的说法正确的是()
A.set中一定不能存储键值对,只能存储key
B.set可以将序列中重复性的元素去除掉
C.set中不能存储对象,因为对象字段较多,没有办法比较
D.set默认是升序,因为其默认是按照大于的方式比较的
3. 下面关于map的说法正确的是()
A.map的查询效率是O(log_2N),因为其底层使用的是二叉搜索树
B.map的key和value的类型可以相同
C.map中的有序只能是升序,不能是降序
D.map中的key可以直接修改
4. 下面关于map和set说法错误的是()
A.map中存储的是键值对,set中只储存了key
B.map和set查询的时间复杂度都是O(log_2N)
C.map和set都重载了[]运算符
D.map和set底层都是使用红黑树实现的
答案:
1. A
A:正确,因为set要保证其有序,因此set中元素不能被直接修改,若要修改可以先删除,在插入
B:错误,map是C++98中已存在的,unordered_map是C++11中才有的
C:错误,map和set底层结构都是红黑树,而其底层红黑树在实现时并没有区分是存k模型还是KV 模型
D:错误,map中key是唯一的,每个key都有与之对应的value,经常需要通过key获取value,因此 map单重载了[ ]运算符, multimap中key是可以重复的,如果重载了[ ]运算符,给定 一个key时,就没有办法返回 value了,因此,multimap中没有重载[ ]运算符
2. B
A:错误,set中可以存储键值对,实例化set时,将set中元素类型设置为pair即可
B:正确,因为set中的key是不能重复的
C:错误,set中任意类型元素都可以存储,存储对象时,需要用户提供比较规则
D:错误,set默认是升序,正确,但是其内部默认不是按照大于比较,而是按照小于比较
3. B
A:错误,map的查询效率是O(log_2N)是正确的,但map的底层结构不是二叉搜索树,而是红黑树
B:正确,key和value的类型由用户自己设置,可以相同也可以不同,取决于应用场景需要
C:错误,map可以是升序,也可是降序,默认情况下是升序,如果需要降序,需要用户在实例化 map时指定比较规则
D:错误,map中key不能修改,因为如果修改了就不能保证红黑树的特性了,即有序
4. C
A:正确,map和set的概念
B:正确,因map和set的底层结构都是红黑树,而红黑树是近似的平衡二叉搜索树,故查询时间 复杂度为O(log_2N)
C:错误,map中重载了[ ]运算符,因为其需要通过key获取value,set中没有
D:正确
本篇完。
下一部分:AVL树的介绍和模拟实现,然后是红黑树。再就是set和map的模拟实现。
穿越回来复习顺便贴个下篇链接: