目录
1. 关联式容器
1.1 树形结构的关联式容器
2. set的相关介绍
2.1 set的构造和迭代器
2.2 set的容量和操作函数
2.3 set使用代码
2.4 multiset使用
3. map的相关介绍
3.1 键值对
3.2 map的构造和迭代器
3.3 map的容量和操作函数
3.4 map使用代码
3.5 multimap使用
4. 笔试OJ题
692. 前K个高频单词 - 力扣(LeetCode)
priority_queue解析代码:
sort解析代码:
stable_sort解析代码:
multimap解析代码:
349. 两个数组的交集 - 力扣(LeetCode)
set解析代码:
单词识别_牛客题霸_牛客网 (nowcoder.com)
set解析代码:
multimap解析代码:
5. 笔试选择题
答案:
本章完。
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++11int 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); // 删除所有的3for (const auto& e : ms){cout << e << " ";}cout << endl;pos = ms.find(1); // 删除一个1if (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 处理多个 casecout << 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的模拟实现。