STL容器 —— map和set使用手册

文章目录

    • 1. 关联式容器
    • 2. set的学习
      • 2.1 set的构造函数
      • 2.2 set的迭代器
      • 2.3 set的容量
      • 2.4 set的增删查
    • 3. map的学习
      • 3.1 map的构造函数
      • 3.2 map的迭代器
      • 3.3 map的增删查`改`
    • 4. multiset和multimap简单介绍
      • 4.1 multiset
      • 4.2 multimap
      • 4.3 总结
    • 5. 两道小题 巩固知识
      • 5.1 统计水果个数
      • 5.2 给统计出的水果进行排序
    • 6.结尾语

前言: map和set底层是用的红黑树实现的,本章不讲解如何模拟实现map和set。主要讲解如何去使用STL库中的map和set。


1. 关联式容器

序列式容器底层是用线性序列的数据结构,存储的一个元素本身,比如:vector,list,deque等。

关联式容器底层是用树形结构或者哈希结构来实现,本章讲的 set和map 底层用的是红黑树。并且,存储的是一个键值对。

  • 键值对:用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。
  • STL中定义了键值对 -> pair

STL容器 —— map和set使用手册_第1张图片

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。 对,就是这样的,键值对无非就是将两个数据进行了关联,封装到一起了。

2. set的学习

STL容器 —— map和set使用手册_第2张图片

set的模板参数:

  • 第一个 class T 是 其中键值对的 模板参数,因为set 是 key模型,所以其中键值对pair,也就是说 这个键值对中存放的是相同的两个值;

  • 第二个 是个仿函数,因为底层是用的 红黑树,所以就是个平衡搜索树,默认情况下是 左子树小于根节点,右子树大于根节点。set 可以 传仿函数 通过仿函数来改变默认情况,比如这里传一个 greater ,就会使得 左子树大于根节点,右子树小于根节点

  • 第三个 是set中元素空间的管理方式,使用STL提供的空间配置器管理

2.1 set的构造函数

STL容器 —— map和set使用手册_第3张图片

  1. set (const Compare& comp = Compare(), const Allocator& = Allocator() );
  • 构造空的set
  1. set (InputIterator first, InputIterator last, const Compare& comp = Compare(), const Allocator& = Allocator() );
  • 用[first, last)区间中的元素构造set / 利用迭代器构造set
  1. set (const set& x);
  • 这是set拷贝构造

例子:

#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;
}

2.2 set的迭代器

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;
}

在这里插入图片描述

确实迭代器走的是中序。

迭代器具体如下:

STL容器 —— map和set使用手册_第4张图片

  • begin(),end() :返回set中起始位置元素的迭代器,返回set中最后一个元素后面的迭代器
  • rbegin(),rend() :返回set第一个元素的反向迭代器,即end ;返回set最后一个元素下一个位置的反向迭代器,即begin()
  • 对于 cbegin(),cend(),c rbegin(),crend() 返回的是 const 修饰后的 迭代器。

2.3 set的容量

在这里插入图片描述

  • empty() 是 用于 判断 set是否为空:为空 返回 true ,不为空 返回 false
  • size() 是返回 set中节点的个数
  • max_size 表示 set中允许插入的最多节点个数

2.4 set的增删查

STL容器 —— map和set使用手册_第5张图片
在这里插入图片描述

  • insert()
    STL容器 —— map和set使用手册_第6张图片

insert()函数很有讲头:

(1) 先来看第一个 重载 -> pair insert (const value_type& val)

set是不允许重复插入的,也就是 set中不可能有重复的值。插入一个值后,我们可以根据什么来判断是否插入成功呢?

看这个返回值 pair 一个键值对:

  • 如果 插入的值成功,那么返回值 pair中的bool 会设置为 true,iterator 会设置成 插入位置的迭代器
  • 如果 插入的值是重复的,那么就不能插入了,那么返回值 pair中的bool 会设置为 false,iterator 会设置成 与插入值相同的迭代器的位置

这设计的其实很巧妙,一会 我们做一道题,会用到这个,而且这个功能 主要用于 map中 []的实现,一会讲。

(2) 第二个重载 iterator insert (iterator position, const value_type& val)

这个简单,就是在指定迭代器的位置,插入一个值。返回 新插入节点的迭代器;如果插入失败,返回的是 指点迭代器的位置。

(3) 第三个重载 void insert (InputIterator first, InputIterator last)

