C++进阶第二十篇——map和set(map和set的用法+multimap+multiset+map和set代码实现)

Posted 呆呆兽学编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++进阶第二十篇——map和set(map和set的用法+multimap+multiset+map和set代码实现)相关的知识,希望对你有一定的参考价值。

⭐️今天我要给大家介绍两个新的容器,它们都是关联式容器——map和set,我会先介绍它们的使用方法,然后带大家用上一篇博客中的红黑树封装出map和set。
⭐️博客代码已上传至gitee:https://gitee.com/byte-binxin/cpp-class-code

目录


🌏关联式容器

关联式容器也是用来存储数据的,与序列式容器(如vector、list等)不同的是,其里面存储的是<key, value>结构的键值对,在数据检索时比序列式容器效率更高。今天要介绍的的四种容器是树形关联式容器:map、set、multimap和multiset。它们的底层都是用红黑树来实现的,容器中的元素是一个有序的序列。

🌏键值对

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

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

一般的两种方式创建键值对对象:
第一种: pair<T1, T2>(x, y) 使用构造函数的方式构造一个匿名对象
第二种: make_pair(x, y) 是一个函数模板,其中返回的是一个pair的匿名对象

实例演示:

void test()

	// pair<T1, T2>(T1(), T2()) 通过构造函数构造一个匿名对象
	// make_pair(T1() , T2())  是一个模板函数,返回的是pair的匿名对象,用起来更方便
	pair<int, int>(1, 1);
	make_pair(1, 1);

🌏set

🌲set的介绍


总结几点:

  1. set是按照一定次序存储元素的容器
  2. 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
  3. 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序。
  4. set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对子集进行直接迭代。
  5. set在底层是用红黑树实现的。

🌲set的使用

🍯set的几个构造函数

  1. 构造函数: set (const Compare& comp = Compare(), const Allocator& =Allocator() ); 构造空的set容器
  2. 拷贝构造: set (const set& x);

🍯set的迭代器

和之前几个容器一样,有正向迭代器和反向迭代器,还有const迭代器。这里用法也和之前的类似,不过多介绍。下面会给大家演示。

🍯set的大小和容量

  1. empty: 判断set是否为空
  2. size: 返回set中元素的个数

🍯set的插入和删除

  1. insert: pair<iterator,bool> insert (const value_type& val); 插入元素,返回值是键值对,其中如果第二个参数为true,那么第一个参数是插入元素的迭代器的位置,为false的话,第一个参数就是已经存在元素的迭代器的位置
  2. erase: void erase (iterator position); 删除position位置的元素

🍯非成员函数


这里只介绍find一个。

find 查找某个元素。这里find的时间复杂度为O(logN),比算法中的find(时间复杂是O(N))更高效,所以set容器一般室友自己的find进行查找。

实例演示:
实例1 插入、删除、查找和迭代器遍历

void test_set1()

	
	set<int> s;

	s.insert(5);
	s.insert(1);
	s.insert(6);
	s.insert(3);
	s.insert(6);
	s.insert(s.begin(), 10);

	set<int>::iterator pos = s.find(15);// 底层是搜索二叉树,时间复杂度是O(logN)
	// set<int>::iterator pos = find(s.begin(), s.end(), 3);// 遍历查找,时间复杂度是O(N)
	if (pos != s.end())
	
		// cout << *pos << endl; 
		s.erase(pos);// 没有会报错
	
	//s.erase(1); // 没找到不会报错

	set<int>::iterator it = s.begin();
	while (it != s.end())
	
		cout << *it << " ";
		++it;
	
	cout << endl;

	for (auto e : s)
	
		cout << e << " ";
	
	cout << endl;

代码运行结果如下:

实例2 下面是对算法中的find和set中的find进行效率比较的小测试

void test_set2()

	srand((size_t)time(nullptr));
	set<int> s;
	for (size_t i = 0; i < 10000; ++i)
	
		s.insert(rand());
	
	cout << "个数:" << s.size() << endl;
	int begin1 = clock();
	for (auto e : s)
	
		s.find(e);
	
	int end1 = clock();

	int begin2 = clock();
	for (auto e : s)
	
		find(s.begin(), s.end(), e);
	
	int end2 = clock();

	cout << "用时1:" << end1 - begin1 << "ms" << endl;
	cout << "用时2:" << end2 - begin2 << "ms" << endl;

代码运行结果如下:

🌏map

🌲map的介绍


总结以下几点:

  1. map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。
  2. 在内部,map中的元素总是按照键值key进行比较排序的。
  3. map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。
  4. map支持下标访问符,支持operator[],即在[]中放入key,就可以找到与key对应的value。
  5. map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))

🌲map的用法

🍯map的几个构造函数

  1. 构造函数: map() 构造一个空的map容器
  2. 拷贝构造: map(const map& m);

🍯map的迭代器

