目录
1.关联式容器
在初阶阶段,我们已经接触过STL中的部分容器,比如:vector、list、deque、 forward_list(C++11)等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面存储的是元素本身。
那什么是关联式容器?它与序列式容器有什么区别? 关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<Key, Value>结构的键值对,在数据检索时比序列式容器效率更高。
2. 键值对
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量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. 树形结构的关联式容器
根据应用场景的不桶,STL总共实现了两种不同结构的管理式容器:树型结构与哈希结构。树型结构的关联式容器主要有四种:map、set、multimap、multiset。这四种容器的共同点是:使用平衡搜索树(即红黑树)作为其底层结果,容器中的元素是一个有序的序列。下面一依次介绍每一个容器。
4. set
4.1 set的介绍
- set是按照一定次序存储元素的容器
- 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。 set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
- 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序。
- set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对子集进行直接迭代。
- set在底层是用二叉搜索树(红黑树)实现的。
注意:
- 与map/multimap不同,map/multimap中存储的是真正的键值对,set中只放 value,但在底层实际存放的是由构成的键值对。
- set中插入元素时,只需要插入value即可,不需要构造键值对。
- set中的元素不可以重复(因此可以使用set进行去重)。
- 使用set的迭代器遍历set中的元素,可以得到有序序列
- set中的元素默认按照小于来比较
- set中查找某个元素,时间复杂度为:$log_2 n$
- set中的元素不允许修改(为什么?)
- set中的底层使用二叉搜索树(红黑树)来实现。
4.2 set的使用
1. set的模板参数列表
T: set中存放元素的类型,实际在底层存储的键值对
Compare:set中元素默认按照小于来比较
Alloc:set中元素空间的管理方式,使用STL提供的空间配置器管理
2. set的构造
3. set的迭代器
4. set的容量
5. set修改操作
C++11 emplace:
Construct and insert element:
构造和插入元素如果唯一,则在集合中插入一个新元素。这个新元素是使用args作为其构造的参数来就地构造的。只有当容器中没有其他元素与被插入的元素相等时,才会进行插入(set容器中的元素是唯一的)。如果插入,这将有效地将容器大小增加1。在内部,set容器按照其比较对象指定的标准对其所有元素进行排序。元素总是按照这个顺序插入到相应的位置。该元素是通过调用allocator_traits::construct来就地构造的,并将参数转发。存在一个类似的成员函数insert,它复制或移动现有对象到容器中。
Return value:
如果函数成功插入元素(因为集合中不存在等效元素),则函数返回一对指向新插入元素的迭代器,并返回值true。否则,它返回一个指向容器内等效元素的迭代器,并返回值false。成员类型迭代器是指向元素的双向迭代器类型。Pair是在<utility>中声明的类模板(参见Pair)。
6. set的使用举例
void TestSet() { // 用数组array中的元素构造set int array[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0, 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 }; set<int> s(array, array + sizeof(array) / sizeof(array)); cout << s.size() << endl; // 正向打印set中的元素,从打印结果中可以看出:set可去重 for (auto& e : s) cout << e << " "; cout << endl; // 使用迭代器逆向打印set中的元素 for (auto it = s.rbegin(); it != s.rend(); ++it) cout << *it << " "; cout << endl; // set中值为3的元素出现了几次 cout << s.count(3) << endl; } void test2() { std::set<int> myset; std::set<int>::iterator itlow, itup; for (int i = 1; i < 10; i++) myset.insert(i * 10); // 10 20 30 40 50 60 70 80 90 itlow = myset.lower_bound(30); // ^ itup = myset.upper_bound(60); // ^ myset.erase(itlow, itup); // 10 20 70 80 90 std::cout << "myset contains:"; for (std::set<int>::iterator it = myset.begin(); it != myset.end(); ++it) std::cout << ' ' << *it; std::cout << '\n'; }
5. map
5.1 map的介绍
- map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。
- 在map中,键值key通常用于排序和唯一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型 value_type绑定在一起,为其取别名称为pair:typedef pair value_type;
- 在内部,map中的元素总是按照键值key进行比较排序的。
- map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。
- map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。
- map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))。
5.2 map的使用
1. map的模板参数说明
key: 键值对中key的类型
T: 键值对中value的类型
Compare: 比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比 较,一般情况下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户 自己显式传递比较规则(一般情况下按照函数指针或者仿函数来传递)
Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的 空间配置器
注意:
在使用map时,需要包含头文件。
2. map的构造
3. map的迭代器
4. map的容量与元素访问
问题:当key不在map中时,通过operator获取对应value时会发生什么问题?
注意:在元素访问时,有一个与operator[]类似的操作at()(该函数不常用)函数,都是通过 key找到与key对应的value然后返回其引用,不同的是:当key不存在时,operator[]用默认value与key构造键值对然后插入,返回该默认value,at()函数直接抛异常。
5. map中元素的修改
6. map使用举例
void test_map1() { map<string, string> dict; dict.insert(pair<string, string>("InsertSort", "插入排序")); dict.insert(pair<string, string>("ShellSort", "希尔排序")); dict.insert(make_pair<string, string>("MergeSort", "归并排序")); dict.insert(make_pair("QuickSort", "快速排序")); for (auto& e : dict) { printf("[key - %s,value - %s]\t", e.first.c_str(), e.second.c_str()); } cout << endl; map<string,string>::iterator mapObj = dict.find("ShellSort"); if (mapObj != dict.end()) { printf("find it :: [key - %s,value - %s]\n", mapObj->first.c_str(), mapObj->second.c_str()); } auto isSuceessInsert = dict.insert(pair<string, string>("QuickSort", "快速排序")); if (isSuceessInsert.second == false) { cout << "inset fail" << endl; } else { cout << "insert success" << endl; } cout << dict["InsertSort"] << endl; } void test_map2() { string arr[] = { "香蕉","苹果","香蕉","西瓜","苹果","香蕉","苹果","梨","香蕉","梨","橘子" }; map<string, int> countFruit; for (auto& str : arr) { countFruit[str]++; } for (auto& e : countFruit) { printf("[key - %s,value - %d]\t", e.first.c_str(), e.second); } cout << endl; }
【总结】
- map中的的元素是键值对
- map中的key是唯一的,并且不能修改
- 默认按照小于的方式对key进行比较
- map中的元素如果用迭代器去遍历,可以得到一个有序的序列
- map的底层为平衡搜索树(红黑树),查找效率比较高$O(log_2 N)$
- 支持[]操作符,operator[]中实际进行插入查找。
6. multiset
6.1 multiset的介绍
- multiset是按照特定顺序存储元素的容器,其中元素是可以重复的。
- 在multiset中,元素的value也会识别它(因为multiset中本身存储的就是组成的键值对,因此value本身就是key,key就是value,类型为T). multiset元素的值不能在容器中进行修改(因为元素总是const的),但可以从容器中插入或删除。
- 在内部,multiset中的元素总是按照其内部比较规则(类型比较)所指示的特定严格弱排序准则 进行排序。
- multiset容器通过key访问单个元素的速度通常比unordered_multiset容器慢,但当使用迭代器遍历时会得到一个有序序列。
- multiset底层结构为二叉搜索树(红黑树)。
注意:
- multiset中再底层中存储的是的键值对
- mtltiset的插入接口中只需要插入即可
- 与set的区别是,multiset中的元素可以重复,set是中value是唯一的
- 使用迭代器对multiset中的元素进行遍历,可以得到有序的序列
6.2 multiset的使用
此处只简单演示set与multiset的不同
void TestSet() { int array[] = { 2, 1, 3, 9, 6, 0, 5, 8, 4, 7 }; // 注意:multiset在底层实际存储的是<int, int>的键值对 multiset<int> s(array, array + sizeof(array)/sizeof(array[0])); for (auto& e : s) cout << e << " "; cout << endl; return 0; }
7. multimap
7.1 multimap的介绍
- Multimaps是关联式容器,它按照特定的顺序,存储由key和value映射成的键值对,其中多个键值对之间的key是可以重复的。
- 在multimap中,通常按照key排序和惟一地标识元素,而映射的value存储与key关联的内 容。key和value的类型可能不同,通过multimap内部的成员类型value_type组合在一起, value_type是组合key和value的键值对: typedef pair value_type;
- 在内部,multimap中的元素总是通过其内部比较对象,按照指定的特定严格弱排序标准对 key进行排序的。
- multimap通过key访问单个元素的速度通常比unordered_multimap容器慢,但是使用迭代器直接遍历multimap中的元素可以得到关于key有序的序列。
- multimap在底层用二叉搜索树(红黑树)来实现。
注意:multimap和map的唯一不同就是:map中的key是唯一的,而multimap中key是可以 重复的。
7.2 multimap的使用
multimap中的接口可以参考map,功能都是类似的。
注意:
- multimap中的key是可以重复的。
- multimap中的元素默认将key按照小于来比较
- multimap中没有重载operator[]操作(为什么?)。
- 使用时与map包含的头文件相同:
8. 在OJ中的使用
8.1 前K个高频单词
方法1—TopK问题(使用优先级队列):
namespace std{ template<> class less<pair<string,int>>{ public: bool operator()(const pair<string,int>& kv1, const pair<string,int> kv2) const { if(kv1.second < kv2.second || (kv1.second == kv2.second && kv1.first > kv2.first)){ return true; } return false; } }; } class Solution { public: vector<string> topKFrequent(vector<string>& words, int k) { map<string,int> countMap; for(auto& e : words){ countMap[e]++; } typedef priority_queue<pair<string,int>> MaxHeap; MaxHeap mh(countMap.begin(), countMap.end()); vector<string> v; while(k--){ v.push_back(mh.top().first); mh.pop(); } return v; } };
方法2—重写仿函数,转换为vector,使用sort:
namespace std{ template<> class greater<pair<string,int>>{ public: bool operator()(const pair<string,int>& kv1, const pair<string,int> kv2) const { if(kv1.second > kv2.second || (kv1.second == kv2.second && kv1.first < kv2.first)){ return true; } return false; } }; } class Solution { public: vector<string> topKFrequent(vector<string>& words, int k) { map<string,int> countMap; for(auto& e : words){ countMap[e]++; } // typedef priority_queue<pair<string,int>> MaxHeap; // MaxHeap mh(countMap.begin(), countMap.end()); // vector<string> v; // while(k--){ // v.push_back(mh.top().first); // mh.pop(); // } vector<pair<string, int>> sortV(countMap.begin(),countMap.end()); sort(sortV.begin(),sortV.end(),greater<pair<string,int>>()); vector<string> v; for(int i = 0; i < k; ++i){ v.push_back(sortV[i].first); } return v; } };
方法3—最优使用multimap:
class Solution { public: vector<string> topKFrequent(vector<string>& words, int k) { map<string,int> dict; for(auto& e : words){ dict[e]++; } multimap<int,string,greater<int>> mdict; for(auto& e: dict){ mdict.insert(make_pair(e.second,e.first)); } auto mit = mdict.begin(); vector<string> v; while(k--){ v.push_back(mit->second); mit++; } return v; } };
8.2 两个数组的交集
方法1—使用set去重搜索:
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()); vector<int> v; auto it = s1.begin(); while(it != s1.end()){ auto fit = s2.find(*it); if(fit != s2.end()){ v.push_back(*fit); } it++; } return v; } };
方法2—算法思想,去重排序后,找交集:
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()); vector<int> v; auto it1 = s1.begin(); auto it2 = s2.begin(); //去重排序后————找交集 while(it1 != s1.end() && it2 != s2.end()){ if(*it1 < *it2){ it1++; }else if(*it1 > *it2){ it2++; }else{ v.push_back(*it1); it1++; it2++; } } return v; } };
9. 底层结构
前面对map/multimap/set/multiset进行了简单的介绍,在其文档介绍中发现,这几个容器有个 共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此 map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。