插入 一段迭代器区间 内的数据。


  • erase()

STL容器 —— map和set使用手册_第7张图片

  • void erase (iterator position);

删除指定迭代器位置的节点,这有个细节就是 这个迭代器必须有效,如果是无效的迭代器,那么会报错的。

比如你要删除迭代器 end()的位置,很明显不可以。

STL容器 —— map和set使用手册_第8张图片

  • size_type erase (const value_type& val);

它是值删除 也就是 删除指定的值,如果删除的值不存在,也不回报错。

这里有意思的就是 它的返回值 是 size_type ,返回的什么呢?就是 具体删除 多少个节点。有点懵,无非就是 删除了 1 个或者 0个。因为,set中没有重复的数据呀。这里主要用在 multiset 中,这个可以理解成支持重复插入的set。

  • void erase (iterator first, iterator last);

删除一段迭代器区间内的数据。


  • swap()

在这里插入图片描述

为什么 set中要有swap呢?如果使用 stl中的swap,是一个深拷贝,效率低。

set中的swap,就是交换一下 两个set的根节点 ,就完成了交换,效率很高。


  • clear()
    STL容器 —— map和set使用手册_第9张图片

这个cleatr() ,就是清空 set中的数据,使其变为一颗空树。


  • find()
    在这里插入图片描述

find()用于查找一个指定值,返回其位置的迭代器。如果查找的值不存在,那么返回的 迭代器 end()。


  • set()
    在这里插入图片描述
    count()用于统计 set中 指定值 的节点个数,对于set来说 无非就是 存val值的节点是 0,或者 1。它可以用于验证 set中是否存在某个值,存在返回 1,不存在返回 0。 但对于 multiset 来说,就不一定是 0,1 了。

3. map的学习

STL容器 —— map和set使用手册_第10张图片
对比着set学:

模板参数 多了个key,那么它存的键值对 就是 pair,map是key-value模型。

  • key: 键值对中key的类型
  • T: 键值对中value的类型

在这里插入图片描述
在这里插入图片描述
map中存的 节点value_type 是键值对 pair
map中 key 是用于构建平衡二叉树的,也就是 key_type
map中 value (T) 是与 key 构成键值对的,一 一对应,所以可以理解为 节点的数据,mapped_type

然后仿函数这块和set类似,从默认的模板参数 less< key >,也能看出 构建树 用的是 key。

最后一个模板参数 Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器


3.1 map的构造函数

STL容器 —— map和set使用手册_第11张图片
和set完全类似,不赘述。

最常用的就是 构建一个空的map

map<int, string> m;

3.2 map的迭代器

map迭代器也走的是中序,只不过是用 key进行的 建树。

其余都和 set一样的,不赘述哈。

3.3 map的增删查

set 不支持 改,因为改了key就破坏了 树的结构。为什么 map支持改?map 改的不是key,而是与 key一 一对应的value 的值,改value的值 是不会破坏树的结构的。那么我提一个问题:value可以重复吗?答案是:可以重复,因为它的值 与树的结构没关系。

STL容器 —— map和set使用手册_第12张图片
在这里插入图片描述
在这里插入图片描述

  • insert()
    STL容器 —— map和set使用手册_第13张图片
    注意这里插入的 value_type 是键值对,map最开始 我就讲过了。理解了这一点,其余就和set一样。

先看 pair insert ( const value_type& x )

    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类似。


  • erase()

STL容器 —— map和set使用手册_第14张图片

这里讲讲第二个重载 size_type erase (const key_type& k);

它的参数是 key_type,是key 。可不是 value_type哦,这要注意。它的返回参数 是 删除 键值为 key节点的数目。


  • swap()和clear()和set完全类似,不赘述

  • find()

在这里插入图片描述
用于查找 键值为 k的 节点,找到了返回 此节点的迭代器。找不到 返回 迭代器 end()。

  • count()
    在这里插入图片描述

用于统计 键值为 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 我用 auto i 接收完全没问题,i.first就是key为2的节点的迭代器。这里再复习一下pair,pair的 first是 T1,pair的second是 T2,这可以查文档的。

STL容器 —— map和set使用手册_第15张图片
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中,这只是为了帮助大家理解。


好,铺垫了那么多,现在开始讲 []的实现原理。

STL容器 —— map和set使用手册_第16张图片

STL容器 —— map和set使用手册_第17张图片

它的参数 是 key,返回的是什么?

