作者:清风玉骨
专栏:C++进阶
昨夜松边醉倒,问松我醉何如。只疑松动要来扶,以手推曰:去! 《西江月·遣兴》
目录
Operations:lower_bound & upper_bound
关联式容器
在前面,我们已经接触过STL中的部分容器,比如:vector、list、deque、forward_list(C++11)等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面存储的是元素本身。
关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key, value>结构的键值对,在数据检索时比序列式容器效率更高。
键值对
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。
比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。
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中查找某个元素,时间复杂度为:logN
7. set中的元素不允许修改(为什么?)解释:
set 容器内的元素会被自动排序,set 与 map 不同,set 中的元素即是键值又是实值,set 不允许两个元素有相同的键值。
不能通过 set 的迭代器去修改 set 元素,原因是修改元素会破坏 set 组织。
8. set中的底层使用二叉搜索树(红黑树)来实现
时间复杂度
set作为一个检索容器来说,是非常厉害的,它的检索速度可以达到 logN 的恐怖阶段。可能有些人对这时间复杂度不太敏感,打一个比方,在十亿(大约2的三十次方)的数据中查找到某一个数据,我们只需要大概30次就可以找到!也就是说假如有十亿的人口通过该容器,利用身份证号码去找到具体某一个人的话,只需要30次!
类模板
成员函数
观看下面的图,我们发现有种似曾相识的感觉,通过前面几个容器的学习,我们大致知道有什么成员函数以及如何使用,至于最下方的几个在实际使用中较少出现,所以这里就不再详细介绍了,简单介绍一些函数
set - C++ Reference (cplusplus.com) -- 具体内容参考该链接
Modifiers: insert -- 插入
我们发现它是支持插入一段区间的(迭代器区间),不过哦,这个插入方式,谨慎使用,容易破坏树的结构,至于为什么,可以联想搜索二叉树的原理
iterator -- 迭代器
可以根据上图知晓迭代器的使用,当然其他的迭代器就不一一介绍了,前面的容器介绍过很多次
可以使用范围for进行遍历,范围for的本质是利用迭代器直接替换的,所以通过这一点可以解释为什么set可以使用范围for,同理其他的容器也是如此,可以根据这一点判断是否支持范围for的使用。
Modifiers:erase
也可以直接使用erase直接删除,不过这样是无法知道它是否删除了没,我们得用另外的方法观察,可以看到是有返回值的
Operations:find
没找到返回 end() 迭代器
有一点需要知道的是,我们也可以使用库里面实现的find,来找到我们想要的数据位置,因为库里面的定义是一个模板,传什么可以直接推出来
不过这两个的查找原理是不相同的,set时间复杂度可以达到 O(logN),而库里面实现的只能达到O(N),
库里面的实现
set里面的实现借助了数据结构的优势,所以要快上许多
Modifiers:swap
利用set里面实现的swap的代价是很小的,只需要交换根节点
Modifiers:count
很简单,容器内有就返回1,没有就返回 0。不过我们会发现相比find,count的用处并不是很突出,那么为什么会单独设置这么一个函数呢?这里可以提及set的另一个,multiset
multiset -- 链接
multiset
multiset -- 多样的set
可以支持多样元素的插入
这一个容器和set的使用是完全一样的,这里就不再赘述了,上面说的 count 在这样的一个容器中就凸显出了它的用处了
那么利用它遍历一个元素的时候找到的是哪一个呢?答案是,中序遍历的第一个元素,下图可以求证
Operations:lower_bound & upper_bound
可以找到某一区间,注意是左开右闭,因为从erase(删除处理方式为左边右开)中可以看到,不过这个使用较少
余下的就不一一介绍了,可以自行查看<set> - C++ Reference (cplusplus.com)
测试代码
#include<iostream>
#include<set>
using namespace std;
void test_set1() {
set<int> s; //注意的是set并没有提供push函数,而是insert插人式
s.insert(3);
s.insert(1);
s.insert(4);
s.insert(7);
s.insert(2);
s.insert(1);
//set的功能为 排序 + 去重
//set<int>::iterator it = s.begin(); //迭代器
auto it = s.begin();
while (it != s.end()){
cout << *it << ' ';
it++;
}
cout << endl;
//使用auto与范围for也是当然也是可以的,注意范围for在底层原理是直接替换成迭代器的
for (auto e : s) {
cout << e << ' ';
}
cout << endl;
//erase 有就删除没有也不会报错, find 字面意思
//auto pos = s.find(3);
auto pos = find(s.begin(),s.end(),3); //使用库里面的也是可以的,迭代器区间
if (pos != s.end()) {
s.erase(pos);
}
for (auto e : s) {
cout << e << ' ';
}
cout << endl;
//也可以直接使用erase直接删除,不过这样是无法知道它是否删除了没,我们得用另外的方法观察
cout<<s.erase(1)<<endl; //成功返回该值
cout<<s.erase(3)<<endl; //失败返回 0
for (auto e : s) {
cout << e << ' ';
}
cout << endl;
if (s.count(3))
{
//...
}
}
void test_set2() {
multiset<int> s; //注意的是set并没有提供push函数,而是insert插人式
s.insert(3);
s.insert(1);
s.insert(4);
s.insert(7);
s.insert(2);
s.insert(1);
s.insert(2);
s.insert(3);
s.insert(1);
s.insert(3);
s.insert(2);
s.insert(3);
s.insert(1);
//multiset的功能为 排序
//set<int>::iterator it = s.begin(); //迭代器
auto it = s.begin();
while (it != s.end()) {
cout << *it << ' ';
it++;
}
cout << endl;
//使用auto与范围for也是当然也是可以的,注意范围for在底层原理是直接替换成迭代器的
for (auto e : s) {
cout << e << ' ';
}
cout << endl;
//erase -- 有就删除没有也不会报错, find -- 字面意思
auto pos = s.find(3);
while(pos != s.end()) {
cout << *pos << " ";
++pos;
}
cout << endl;
//erase -- 有就删除没有也不会报错, find -- 字面意思
//auto pos = s.find(3);
//auto pos = find(s.begin(), s.end(), 3); //使用库里面的也是可以的,迭代器区间
//if (pos != s.end()) {
// s.erase(pos);
//}
//for (auto e : s) {
// cout << e << ' ';
//}
//cout << endl;
//cout << s.count(1) << endl; //返回元素的数量
//也可以直接使用erase直接删除,不过这样是无法知道它是否删除了没,我们得用另外的方法观察
//cout << s.erase(1) << endl; //成功返回该元素数量
//cout << s.erase(3) << endl; //失败返回 0
//for (auto e : s) {
// cout << e << ' ';
//}
//cout << endl;
}
void test_set3() {
// set::lower_bound/upper_bound
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); //返回大于等于的边界,假如是35的话30就会保留下来
itup = myset.upper_bound(60); //返回大于的边界,这里是80
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';
}
int main() {
//test_set1();
//test_set2();
test_set3();
return 0;
}
map的使用介绍
map
map - C++ Reference (cplusplus.com)
解释
key: 键值对中key的类型
T: 键值对中value的类型
Compare: 比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比
较,一般情况下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户
自己显式传递比较规则(一般情况下按照函数指针或者仿函数来传递)
Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的
空间配置器
注意:在使用map时,需要包含头文件,map是一个典型的KV结构的容器,通过它的模板可以看到一个独特例行,pair结构,下面再详细解释
文章大致翻译
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通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))
成员函数与set几乎一模一样,这里就不再详细介绍了,下面介绍几个不同之处
pair
pair - C++ Reference (cplusplus.com)
pair的构造
键值对
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量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)
{}
};
通常我们会使用first来当作key,second来当作value
Modifiers:insert
进一步简化可以写成下图
make_pair
make_pair - C++ Reference (cplusplus.com)
作用自动识别参数并返回相应的pair
实际使用例子
更简洁的写法
这里的方括号需要进一步解释,我们都知道方括号是为了支持随机访问的,但是这里的却是根据key值来进行访问的
map:operator[ ]
map::operator[] - C++ Reference (cplusplus.com)
map重载的方括号使用
实际例子
multimap
multimap - C++ Reference (cplusplus.com)
multimap与map没有什么很大的差别,参考set与multiset,不过map与multiset有几个需要注意的点,multiset是没有提供方括号的重载的,因为multiset的原理会允许它有相同key值的存在,所以没有提供该项功能,还有一点它的find找的是中序的第一个和set一样,区别是它允许键值重复
统计水果次数
测试代码
#include<iostream>
#include<map>
#include<string>
using namespace std;
int main() {
//map<string, string> dict;
//dict.insert(pair<string, string>("排序", "sort"));
//dict.insert(pair<string, string>("左边", "left"));
//dict.insert(pair<string, string>("右边", "right"));
//dict.insert(make_pair("字符串", "string"));
//dict["迭代器"] = "iterator"; //插入 + 修改
//dict["insert"]; //key不在就是插入,不在就是查找
//dict.insert(pair<string, string>("左边", "xxx"));
//dict["insert"] = "插入";//修改的作用
//cout << dict["左边"] << endl;//key不在就是插入,不在就是查找
//
//map<string, string>::iterator it = dict.begin();
//while (it != dict.end())
//{
// //cout << (*it).first<< ":"<<(*it).second << endl; //注意pair是不支持流插入的,我们得直接访问
// //按道理需要两个->不过太难看了,所以编译器就直接省略掉了一个
// cout << it->first << ":" << it->second << endl; //struct没有访问限定符,C++中有个不成文的原则
// it++; //当一个类成员都是公用,那么直接使用struct
//}
//cout << endl;
//for (const auto kv : dict) {
// cout << kv.first << ":" << kv.second << endl;
//}
//cout << endl;
// 统计水果出现的次数
//string arr[] = { "苹果", "西瓜", "香蕉", "草莓", "苹果", "西瓜", "苹果",
// "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
//map<string, int> countMap;
//for (auto e : arr) {
// map<string, int>::iterator it = countMap.find(e);
// //auto it = countMap.find(e);
// if (it == countMap.end())
// {
// countMap.insert(make_pair(e, 1));
// }
// else
// {
// it->second++;
// }
//}
//for (auto e : arr) {
// countMap[e]++; //这里的方括号需要进一个解释
//}
//for (const auto kv : countMap) {
// cout << kv.first << ":" << kv.second << endl;
//}
multimap<string, string> dict;
dict.insert(make_pair("left", "左边"));
dict.insert(make_pair("left", "剩余"));
dict.insert(make_pair("string", "字符串"));
dict.insert(make_pair("left", "xxx"));
for (const auto kv : dict) {
cout << kv.first << ":" << kv.second << endl;
}
// 统计水果出现的次数
string arr[] = { "苹果", "西瓜", "香蕉", "草莓", "苹果", "西瓜", "苹果",
"苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
multimap<string, int> countMap;
for (auto e : arr) {
multimap<string, int>::iterator it = countMap.find(e);
//auto it = countMap.find(e);
if (it == countMap.end())
{
countMap.insert(make_pair(e, 1));
}
else
{
it->second++;
}
}
for (const auto kv : countMap) {
cout << kv.first << ":" << kv.second << endl;
}
return 0;
}
在OJ中的使用
经典的稳定性与排序问题
至于为何不使用greater(可以对pair类型进行排序)进行比较,而直接自己写一个compare比较,这是基于pair的比较方式是与这题有所出路的
class Solution {
public:
//方法二:直接sort进行排序
//仿函数 -- 比较函数 需要进行改进
struct Compare{
bool operator()(const pair<int,string>& l, const pair<int, string>& r){
return (l.first > r.first) || ( (l.first == r.first) && l.second < r.second );
}
};
vector<string> topKFrequent(vector<string>& words, int k) {
//1.把数据放入map中
map<string, int> CountMap;
for(auto& str : words){
CountMap[str]++;
}
//2.因为sort算法不可以直接排序map(得是随机迭代器),其次sort算法并不是一个稳定的算法,联系快排的原理,要进行处理
vector<pair<int,string>> v;
for(auto& kv : CountMap){
v.push_back(make_pair(kv.second, kv.first));
}
//3.利用stable_sort可以解决第一个问题
//稳定的排序
//stable_sort(v.begin(), v.end(), Compare());
sort(v.begin(), v.end(), Compare());
//4.创建一个返回的值
vector<string> ret;
for(int i = 0; i<k; ++i){
ret.push_back(v[i].second);
}
return ret;
}
};
解析
比对算法逻辑
先排序 + 去重
相等就是交集值,然后同时++
否则就小的++,向后找
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());
//比对算法逻辑
//先排序 + 去重
//相等就是交集值,然后同时++
//否则就小的++,向后找
auto it1 = s1.begin();
auto it2 = s2.begin();
vector<int> ret;
while(it1 != s1.end() && it2 != s2.end()){
if(*it1 > *it2)
it2++;
else if(*it1 < *it2)
it1++;
else{
ret.push_back(*it1);
it1++;
it2++;
}
}
return ret;
}
};