序列式容器底层是用线性序列的数据结构,存储的一个元素本身,比如:vector,list,deque等。
关联式容器底层是用树形结构或者哈希结构来实现,本章讲的 set和map 底层用的是红黑树。并且,存储的是一个键值对。
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)
{}
}
可以看到 pair类中有两个 成员: T1 first,T2 second。 对,就是这样的,键值对无非就是将两个数据进行了关联,封装到一起了。
set的模板参数:
第一个 class T 是 其中键值对的 模板参数,因为set 是 key模型,所以其中键值对pair
第二个 是个仿函数,因为底层是用的 红黑树,所以就是个平衡搜索树,默认情况下是 左子树小于根节点,右子树大于根节点。set 可以 传仿函数 通过仿函数来改变默认情况,比如这里传一个 greater ,就会使得 左子树大于根节点,右子树小于根节点
第三个 是set中元素空间的管理方式,使用STL提供的空间配置器管理
例子:
#include
#include
using namespace std;
int main()
{
set<int> n; // 构造空的set
int mum[] = { 1,2,3,4,5 };
set<int>n1(mum, mum + 5); // 使用区间
set<int>n2(n1.begin(), n1.end()); //使用迭代器
set<int>n3(n2); // 拷贝构造
return 0;
}
set的底层是红黑树,那么走中序的话,就是有序的。那么set的迭代器是按照中序进行指向的吗?
可以验证一下:
int main()
{
int num[] = { 2,9,11,1,3,4,22 };
set<int>n(num, num + 7);
auto it = n.begin();
while(it != n.end())
{
cout << *it << " ";
it++;
}
cout << endl;
return 0;
}
确实迭代器走的是中序。
迭代器具体如下:
insert()函数很有讲头:
(1) 先来看第一个 重载 -> pair
set是不允许重复插入的,也就是 set中不可能有重复的值。插入一个值后,我们可以根据什么来判断是否插入成功呢?
看这个返回值 pair
这设计的其实很巧妙,一会 我们做一道题,会用到这个,而且这个功能 主要用于 map中 []
的实现,一会讲。
(2) 第二个重载 iterator insert (iterator position, const value_type& val)
这个简单,就是在指定迭代器的位置,插入一个值。返回 新插入节点的迭代器;如果插入失败,返回的是 指点迭代器的位置。
(3) 第三个重载 void insert (InputIterator first, InputIterator last)
插入 一段迭代器区间 内的数据。
删除指定迭代器位置的节点,这有个细节就是 这个迭代器必须有效,如果是无效的迭代器,那么会报错的。
比如你要删除迭代器 end()的位置,很明显不可以。
它是值删除
也就是 删除指定的值,如果删除的值不存在,也不回报错。
这里有意思的就是 它的返回值 是 size_type ,返回的什么呢?就是 具体删除 多少个节点。有点懵,无非就是 删除了 1 个或者 0个。因为,set中没有重复的数据呀。这里主要用在 multiset 中,这个可以理解成支持重复插入的set。
删除一段迭代器区间内的数据。
为什么 set中要有swap呢?如果使用 stl中的swap,是一个深拷贝,效率低。
set中的swap,就是交换一下 两个set的根节点 ,就完成了交换,效率很高。
这个cleatr() ,就是清空 set中的数据,使其变为一颗空树。
find()用于查找一个指定值,返回其位置的迭代器。如果查找的值不存在,那么返回的 迭代器 end()。
模板参数 多了个key,那么它存的键值对 就是 pair
map中存的 节点value_type 是键值对 pair
map中 key 是用于构建平衡二叉树的,也就是 key_type
map中 value (T) 是与 key 构成键值对的,一 一对应,所以可以理解为 节点的数据,mapped_type
然后仿函数这块和set类似,从默认的模板参数 less< key >,也能看出 构建树 用的是 key。
最后一个模板参数 Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器
最常用的就是 构建一个空的map
map<int, string> m;
map迭代器也走的是中序,只不过是用 key进行的 建树。
其余都和 set一样的,不赘述哈。
改
set 不支持 改,因为改了key就破坏了 树的结构。为什么 map支持改?map 改的不是key,而是与 key一 一对应的value 的值,改value的值 是不会破坏树的结构的。那么我提一个问题:value可以重复吗?答案是:可以重复,因为它的值 与树的结构没关系。
先看 pair
map<int, string> m;
//匿名对象插入
m.insert(pair<int, string>(2, "two"));
// 实名对象插入
pair<int, string> mpair(3, "three");
m.insert(mpair);
// 调用mkpair
m.insert(make_pair(1, "one"));
总的来说有以上三种 插入 pair值的方式,第一种和第三种是常用的。
make_pair()是一个函数模板,本质也是返回一个pair的匿名对象:
template <class T1,class T2>
pair<T1,T2> make_pair (T1 x, T2 y)
{
return ( pair<T1,T2>(x,y) );
}
这个insert()还是关注它的返回值,返回的是一个键值对,规则和set类似。它是 如果插入的pair的 key重复那么就不插入了,返回值 中的 iterator是key相同的节点迭代器位置,中的bool设置为 fasle;如果插入的pair的 key不重复,那么就插入,返回值中的 iterator是新插入节点的迭代器位置,中的bool设置为 true。
剩余的那两个重载就自行学习吧,完全和set类似。
这里讲讲第二个重载 size_type erase (const key_type& k);
它的参数是 key_type,是key 。可不是 value_type哦,这要注意。它的返回参数 是 删除 键值为 key节点的数目。
用于查找 键值为 k的 节点,找到了返回 此节点的迭代器。找不到 返回 迭代器 end()。
用于统计 键值为 k的节点的数目。
以上就是 map的增删查,现在来讲讲 改
。这里map设计的很巧妙。
它用的是 []
,有点意思,其实本质上 用insert()
就可以完成 改。我们先来讲讲 如何利用 insert()来进行改。
map<int, string> m;
m.insert(pair<int, string>(2, "two"));
pair<int, string> mpair(3, "three");
m.insert(mpair);
m.insert(make_pair(1, "one"));
// 修改
auto i = m.insert(pair<int, string>(2, "two"));
i.first->second = "2";
先解释一下,这个代码 无非难理解就是:
auto i = m.insert(pair<int, string>(2, "two"));
i.first->second = "2";
insert()返回的是 pair
i.first是节点的迭代器,节点的类型是 pair->
,就可以修改 T。也就是 i.first->second ,这个 second就是 T,如果想要看看 key,那么可以 i.first->first,但是这个不可以修改。
讲到这,其实insert还能做一件事,那就是查找 ,怎么查找,利用返回值呗!
map<int, string> m;
m.insert(pair<int, string>(2, "two"));
pair<int, string> mpair(3, "three");
m.insert(mpair);
m.insert(make_pair(1, "one"));
auto i = m.insert(pair<int, string>(2, "two"));
if (i.second == false)
{
cout << "key 已经存在" << endl;
}
说实在的也不算严格意义上的查找,因为 如果利用insert()查找的节点不存在,它会把这个不存在的节点插入到map中,这只是为了帮助大家理解。
好,铺垫了那么多,现在开始讲 []
的实现原理。
它的参数 是 key,返回的是什么?
(*((this->insert(make_pair(k,mapped_type()))).first)).second;
看上去很复杂,其实就是 利用的 insert的返回值。[]
的目的就是访问、修改 。key- value,中的value,也就是 mapped_type。
其实我们可以简化一下:直接 ->
,这比较易懂对吧?
举个例子吧:
map<string, string> dict;
dict.insert(make_pair("sort", "排序"));
dict.insert(make_pair("left", "左边"));
dict.insert(make_pair("left", "剩余"));
dict["left"] = "剩余"; // 修改
dict["test"]; // 插入
cout << dict["sort"] << endl; // 查找
dict["string"] = "字符串"; // 插入+修改
也就是说:map的[],可以做到 ,许多 骚操作。和序列式容器的[] 是 有区别的。
先查一下 multi 单词的意思:
昂,那么 multiset,multimap 和 set,map 的区别就在于,是否支持重复的key 插入。
先来个简单的:
vector<string> fruits = {"苹果","香蕉","香蕉","苹果","葡萄","西瓜","苹果","樱桃","橘子"};
map<string, int> conut_fruits;
for (auto& f : fruits)
{
auto i=conut_fruits.insert(make_pair(f, 1));
if (i.second== false)
{
i.first->second++;
}
}
再来看这个:
vector<string> fruits = {"苹果","香蕉","香蕉","苹果","葡萄","西瓜","苹果","樱桃","橘子"};
map<string, int> conut_fruits;
for (auto& f : fruits)
{
conut_fruits[f]++;
}
就这么简单,就可以统计水果出现的次数,因为 [] 返回的就是 value,直接 ++就好了。好好想想。
上面是统计出水果的个数,但现在要求 进行排序。
有多种解法:
首先我们可以利用库中的 sort()进行排序:
但是有个问题-> 库中排序,有没有对 pair<> 操作?没有,我们需要自己 定义 一个 仿函数 去支持我们这块的排序。
template<class T1,class T2>
struct compare_map
{
bool operator()(const pair<T1,T2>& l,const pair<T1,T2>& r)
{
return l.second > r.second;
}
};
int main()
{
vector<string> fruits = {"苹果","香蕉","香蕉","苹果","葡萄","西瓜","苹果","樱桃","橘子"};
map<string, int> conut_fruits;
for (auto& f : fruits)
{
conut_fruits[f]++;
}
vector<pair<string, int>> sort_map;
for (auto& n : conut_fruits)
{
sort_map.push_back(n);
}
sort(sort_map.begin(), sort_map.end(),compare_map<string,int>());
}
这样就完成了排序,看看效果:
上面有个效率低,低在vector,存的是 map的节点;我们可不可以 让 vector存 map的节点的指针,也就是 存迭代器,去完成 这个排序,我们来试一试:
struct i_compare_map
{
bool operator()(const map<string,int>::iterator &l,const map<string,int>::iterator &r)
{
return l->second > r->second;
}
};
int main()
{
vector<string> fruits = {"苹果","香蕉","香蕉","苹果","葡萄","西瓜","苹果","樱桃","橘子"};
map<string, int> conut_fruits;
for (auto& f : fruits)
{
conut_fruits[f]++;
}
vector<map<string, int>::iterator> i_sort_map;
auto it = conut_fruits.begin();
while (it != conut_fruits.end())
{
i_sort_map.push_back(it);
it++;
}
sort(i_sort_map.begin(), i_sort_map.end(),i_compare_map());
}
这里比较 迭代器的仿函数 我们没有支持泛型,这是迭代器 使用规则,等写篇博客好好讲一下这里,不过,以上的代码也是好懂的。
template<class T1,class T2>
struct compare_map
{
bool operator()(const pair<T1,T2>& l,const pair<T1,T2>& r)
{
return l.second < r.second;
}
};
int main()
{
vector<string> fruits = {"苹果","香蕉","香蕉","苹果","葡萄","西瓜","苹果","樱桃","橘子"};
map<string, int> conut_fruits;
for (auto& f : fruits)
{
conut_fruits[f]++;
}
priority_queue<pair<string, int>, vector<pair<string, int>>, compare_map<string, int>> pq_sort_map;
for (auto& n: conut_fruits)
{
pq_sort_map.push(n);
}
int k = 3;
while (k--)
{
pair<string, int> i = pq_sort_map.top();
pq_sort_map.pop();
cout << i.first << ":" << i.second << endl;
}
}
看一下结果:
但是有个细节啊,我那个仿函数 把 > 变成 < 了。这是 优先级队列的一个 大佬设计,我有篇博客 专门写的 优先级队列 模拟实现,可以看看。
struct i_compare_map
{
bool operator()(const map<string,int>::iterator &l,const map<string,int>::iterator &r)
{
return l->second < r->second;
}
};
int main()
{
for (auto& f : fruits)
{
conut_fruits[f]++;
}
priority_queue<map<string,int>::iterator, vector<map<string, int>::iterator>, i_compare_map> i_pq_sort_map;
auto it = conut_fruits.begin();
while (it != conut_fruits.end())
{
i_pq_sort_map.push(it);
it++;
}
int k = 3;
while (k--)
{
auto i = i_pq_sort_map.top();
i_pq_sort_map.pop();
cout << i->first << ":" << i->second << endl;
}
}
看运行结果:
以上就本期内容,有问题私信,评论;