(*((this->insert(make_pair(k,mapped_type()))).first)).second;

看上去很复杂,其实就是 利用的 insert的返回值。[] 的目的就是访问、修改 。key- value,中的value,也就是 mapped_type。

  • 先看这一层,就是个insert()调用,里面是个make_pair():
    有人可能会问了:如果访问的 key 不存在,那不就出错了吗?不会,如果key不存在,那这就是个插入,它会把这个存在的key插入到map中,那么 value呢?又没给value ,这个真的不用操心,人家是有默认值的。

STL容器 —— map和set使用手册_第18张图片

  • 然后再套一层:
    insert()的返回值是个pair,它的first就是 key位置的迭代器。

在这里插入图片描述

  • 继续套娃:
    对这个迭代器进行解引用,是不是就拿到了这个节点,也就是pair
    在这里插入图片描述
  • 最后一步:
    这就是拿到节点 的 mapped_type(T)。
    STL容器 —— map和set使用手册_第19张图片

其实我们可以简化一下:直接 ->,这比较易懂对吧?

在这里插入图片描述


举个例子吧:

    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的[],可以做到 ,许多 骚操作。和序列式容器的[] 是 有区别的。


4. multiset和multimap简单介绍

4.1 multiset

  1. multiset是按照特定顺序存储元素的容器,其中元素是可以重复的。
  2. 在multiset中,元素的value也会识别它(因为multiset中本身存储的就是组成的键值对,
    因此value本身就是key,key就是value,类型为T). multiset元素的值不能在容器中进行修改(因为元素
    总是const的),但可以从容器中插入或删除。
  3. 在内部,multiset中的元素总是按照其内部比较规则(类型比较)所指示的特定严格弱排序准则进行排
    序。
  4. multiset容器通过key访问单个元素的速度通常比unordered_multiset容器慢,但当使用迭代器遍历时
    会得到一个有序序列。
  5. multiset底层结构为二叉搜索树(红黑树)。

4.2 multimap

  1. Multimaps是关联式容器,它按照特定的顺序,存储由key和value映射成的键值对,其中
    多个键值对之间的key是可以重复的。
  2. 在multimap中,通常按照key排序和惟一地标识元素,而映射的value存储与key关联的内容。key和
    value的类型可能不同,通过multimap内部的成员类型value_type组合在一起,value_type是组合key
    和value的键值对:
    typedef pair value_type;
  3. 在内部,multimap中的元素总是通过其内部比较对象,按照指定的特定严格弱排序标准对key进行排序
    的。
  4. multimap通过key访问单个元素的速度通常比unordered_multimap容器慢,但是使用迭代器直接遍历
    multimap中的元素可以得到关于key有序的序列。
  5. multimap在底层用二叉搜索树(红黑树)来实现。

4.3 总结

先查一下 multi 单词的意思:

STL容器 —— map和set使用手册_第20张图片

昂,那么 multiset,multimap 和 set,map 的区别就在于,是否支持重复的key 插入。


5. 两道小题 巩固知识

5.1 统计水果个数

先来个简单的:

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,直接 ++就好了。好好想想。


5.2 给统计出的水果进行排序

上面是统计出水果的个数,但现在要求 进行排序。

有多种解法:

  1. 首先我们可以利用库中的 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>());
}

这样就完成了排序,看看效果:

STL容器 —— map和set使用手册_第21张图片


  1. 还是用 库里的sort(),但是 对上面优化一下

上面有个效率低,低在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());
}

这里比较 迭代器的仿函数 我们没有支持泛型,这是迭代器 使用规则,等写篇博客好好讲一下这里,不过,以上的代码也是好懂的。

运行结果:
STL容器 —— map和set使用手册_第22张图片


  1. 优先级队列,可以完成topK问题,比如 我要求 :排序出数量在 前 K 个 的水果:
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;
	}
}

看一下结果:

STL容器 —— map和set使用手册_第23张图片
但是有个细节啊,我那个仿函数 把 > 变成 < 了。这是 优先级队列的一个 大佬设计,我有篇博客 专门写的 优先级队列 模拟实现,可以看看。


  1. 当然 也可以 改成 优先级队列 存放 迭代器

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;
	}
}

看运行结果:

在这里插入图片描述


6.结尾语

以上就本期内容,有问题私信,评论;

你可能感兴趣的:(STL,map,set,STL,数据结构,c++)