前言
我们已经对二叉搜索树进行了介绍了并进行了实现。本期我们来学习一下树形的序列化容器例如map、set等。
本期内容介绍
关联式容器和pair的介绍
set和multiset
map和multimap
关联式容器和键值对的介绍
在C++初阶的时候我们已经对STL的部分容器进行了介绍以及模拟实现。例如:vector、list等,这些容器本身统称为序列式容器,原因是他们的底层为线性序列的数据结构,里面存储的是元素本身。那什么是关联式容器呢?他和序列化容器有什么区别呢?
关联式容器也是用来存储数据的,与序列化容器不同的是,里面存的不只是元素本身,而是存的是一个和元素本身有某个关系的键值对即<key, value>的结构,在数据检索时比序列式容器更高效!
OK,这里谈到了键值对,我们虽然以前用过但是没有介绍过,这里就介绍一下~!
键值对
用来表示具有一一对应关系的一种结构,该结构中一般包含两个成员变量key和val,key表示键值,val表示和key对应的某种信息!例如我们上期在二叉搜索树最后介绍的一个栗子英文单词对应一个汉语、你对应你的学号等,这些都是键值对!C++中标准库提供了一个键值对结构的模板类即pair!OK,我们来看看STL源码(参考的SGI版本)如何定义的:
除去条件编译暂时不看,它的成员变量其实很简单,一个first是key值和一个second是val值。OK,这里暂时不演示它的使用后面set和map中会用到一起演示~!
树形结构的关联容器
根据应用场景不同,STL总共实现了两种不同的结构式的管理容器:树形结构与哈希结构。树形结构的关联式容器有四种:set、multiset、map、multimap。这四种容器的共同点是:使用平衡搜索树(红黑树)作为底层的实现的,容器中的元素是有序的序列,哈希结构的unordered_set和unordered_map是无序的(后面哈希那一期专门介绍)。OK,下面我们就来依次的学习这几个容器!
set
OK,还是老样子,先来看看文档!
这里的set和multiset都在set的头文件中!为了更好的理解文档,我们有必要先对他的内部类型进行了解!
可以清楚的看到key和val的类型都是T,实际上set的底层存的不只是单个元素而是<K,K>的键值对!具体后面底层在详细介绍!迭代器不再多说了,size_type就是size_t。
第一个模版参数T是存储元素的类型,后面的compare是元素的存储顺序的比较规则!在后面的空间配置器暂时不谈,后期介绍!
1、set中存出的元素是按照cmp规则且不重复的
2、set中的元素不可以修改,可以允许插入、删除操作
3、set可以用迭代器访问,并且访问出来是有序的
4、set插入时,不需要键值对,只需要插入K即可(虽然底层是<K, K>的键值对)
5、set的底层是红黑树实现的
构造、拷贝构造、赋值拷贝、析构
当然也可以用我们以前介绍的C++11提供的初始化序列进行构造:
set<int> s1;//空构造
vector<char> v = { 'a', 'b', 'c', 'd' };
set<char> s2(v.begin(), v.end());//迭代器区间构造
set<int> s3(s1);//拷贝构造
set<int> s4 = { 1,4,2,5,7,3,8 };//初始化序列对象构造
OK,我们下面来看看赋值拷贝:
s1 = s3;//赋值拷贝
析构还是一样,清理资源,释放空间!
迭代器
正向
正向又分为正向const和正向非const的:
OK,来演示一下:
void test_set2()
{
set<int> s = { 1,4,2,5,7,3,8,1,2,4 };//初始化序列对象构造
set<int>::iterator it = s.begin();//非const迭代器
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
const set<int> s1 = { 2,4,7,2,67,7,2,8,1,9,0 };
set<int>::const_iterator cit = s1.begin();//const迭代器
while (cit != s1.end())
{
cout << *cit << " ";
++cit;
}
cout << endl;
}
既然支持迭代器那必然支持范围for,我们前面就多次介绍了范围for本质就是替换成迭代器!这里就不在演示了~!另外,const和非const得区别主要是权限的问题,入过不修改建议使用const的!
上面的结果我们发现用迭代器遍历出来是有序(升序)的,原因是set底层是二叉搜索树(红黑树)!
反向
容量
empty
size
set<int> s = { 1,4,2,5,7,3,8,1,2,4 };
bool IsEmp = s.empty();
cout << IsEmp << endl;
size_t sz = s.size();
cout << sz << endl;
修改
注意:上面说的是set的元素不可被修改,但是没说过set不可被修改!!!这里的修改是对set的修改,不是对元素的修改!!!
insert
//遍历打印
void print(const set<int>& s)
{
for (const auto& e : s)
{
cout << e << " ";
}
cout << endl;
}
//演示修改
void test_set5()
{
set<int> s = { 1,2,3,4,5,6 };
print(s);
//插入一个元素
s.insert(0);
print(s);
//在end位置插入一个元素
s.insert(s.end(), 10);
print(s);
//插入一段迭代器区间
vector<int> nums = { 100, 200, 300 };
s.insert(nums.begin(), nums.end());
print(s);
}
这里的第一个,插入一个val:
pair<iterator, bool> insert(const value_type& val);
注意:他的返回值,他是返回了一个pair里面的first表示插入元素的位置的迭代器,second是代表是否插入成功!至为什么这么做底层再介绍~!
第二个pos位置也不是就在pos位置插入了,而是从pos位置开始搜索插入位置!!找到了插入并返回插入后的位置,否则返回已经存的val的位置
erase
注意第二个:他有个返回值,返回的是删除元素的个数。如果被删除的元素val存在,则删除返回1,否则返回0!
swap
这个swap和我们前面容器中介绍的一样,进行了对底层指针的直接交换!
set<int> s = { 1,2,3,4,5,6 };
print(s);
set<int> s1 = { 10,20,30,40,50,60 };
print(s1);
s.swap(s1);
print(s);
print(s1);
clear
set<int> s = { 1,2,3,4,5,6 };
print(s);
cout << s.size() << endl;
s.clear();//清空set
print(s);
cout << s.size() << endl;
其他操作
lower_bound
返回大于等于val 的第一个元素的迭代器
upper_bound
返回大于val 的第一个元素的迭代器
OK,这两个放在一起演示,顺便再来演示一下迭代器区间的删除:
set<int> s = { 1,2,3,4,5,6,7,8,9,10 };
print(s);
set<int>::iterator start = s.lower_bound(2);// >=2的第一个位置,这里的位置是2的位置
set<int>::iterator end = s.upper_bound(7);//>7的第一个位置,也就是这里的位置是8的位置
s.erase(start, end);//迭代器区间的删除
print(s);
find
查找值为val的元素,找到了返回val位置的迭代器,找不到返回end
set<int> s = { 1,2,3,4,5,6,7,8,9,10 };
auto ret1 = s.find(3);
if (ret1 != s.end())
cout << *ret1 << endl;
else
cout << "没找到哦~" << endl;
count
返回val出现的次数,因为set的元素不可重复所以有返回1,没有返回0
set在OJ中的简单使用
OK,set就介绍到这里!下面我们做个题目练一下:
思路:先把第一个数组的元素放到set中,然后遍历第二个数组,如果第二个数组的元素在set中则就说明当前元素就是交集,插入到返回数组即可,并且把当前元素从set中删除防止重复统计!
代码实现:
multiset
multiset和set头文件的都是set,他两的唯一区别是:multiset可以重复,而set是不能重复!
其他的接口包括使用和set都一模一样!这里就不在一一介绍了
演示一下,可以存在重复的元素:
//演示multiset
void test_set9()
{
multiset<int> ms = { 1,1,2,3,3,4,5,6,7,7,8 };
for (auto& e : ms)
{
cout << e << " ";
}
}
OK,关于set和multiset就介绍到这里,下面我们进入到map的介绍!
map
map和上面的set一样,也有一个mulimap,他们的头文件都是map
在正是的看文档前还是先来看看相关的成员类,便于后续的学习和理解:
key_type即Key是第一个参数的类型,mapped_type即T是key相关联的值的类型。size_type就是size_t
OK,我们先来介绍map,还是一样看看文档:
map是关联式容器,存储形式是一个key和一个与key相关联的val的组合。第一个参数key表示存储的标识,T是key对应要存储的值val
1、map存储的是两个值,一个是key(标识)一个是val(key对应的值)
2、map的不可修改key值,可以修改val,可以进行插入和删除的操作
3、map中的key不可重复,val可以重复
4、map中key和val 的类型可以不用相同
5、map中的存储(插入)的key和val组合的键值对就是上面介绍的pair<Key,T>
6、map可以使用操作符[]访问元素
7、map的底层是红黑树实现的
构造、拷贝构造、赋值拷贝、析构
map<int, int> m1;//空构造
map<int, string> m2;//key和val的类型可以不同
vector<pair<int, int>> nums = { {1,2}, {2,3}, {3,4} };
map<int, int> m3(nums.begin(), nums.end());//迭代器区间构造
map<int, string> m4(m2);//拷贝构造
map<int,int> m5 = { {1,2}, {2,3}, {3,4} };//初始化序列对象构造
这里C++11还提供了用初始化序列对象赋值拷贝的:
m3 = m1;//赋值拷贝
m1 = { {1,2},{2,2},{3,3} };//初始化序列对象赋值拷贝
这里一定要和前面介绍的隐式类型转换区分清楚!隐式类型转化是上面初始化序列中的一个个{k,v}转换成pair,是先转换成为一个个的临时的pair类型的对象,然后用临时的pair对象去初始化pair的实际对象。这里是用初始化序列对象去拷贝或赋值,初始化序列是有类型的!!
析构就不多说了,和以前的一样!
迭代器
这个就不在详细一一介绍了上面的set一起以前的容器都介绍过多次了,和以前的用法一模一样!!这里就直接用一下:
map<int, int> m = { {1,2},{2,2},{3,3} ,{4,4} };
map<int, int>::iterator it = m.begin();//正向迭代器
while (it != m.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
for (auto& e : m)//范围for遍历
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
const map<int, int> cm = { {1,8},{2,2},{3,3} ,{4,4} };
map<int, int>::const_iterator cit = cm.begin();
while (cit != cm.end())
{
cout << cit->first << ":" << cit->second << endl;
++cit;
}
cout << endl;
容量
empty
判断map是否为空
size
获取map中有效元素的个数
map<int, int> m = { {1,2},{2,2},{3,3} ,{4,4} };
bool emp = m.empty();
cout << "empty: " << emp << endl;
size_t sz = m.size();
cout << "sz: " << sz << endl;
修改
insert
1、插入一个pair<k,v>,返回一个pair<iterator,bool>,如果插入成功pair的first指向被插入元素的位置,second就是true。插入失败(k存在),first指向和k相等的元素的位置,second被设置为false;
2、在迭代器pos位置开始搜索插入位置,找到了插入一个pair<k,v>,插入成功返回新插入元素位置的迭代器,否则返回和k相等的元素位置的迭代器;
3、插入一段迭代器区间
4、插入一个初始化序列的对象
map<int, int> m;
pair<int, int> pi(3, 5);
m.insert(pi);//插入一个pair
m.insert(make_pair(1, 3));//插入make_pair
m.insert(pair<int, int>(5, 8));//插入pair的匿名对象
m.insert({ 2,2 });//隐式转化
m.insert({ {1,2},{2,2},{3,3} ,{4,4} });//插入一个初始化序列的对象
这理注意的是有个make_pair这是啥呢?其实这里是为了写起来方便整的一个函数模版:
就是不想写第一个和第三个,就写了一个模板让编译器生成~!当然我们可以用他,当然隐式类型转换也是很好用的!
erase
void print(map<int, int>& m)
{
for (auto& e : m)
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
}
void test_map5()
{
map<int, int> m;
m.insert({ {1,2},{2,2},{3,3} ,{4,4} });//插入一个初始化序列的对象
print(m);
m.erase(m.begin());
print(m);
auto ret = m.erase(2);//返回删除元素的个数
cout << ret << endl;
print(m);
m.erase(++m.begin(), --m.end());
print(m);
}
swap
交换两个map中的内容
clear
清空map中的元素
void print(map<string, int>& m)
{
for (auto& e : m)
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
}
void test_map6()
{
map<string, int> m1 = { {"cp",1}, {"dd", 2} };
map<string, int> m2 = { {"xxxxx",2}, {"ttttt", 7} };
print(m1);
print(m2);
m1.swap(m2);
print(m1);
print(m2);
m1.clear();
print(m1);
print(m2);
}
元素访问
[]
返回key对应val的引用
如果key不存在,插入key,val就是其val类型的默认构造
下面它的解释这个函数的原型大概就是这样的:
template<class K, class V>
V& operator[](const K& key)
{
// 不管插入成功还是失败,pair中iterator始终指向key所在节点的iterator
pair<iterator, bool> ret = this->insert(make_pair(key, V()));
iterator it = ret.fisrt;
return it->second;
}
[]可以进行的操作:map<K,V> m1;
1、插入: m1[k];//插入,此时的first就是k,second就是默认构造V();
2、插入+修改:m1[k1] = v;//此时把原来key对应的val的默认构造值被v给覆盖了
3、查找:m1[key]就可以查到与k对应的值val
4、修改:m1[k1] = v2;
map<string, string> dict;
dict.insert({ "string", "字符串" });
// 插入(一般不会这么用)
dict["right"];
cout << dict["right"] << endl;
// 插入+修改
dict["left"] = "左边";
// "查找"
cout << dict["string"] << endl;
// 修改
dict["right"] = "右边";
cout << dict["right"] << endl;
at
这个和上面的[]大致一样,唯一区别是,[]插入时如果key不存在会插入,其val就是val一应的默认构造!而at是会报越界异常!
OK,演示一下不同(只是看看后面异常再详细解释):
void test_map7()
{
map<string, string> dict;
dict.insert({ "string", "字符串" });
dict["right"];//key不存在插入,其val就是val类型的默认构造
cout << dict["right"] << endl;
try
{
dict.at("left");
}
catch (exception& e)
{
cout << e.what() << endl;//打印异常信息
}
}
其他操作
find
count
lower_bound
upper_bound
因为这个和set的几乎一样,就不在多介绍了!直接用一个例子演示:
map<string, string> dict;
dict.insert({ "string", "字符串" });
// 插入+修改
dict["left"] = "左边";
cout << dict["left"] << endl;
map<string, string>::iterator ret = dict.find("left");
if (ret != dict.end())
cout << "存在" << endl;
else
cout << "不存在" << endl;
size_t cnt = dict.count("left");
cout << cnt << endl;
map<int, int> m;
m[1] = 10;
m[2] = 20;
m[3] = 30;
m[4] = 40;
map<int, int>::iterator start = m.lower_bound(2);
map<int, int>::iterator end = m.upper_bound(3);
m.erase(start, end);
print(m);//自己上面写的打印函数
map在OJ中的简单应用
思路:map + 堆
先开一个<string, int>的map,前着K表示单词,后者V表示单词的次数!然后遍历数组words把给个字符串放到map中,统计出现的次数!
然后开一个小堆,将map中的元素逐一放到小堆中,维护小堆的大小为K,最后堆中的那K个元素就是符合的!
但是这里它的规则不是完全小堆的规则,如果频次高,频次高的优先,频次相同,字典序小的优先!
返回的单词是频次由高到低的,所以可以先正这放,最后逆置,也可以一开始计算好倒着放!
代码实现:
class Solution
{
typedef pair<string, int> PSI;
struct cmp
{
bool operator()(const PSI& a, const PSI& b)
{
if(a.second == b.second)//频次相同
{
return a.first < b.first;//字典序小的优先
}
return a.second > b.second;//频次不等,高频次优先
}
};
public:
vector<string> topKFrequent(vector<string>& words, int k)
{
map<string, int> map;//key是单词val是出现的次数
for(auto& e : words)
map[e]++;//统计每个单词的次数
priority_queue<PSI, vector<PSI>, cmp> minHeap;//前k大,所以建小堆
for(auto& e : map)
{
string x = e.first;//取key即字符串
int y = e.second;//取key对应的val即单词次数
minHeap.push({x, y});//插入到小堆
if(minHeap.size() > k)
{
minHeap.pop();//堆的元素超过k个则删除,维护永远是k个
}
}
vector<string> ret(k);//返回结果的数组
int i = k-1;
while (minHeap.size())
{
PSI top = minHeap.top();//去除
ret[i--] = top.first;//放到返回数组的合适位置(高频次在前,因为是小堆倒着放)
minHeap.pop();//删除当前堆顶的元素
}
return ret;//返回
}
};
multimap
multimap和map的唯一区别就是multimap的key是可以重复的,而map的key是不可重复的!因为multimap的key是可以重复的,所以他不在支持用[]了
这里还是只演示不同:
void test_map_and_multimap()
{
map<int, int> m;
m[1] = 10;
m[2] = 20;
m[3] = 30;
m[4] = 190;
m[4] = 80;
m[4] = 40;
for (auto& e : m)
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
multimap<int, int> mm;
mm.insert({ 1,1 });
mm.insert({ 1,1 });
mm.insert({ 2,2 });
mm.insert({ 2,2 });
for (auto& e : mm)
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
}
OK,其他接口如下和map的使用一模一样,这里不在介绍了!
OK!本期分享就到这里,好兄弟!我们下期再见!!
结束语:信念与我同在!