和set是类似的,不过多介绍,后面有实例演示。

🍯大小和容量

  1. empty 判断绒是否为空
  2. size 返回容器中元素个数

🍯插入和删除

  1. insert: pair<iterator,bool> insert (const value_type& x ); 返回的是一个键值对,和set的原理一样
  2. erase: void erase (iterator position); 在pos删除元素

🍯operator[](重点)

operator[]函数的定义如下:

mapped_type& operator[] (const key_type& k) 
 
	return (*((this->insert(make_pair(k,mapped_type()))).first)).second; 

其中,mapped_type是KV模型中V的类型,也就是返回value值得引用。我们可以对这个value进行修改。
分析:((this->insert(make_pair(k,mapped_type()))).first这是一个迭代器,迭代器指向键值对中的第二个元素就是value。所以operato[]的底层是用到了插入,同时可以对value进行修改和访问。
总结: operator[]的三个用处:插入、修改和访问。

实例演示:
实例1 用map统计水果个数,以下用了3种方式,同时还对operator的几种作用进行了说明

void test_map2()

	map<string, int> countMap;

	string fruitArray[] =  "西瓜","桃子","香蕉","桃子","苹果","西瓜", "香蕉","苹果", "香蕉","西瓜","桃子", "西瓜", "西瓜","桃子",
	"桃子", "桃子", "西瓜","桃子","香蕉","桃子","苹果","西瓜" ;

	// 方法一
	//for (auto& e : fruitArray)
	//
	//	map<string, int>::iterator ret = countMap.find(e);
	//	if (ret != countMap.end())// 找到了,说明容器里有,第二个参数加1即可
	//	
	//		++ret->second;
	//	
	//	else
	//	
	//		// 没有就插入,第二个参数记为1
	//		countMap.insert(make_pair(e, 1));
	//	
	//

	// 方法二
	//for (auto& e : fruitArray)
	//
	//	
	//	// countMap无此元素,pair的第一个参数返回新的迭代器,第二个参数返回true
	//	// countMap有此元素,pair的第一个参数返回旧的迭代器,第二个参数返回false
	//	pair<map<string, int>::iterator, bool> ret = countMap.insert(make_pair(e, 1));

	//	// 插入失败,只需要++value即可 

	//	if (ret.second == false)
	//	
	//		++ret.first->second;
	//	
	//
	// 方法三
	for (auto& e : fruitArray)
	
		// mapped_type& operator[] (const key_type& k) ;
		// mapped_type& operator[] (const key_type& k)  return (*((this->insert(make_pair(k,mapped_type()))).first)).second; 
		// ((this->insert(make_pair(k,mapped_type()))).first  迭代器
		// (*( (this->insert(make_pair(k,mapped_type()))).first )).second   返回value的值的引用 operator[]的原型
		countMap[e]++;// 有插入、查找和修改的功能   返回value的值的引用 
	

	countMap["梨子"];// 插入
	countMap["梨子"] = 5;// 修改
	cout << countMap["梨子"] << endl;// 查找  一般不会用 operator[] 来进行查找,因为没找到会进行插入
	countMap["哈密瓜"] = 3;// 插入+修改

	for (auto& e : countMap)
	
		cout << e.first << ":" << e.second << endl;
	

代码运行结果如下:

实例2 测试map的插入、删除和迭代器的使用

void test_map1()

	map<int, int> m;

	// 键值对
	// pair<T1, T2>(T1(), T2()) 通过构造函数构造一个匿名对象
	// make_pair(T1() , T2())  是一个模板函数,返回的是pair的匿名对象,用起来更方便
	//m.insert(pair<int, int>(1, 1));
	m.insert(make_pair(1, 1));
	m.insert(pair<int, int>(2, 2));
	m.insert(pair<int, int>(3, 3));
	m.insert(pair<int, int>(4, 4));

	map<int, int>::iterator it = m.begin();
	while (it != m.end())
	
		// *it  返回 值得引用
		cout << (*it).first << ":" << (*it).second << endl;
		// it-> 返回 值的地址  -> 解引用访问两个元素
		// cout << it->first << ":" << it->second << endl;
		++it;
	
	// e是自定义类型,传引用防止有拷贝构造发生
	for (auto& e : m)
	
		cout << e.first << ":" << e.second << endl;
	


代码运行结果如下:

🌏multiset

🌲介绍

总结几点:

  1. multiset是按照特定顺序存储元素的容器,其中元素是可以重复的。
  2. 底层是红黑树,和set的特点基本类似,只是multiset可以存放多个相同的值。

🌲用法

与set的接口基本相似,直接上演示。
实例演示:

void test_multiset()

	multiset<int> ms;
	// multiset 和 set 的接口基本一致,multiset可以插入重复的
	ms.insert(1);
	ms.insert(5);
	ms.insert(3);
	ms.insert(2);
	ms.insert(3);

	multiset<int>::iterator pos = ms.find(3);// 返回的是第一个3
	cout << *pos << endl;
	++pos;
	cout << *pos << endl;
	++pos;
	cout << *pos << endl;
	++pos;

	for (auto e : ms)
	
		cout << e << " ";
	
	cout << endl;

代码运行结果如下:

🌏multimap

🌲介绍


总结几点:

  1. multimaps是关联式容器,它按照特定的顺序,存储由key和value映射成的键值对<key, value>,其中多个键值对之间的key是可以重复的。
  2. 底层也是红黑树,和map的性质基本类似

🌲用法

实例演示:

void test_multimap()

	// multimap 和 map 的区别:可以有不同的key 
	// 不支持operator[]  因为有多个key时,不知道返回哪个key对应的value的引用
	multimap<int, int> mm;

	mm.insert(make_pair(1, 1));
	mm.insert(make_pair(1, 2));
	mm.insert(make_pair(1, 3));
	mm.insert(make_pair(2, 1));
	mm.insert(make_pair(2, 2));

	for (auto& e : mm)
	
		cout << e.first << ":" << e.second << endl;
	


代码运行结果如下:

🌏用一颗红黑树封装出map和set

🌲对红黑树进行改造

这里是我上一篇关于红黑树的博客——红黑树
这里红黑树完整代码——红黑树完整代码
大概框架:

template<class K, class V>
class RBTree

	typedef RBTreeNode<K, V> Node;
private:
	Node* _root = nullptr;
;

这里的红黑树是一个KV模型,我们要用这个红黑树同时封装出map和set两个容器,直接使用这棵红黑树显然是不行的,set属于是K模型的容器,我们要做怎样的改造才能够同时封装出这两个容器呢?
这里我们参考STL源码的处理方式,下面是源码的部分截图:

可以看出这里,红黑树的第一个类模板参数和之前是一样的,但是第二个参数value和之前是不一样的,这里的直接把value存放在节点里面,通过map和set构造红黑树可以看出value存的是pair<K, V>或K,对于map而言,value存的是pair<K, V>;对于set而言,value存的是K。所以这里的红黑树暂时可以这样改造:

template<class K, class T>
class RBTree

	typedef RBTreeNode<T> Node;// 根据T的类型判断是map还是set 可能是pair<K, V>或K
public:
private:
	Node* _root = nullptr;
;

同时,我们还会发现,上面的红黑树的类模板中有第三个参数是什么呢?
为了获取value中的key值,我们可以让map和set各自传一个仿函数过来,以便获得各自的key值。
两个仿函数如下:

template<class K, class V>
class map

	struct MAPOFV
	
		const K& operator()(const pair<K, V>& kv)
		
			return kv.first;
		
	;
;
template<class K>
class set

	struct SETOFV
	
		const K& operator()(const K& key)
		
			return key;
		
	;
;

第四个类模板参数是一个空间配置器,这里也不实现了,我们实现主体内容即可。后面会介绍空间配置器相关内容。
迭代器的实现
其中operato++就是通过非递归中序遍历的方式走一遍红黑树,走到空就结束

template<class T, class Ptr, class Ref>
struct __rbtree_iterator

	typedef __rbtree_iterator<T, Ptr, Ref> Self;
	typedef RBTreeNode<T> Node;

	Node* _node;

	__rbtree_iterator(Node* node)
		:_node(node)
	

	// 返回值(data)的地址
	Ptr operator->()
	
		return &_node->_data;
	
	// 返回值(data)的引用
	Ref operator*()
	
		return _node->_data;
	
	Self& operator++()
	
		// 1.先判断右子树是否为空,不为空就去右子树找最左节点
		// 2.右子树为空,去找孩子是其左孩子的祖先
		Node* cur = _node;
		if (cur->_right)
		
			cur = cur->_right;
			while (cur->_left)
			
				cur = cur->_left;
			
			
		
		else
		
			Node* parent = cur->_parent;
			while (parent && parent->_right == cur)
			
				cur = parent;
				parent = parent->_parent;
			
			cur = parent;
		
		_node = cur;
		return *this;
	
	Self& operator--()
	
		// 1.先判断左子树是否为空,不为空就去左子树找最右节点
		// 2.右子树为空,去找孩子是其右孩子的祖先
		Node* cur = _node;
		if (cur->_left)
		
			cur = cur->_left;
			while (cur->_right)
			
				cur = curC++从入门到入土第二十篇:关联式容器-map和set

C++从入门到入土第二十篇:关联式容器-map和set

C++从青铜到王者第二十篇:STL之setmapmultisetmultimap的初识

C++进阶第二十二篇——unordered_map和unordered_set(容器接口介绍和使用+底层代码实现)

C++进阶map和set模拟实现

C++进阶map和set模拟实现