STL模板 map set的使用和模拟实现

news2024/11/18 23:45:57

前言:

在本篇文章中,我们将为大家讲述map set这两种容器,map和set是STL容器中比较常见的两个,他们的核心在于使用了KV模型,这样更进一步让我们可以对数据进行存储,同时由于底层使用了红黑树进行封装,因此在搜索效率上得到了极大的保障,但是两者各有各的特点,下面就让我们来看一看。

关联式容器:

在STL库中,我们对于容器一般分为两类:1.序列式容器 2.关联式容器
序列式容器:
比如list,vector,string,queue,stack,deque等等,这是因为他们底层为线性序列的数据结构,里面存储的是元素本身,注意这里我要强调的东西,他们存储的是元素本身,也就是说,你存储的如果是整形那就是整形,是其他类型就是其他类型。
那什么是关联式容器呢?
关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key, value>结构的键值对,我们也称为KV模型,在数据检索时比序列式容器效率更高.
既然谈到<key,value>键值对,那这个所谓的键值对是什么呢?

键值对:

键值对的定义:

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

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就是键值对<first,second>的标志,同时键值对的本质是一个类,因此我们访问键值对的数据的时候,就可以按照访问类的成员那样访问即可。
比如:我们去超市买水果的时候,每一种水果都有其特定的标价,作为管理者来说,我们想要快速获取任意一种水果对应的标价,我们就可以利用一个键值对,将水果的名称作为检索对象key,而对应的价格就是数据value。因此通过这种存储结构,我们只需要找到对应的水果名称pair.first,就可以访问到它对应的价格pair.second。

set容器:

在STL中,数据按照管理的大体结构主要分为两种,即哈希结构和树形结构,而包括set在内的下面的容器都是树形结构。

1.set的介绍:

1. set是按照一定次序存储元素的容器
2. 在set中,元素的value也标识它(value就是key,类型为T),set的pair结构为pair<key,key>,并且每个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中的元素,可以得到有序序列,这得益于红黑树是一种平衡版本的BS树
5. set中的元素默认按照小于来比较,我们从set的仿函数模板参数也可以看到,其默认给的都是Less< T >
6. set中查找某个元素,时间复杂度为: l o g 2 n log_2 n log2n,树形结构的效率都是如此,而且这里还进行了平衡
7. set中的元素不允许修改(为什么?),而set的元素不允许修改的原因在于,set的value就是key,因此一旦对value修改很可能导致有重复的情况出现,因此set不允许修改。
8. set中的底层使用二叉搜索树(红黑树)来实现

2.set的使用:

第一部分:在这里插入图片描述

首先是第一部分,我认为这部分没什么值得说的东西,都是作为STL容器比较基本的东西,我们在后面的模拟实现,在实现迭代器的时候再重点说这部分。

第二部分:

在这里插入图片描述
在这一部分中,我们想要谈一谈的是lower_bound和upper_bound以及equal_range.
1.lower_bound是用来返回我们对应的参数的距离最近的小于key元素的迭代器的,而upper_bound则反之为返回距离最近的大于参数的key元素的迭代器的。他们的组合用法如下:

#include <iostream>
#include <set>

int main ()
{
  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);                //       ^
  itup=myset.upper_bound (60);                 //                   ^

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

  return 0;
}
//myset contains: 10 20 70 80 90

2.而对于equal_range来说
它则是返回一个键值对,其first即lower_bound,而其second即upper_bound,用法如下:

#include <iostream>
#include <set>

int main ()
{
  std::set<int> myset;

  for (int i=1; i<=5; i++) myset.insert(i*10);   // myset: 10 20 30 40 50

  std::pair<std::set<int>::const_iterator,std::set<int>::const_iterator> ret;
  ret = myset.equal_range(30);

  std::cout << "the lower bound points to: " << *ret.first << '\n';
  std::cout << "the upper bound points to: " << *ret.second << '\n';

  return 0;
}
//the lower bound points to: 30
//the upper bound points to: 40

注意,我们使用set是可以去重的,比如如下:

#include <set>
void TestSet()
{
    // 用数组array中的元素构造set
    int array[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0, 1, 3, 5, 7, 9, 2, 4, 
6, 8, 0 };
 set<int> s(array, array+sizeof(array)/sizeof(array));
 cout << s.size() << endl;
 // 正向打印set中的元素,从打印结果中可以看出:set可去重
 for (auto& e : s)
 cout << e << " ";
 cout << endl;
 // 使用迭代器逆向打印set中的元素
 for (auto it = s.rbegin(); it != s.rend(); ++it)
 cout << *it << " ";
 cout << endl;
 // set中值为3的元素出现了几次
 cout << s.count(3) << endl; }

这样我们就可以将数组中重复的元素全部去掉,同时得到一个排列顺序好的数据结构。

map容器:

map的容器底层也是红黑树,在大体的结构上它和set有很大的相似之处。

1.map的介绍:

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。这也是因为得益于key唯一且不可修改的特点,unordered_map就不能实现这个功能。
6. map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))。

2.map的使用:

在这里插入图片描述
在这里插入图片描述
在对于map的使用中,我们不需要进行过多的赘述,很多函数对应的用法和set是一样的,但注意map是真正的KV模型,key用来检索和排序,而value则存储真正的数据。
在这些函数接口中,我注意到[]运算符重载和at:
【】和at实际上是同一个意思,用法相同,只不过[]是将at的方法通过运算符重载变成我们熟悉的下表访问的形式,其用法如下:

#include <iostream>
#include <map>
#include <string>

int main ()
{
  std::map<char,std::string> mymap;

  mymap['a']="an element";
  mymap['b']="another element";
  mymap['c']=mymap['b'];

  std::cout << "mymap['a'] is " << mymap['a'] << '\n';
  std::cout << "mymap['b'] is " << mymap['b'] << '\n';
  std::cout << "mymap['c'] is " << mymap['c'] << '\n';
  std::cout << "mymap['d'] is " << mymap['d'] << '\n';

  std::cout << "mymap now contains " << mymap.size() << " elements.\n";

  return 0;
}
//mymap['a'] is an element
//mymap['b'] is another element
//mymap['c'] is another element
//mymap['d'] is
//mymap now contains 4 elements.


#include <iostream>
#include <string>
#include <map>

int main ()
{
  std::map<std::string,int> mymap = {
                { "alpha", 0 },
                { "beta", 0 },
                { "gamma", 0 } };

  mymap.at("alpha") = 10;
  mymap.at("beta") = 20;
  mymap.at("gamma") = 30;

  for (auto& x: mymap) {
    std::cout << x.first << ": " << x.second << '\n';
  }

  return 0;
}
//alpha: 10
//beta: 20
//gamma: 30

但是在这里,我们要搞清楚operator[]函数的返回值以及功能实现:
在这里插入图片描述

operator[]的原理是:
用<key, T()>构造一个键值对,然后调用insert()函数将该键值对插入到map中 如果key已经存在,插入失败,insert函数返回该key所在位置的迭代器 如果key不存在,插入成功,insert函数返回新插入元素所在位置的迭代器,operator[]函数最后将insert返回值键值对中的value返回
因此,对于operator[]的返回值,我们可以使用起来,它是会返回一个迭代器的,同时也有插入的功能,我们在一会模拟实现map的时候,模拟实现operator[]利用的就是这个关键的原理。

set map容器的模拟实现:

在完善了上述的功能之后,接下来让我们通过模拟实现set map来进一步体会使用,通过模拟实现,我们会对模板的使用封装产生更深的理解。

1.底层红黑树的构建:

这一部分我不做过多的赘述,直接上代码,有不懂的地方可以参考我数据结构专栏的链接: 红黑树。
基本的红黑树结构在那里有详解,代码如下:

enum Colour//颜色枚举体
{
	BLACK,
	RED
};
template<class T>
struct RBTreeNode//红黑树节点类
{
	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;
	Colour _col;
	T _data;
	RBTreeNode(const T& data)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _data(data)
		, _col(RED)//控制节点颜色的成员变量
	{}
};
template<class K, class T,class KeyOf>//利用一个仿函数来处理key的获取问题
class RBTree//红黑树类
{
public:
	typedef struct RBTreeNode<T> Node;
	typedef struct _Treeiterator<T,T&,T*> iterator;//迭代器
	typedef struct _Treeiterator<T,const T&,const T*> const_iterator;//const 迭代器

	iterator begin()//头位置的迭代器,即找到最左边的节点,但是注意不要找过头了,因此我们是cur&&cur->_left
	{
		Node* cur = _root;
		while (cur && cur->_left)
		{
			cur = cur->_left;
		}
		return iterator(cur);//返回迭代器类的匿名构造
	}

	iterator end()//尾部位置的迭代器,也就是空迭代器
	{
		return iterator(nullptr);//返回迭代器类的匿名构造
	}

	const_iterator begin() const
	{
		Node* cur = _root;
		while (cur && cur->_left)
		{
			cur = cur->_left;
		}
		return const_iterator(cur);
	}

	const_iterator end() const
	{
		return const_iterator(nullptr);//返回迭代器类的匿名构造
	}

	pair<Node*,bool> Insert(const T& data)//插入
	{
		if (_root == nullptr)
		{
			_root = new Node(data);
			_root->_col = BLACK;
			return make_pair(_root, true);
		}
		Node* cur = _root;
		Node* parent = nullptr;
		KeyOf key;//设置一个通用的仿函数来将对应的map和set中对应的key取出来,因为pair自己的first没法取,因此利用分别封装的类来返回取接收值
		while (cur)
		{
			if (key(cur->_data) > key(data))
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (key(cur->_data) < key(data))
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return make_pair(cur, false);
			}
		}
		//新增节点给红色,默认构造出来了
		cur = new Node(data);
		Node* newnode = cur;//由于存在更新的问题需要向上调整,因此cur不再是新插入的位置了,因此在调整之前我们先将其保存下来以防止找不到这个节点
		if (key(parent->_data) > key(data))
		{
			parent->_left = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_right = cur;
			cur->_parent = parent;
		}

		//插入完毕,开始红黑节点调整
		while (parent && parent->_col == RED)//这里要加上parent的判断,否则会出现空指针访问报错,先检验parent是否指向空
		{
			Node* grandfather = parent->_parent;
			if (parent == grandfather->_left)//父亲在左边,即叔叔在右边的情况
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED)//uncle不为空且为红色的情况下
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				else//叔叔节点为空或者黑色的情况
				{
					if (cur == parent->_left)//对cur的左右位置进行讨论,可能单旋,也可能双旋,这里为单旋
					{
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else//双旋
					{
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;//调整一次后,对上面就影响了,根节点为黑色,无论上面是什么颜色都不冲突,且不影响整体的黑色节点个数,因此直接跳出循环不要再进行了.
				}
			}
			else//叔叔在左边的情况
			{
				Node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (cur == parent->_right)
					{
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;//这里如果不停止,则父子关系后续是全乱的,后续在循环就会出现大问题,而且由于改为黑色节点已经没必要再向上修改了,及时退出即可
				}
			}
		}
		//有可能最后的节点为grandfather,此时根结点为红色,但要求根节点为黑色,因此不管是否为根节点,我们都最后将root改为黑色,这样就处理了所有情况
		_root->_col = BLACK;
		return make_pair(newnode, true);
	}
	void RotateL(Node* parent)//左单旋旋转调整
	{
		Node* sub1 = parent->_right;
		Node* sub2 = sub1->_left;
		Node* PPparent = parent->_parent;
		parent->_right = sub2;
		if (sub2)//注意这里,可能涉及到sub2为空的情况,比如h==0,此时要对sub2是否为空进行一次判断
		{
			sub2->_parent = parent;
		}
		sub1->_left = parent;
		parent->_parent = sub1;
		if (_root == parent)
		{
			_root = sub1;
			sub1->_parent = nullptr;
		}
		else
		{
			if (PPparent->_right == parent)
			{
				PPparent->_right = sub1;
			}
			else if (PPparent->_left == parent)
			{
				PPparent->_left = sub1;
			}
			sub1->_parent = PPparent;
		}
	}

	void RotateR(Node* parent)//右单旋旋转调整
	{
		Node* sub1 = parent->_left;
		Node* sub2 = sub1->_right;
		Node* PPparent = parent->_parent;
		parent->_left = sub2;
		if (sub2)
		{
			sub2->_parent = parent;
		}
		sub1->_right = parent;
		parent->_parent = sub1;
		if (_root == parent)
		{
			_root = sub1;
			sub1->_parent = nullptr;
		}
		else
		{
			if (PPparent->_right == parent)
			{
				PPparent->_right = sub1;
			}
			else if (PPparent->_left == parent)
			{
				PPparent->_left = sub1;
			}
			sub1->_parent = PPparent;
		}
	}
	void InOrder()//中序遍历接口
	{
		_InOrder(_root);
		cout << endl;
	}
	size_t size()//求节点个数
	{
		return _size(_root);
	}

	bool Check(Node* root, int blacknum, const int refVal)//节点检查函数,注意这里要传值返回而不是传地址,这是因为保证每一层的递归时blacknum的数据不变,保持原来的值,而是把之前的值传到下一个递归去,这样都相互不影响
	{
		if (root == nullptr)
		{
			if (blacknum != refVal)
			{
				cout << "存在黑色节点数量不相等的路径" << endl;
				return false;
			}
			return true;
		}
		if (root->_col == RED && root->_parent->_col == RED)//由于我们向下检查子节点的时候,子节点的情况太多不好检查,我们要检查两个节点是否为红,非常麻烦,所以我们采取向上检查的方式来检查子和父节点是否都为红色即可,因此我们反其道而行之向上检查,即检查当前节点和父节点是否都为红色,若都为红色则这个树不是红黑树
		{
			cout << "有连续的红色节点" << endl;
			return false;
		}
		if (root->_col == BLACK)
		{
			blacknum++;
		}
		return Check(root->_left, blacknum, refVal) && Check(root->_right, blacknum, refVal);
	}

	bool IsBalance()//判断是否为红黑树
	{
		if (_root == nullptr)
		{
			return true;
		}
		if (_root->_col == RED)
		{
			return false;
		}
		int refVal = 0;//黑色节点参考值,倘若不同就说明红黑树出现问题了,黑色节点不相同
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				refVal++;
			}
			cur = cur->_left;
		}
		int blacknum = 0;
		return Check(_root, blacknum, refVal);
	}
private:
	Node* _root = nullptr;
	void _InOrder(Node* root)//中序遍历
	{
		if (root == nullptr)
		{
			return;
		}
		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		_InOrder(root->_right);
	}
	size_t _size(Node* root)//求节点个数
	{
		if (root == nullptr)
		{
			return 0;
		}
		return _size(root->_left) + _size(root->_right) + 1;
	}
};

红黑树的基本结构不变,但我们为此做出了一些修改,由于红黑树同时满足了set 和 map的底层,因此我们统一将数据为class T,同时我们还增加了迭代器的内容:

typedef struct _Treeiterator<T,T&,T*> iterator;//迭代器
typedef struct _Treeiterator<T,const T&,const T*> const_iterator;//const 迭代器

这种常见的迭代器类,我们之前进行容器的模拟实现的时候都是采用这种方式,将T T& T拆分开三个部分传参,这样传参的目的是让我们的const_iterator和iterator两个迭代器共用一份代码,不需要重复写。另一个只要传入T const T& const T即可。

2.迭代器:

下面就是迭代器了,对于迭代器,我们最主要的就是对前置加加和前置减减进行处理。

前置++:

对于前置++,我们的代码如下:

Self& operator++()//前置++
	{
		if (_node->_right)
		{
			Node* cur = _node->_right;
			while (cur->_left)
			{
				cur = cur->_left;
			}
			_node = cur;
		}
		else
		{
			Node* cur = _node;
			Node* parent = _node->_parent;
			while (parent && parent->_right == cur)
			{
				cur = parent;
				parent = parent->_parent;
			}
			_node = parent;//当parent为空时,说明整棵树走完了,直接返回空与end一同判定即可
		}
		return *this;
	}

首先,我们首先要判断我们当前节点的右边有没有节点,如果有,那就向右向下走,直接走到右边节点的最左边,否则说明下面的节点都访问结束了,后续需要执行的便是向上走即可。

前置–:

前置–就是前置++的镜像,因此我不做过多赘述:

	Self& operator--()//前置--,将++反过来,按照右 根 左的顺序进行
	{
		if (_node->_left)
		{
			Node* cur = _node->_left;
			while (cur->_right)
			{
				cur = cur->_right;
			}
			_node = cur;
		}
		else
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && parent->_left == cur)
			{
				cur = parent;
				parent = parent->_parent;
			}
			_node = parent;
		}
		return *this;
	}

3.对于插入的改造:

我们不妨思考一下,由于模板的存在,我们如何统一让set返回first而map返回second呢?
按理来说这时不可能的,此时我们就要做进一步的封装,也就是模板套模板的形式,同时利用仿函数封装一个针对set和map分别处理的仿函数类来处理插入中数值的访问问题,如下:

struct SetKeyOf//仿函数类
{
	const K& operator()(const K& key)
	{
		return key;
	}
};
struct MapKeyOf//仿函数类,其实这个仿函数类对于set意义不大,因为set本身就可以返回key,它的主要作用是帮助map的pair来返回key使用的.
{
	const K& operator()(const pair<K, T>& kv)
	{
		return kv.first;
	}
};

有了这两个分别处理set和map的仿函数,我们就可以在模板中传入一个仿函数模板,如下:

template<class K, class T,class KeyOf>//利用一个仿函数来处理key的获取问题

这样在后续处理的过程中,我们在插入的时候就需要一个仿函数对象处理返回值,如下代码:

KeyOf key;//设置一个通用的仿函数来将对应的map和set中对应的key取出来,因为pair自己的first没法取,因此利用分别封装的类来返回取接收值
while (cur)
{
	if (key(cur->_data) > key(data))
	{
		parent = cur;
		cur = cur->_left;
	}
	else if (key(cur->_data) < key(data))
	{
		parent = cur;
		cur = cur->_right;
	}
	else
	{
		return make_pair(cur, false);
	}
}

比如在这里,我们将节点放入到对应的位置的循环中就先创建了一个仿函数对象处理数据访问的问题,这样不管是set还是map,都会根据模板选择对应的仿函数类,从而选择对应的数据返回方式,这是一种模板套模板,利用仿函数实现一种类似多态的方式。
同时,针对insert的返回方式,我们也要遵守STL文档给的,返回一个T数据类型和bool值的键值对,后续我们根据我们的set和map传入迭代器iterator即可。
插入代码如下:

pair<Node*,bool> Insert(const T& data)//插入
{
	if (_root == nullptr)
	{
		_root = new Node(data);
		_root->_col = BLACK;
		return make_pair(_root, true);
	}
	Node* cur = _root;
	Node* parent = nullptr;
	KeyOf key;//设置一个通用的仿函数来将对应的map和set中对应的key取出来,因为pair自己的first没法取,因此利用分别封装的类来返回取接收值
	while (cur)
	{
		if (key(cur->_data) > key(data))
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (key(cur->_data) < key(data))
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			return make_pair(cur, false);
		}
	}
	//新增节点给红色,默认构造出来了
	cur = new Node(data);
	Node* newnode = cur;//由于存在更新的问题需要向上调整,因此cur不再是新插入的位置了,因此在调整之前我们先将其保存下来以防止找不到这个节点
	if (key(parent->_data) > key(data))
	{
		parent->_left = cur;
		cur->_parent = parent;
	}
	else
	{
		parent->_right = cur;
		cur->_parent = parent;
	}

	//插入完毕,开始红黑节点调整
	while (parent && parent->_col == RED)//这里要加上parent的判断,否则会出现空指针访问报错,先检验parent是否指向空
	{
		Node* grandfather = parent->_parent;
		if (parent == grandfather->_left)//父亲在左边,即叔叔在右边的情况
		{
			Node* uncle = grandfather->_right;
			if (uncle && uncle->_col == RED)//uncle不为空且为红色的情况下
			{
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;
				cur = grandfather;
				parent = cur->_parent;
			}
			else//叔叔节点为空或者黑色的情况
			{
				if (cur == parent->_left)//对cur的左右位置进行讨论,可能单旋,也可能双旋,这里为单旋
				{
					RotateR(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
				}
				else//双旋
				{
					RotateL(parent);
					RotateR(grandfather);
					cur->_col = BLACK;
					grandfather->_col = RED;
				}
				break;//调整一次后,对上面就影响了,根节点为黑色,无论上面是什么颜色都不冲突,且不影响整体的黑色节点个数,因此直接跳出循环不要再进行了.
			}
		}
		else//叔叔在左边的情况
		{
			Node* uncle = grandfather->_left;
			if (uncle && uncle->_col == RED)
			{
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;
				cur = grandfather;
				parent = cur->_parent;
			}
			else
			{
				if (cur == parent->_right)
				{
					RotateL(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
				}
				else
				{
					RotateR(parent);
					RotateL(grandfather);
					cur->_col = BLACK;
					grandfather->_col = RED;
				}
				break;//这里如果不停止,则父子关系后续是全乱的,后续在循环就会出现大问题,而且由于改为黑色节点已经没必要再向上修改了,及时退出即可
			}
		}
	}
	//有可能最后的节点为grandfather,此时根结点为红色,但要求根节点为黑色,因此不管是否为根节点,我们都最后将root改为黑色,这样就处理了所有情况
	_root->_col = BLACK;
	return make_pair(newnode, true);
}

4.底层之上的set和map类,套用红黑树接口函数:

代码如下:

#pragma once
#include"RBTree.h"
namespace hbw
{
	template<class K>
	class set
	{
	public:
		struct SetKeyOf//仿函数类
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
		typedef typename RBTree<K, K, SetKeyOf>::const_iterator iterator;
		typedef typename RBTree<K, K, SetKeyOf>::const_iterator const_iterator;//由于set就要求不能修改,因此不论是iterator还是const_iterator统一都使用const_iterator进行封装

		//typedef struct _Treeiterator<K> iterator;
		iterator begin() const
		{
			return _t.begin();
		}
		iterator end() const
		{
			return _t.end();
		}
		pair<iterator,bool> Insert(const K& key)//set插入
		{
			return _t.Insert(key);
		}
		
	private:
		RBTree<K,K,SetKeyOf> _t;
	};
}
#pragma once
#include"RBTree.h"
namespace hbw
{
	template<class K, class T>
	class map
	{
	public:
		struct MapKeyOf//仿函数类,其实这个仿函数类对于set意义不大,因为set本身就可以返回key,它的主要作用是帮助map的pair来返回key使用的.
		{
			const K& operator()(const pair<K, T>& kv)
			{
				return kv.first;
			}
		};
		//加typedef告诉编译器这里是类型
		typedef typename RBTree<K, pair<const K, T>, MapKeyOf>::iterator iterator;//注意这里要加typename,告诉编译器这是一个类,否则编译器由于没有实例化,无法判断这个模板的类是什么,因此没法确定这个iterator到底是类型还是静态变量,因此对于这种内嵌类型的typedef外部封装,要加typename
		typedef typename RBTree<K, pair<const K, T>, MapKeyOf>::const_iterator const_iterator;
		//typedef struct  _Treeiterator<pair<K,T>> iterator;//不过上面的写法容易出错,因此我直接独立访问_Treeiterator,如果是写在这个RBTtree里面的内部类就只能通过访问限定符去访问,但在这里可以直接访问
		iterator begin()
		{
			return _t.begin();
		}
		iterator end()
		{
			return _t.end();
		}
		const_iterator begin() const 
		{
			return _t.begin();
		}
		const_iterator end() const
		{
			return _t.end();
		}
		pair<iterator, bool> Insert(const pair<K, T>& kv)//map插入
		{
			return _t.Insert(kv);
		}
		T& operator[](const K&key)//map方括号下标访问形式,利用insert的返回值来实现
		{
			pair<iterator, bool> it = _t.Insert(make_pair(key, T()));//这里value给缺省值,因为我们查找的就是value,因此此时的value根本确定不了
			return it.first->second;
		}
		const T& operator[](const K& key) const//map方括号下标访问形式,利用insert的返回值来实现
		{
			pair<iterator, bool> it = _t.Insert(make_pair(key, T()));//这里value给缺省值,因为我们查找的就是value,因此此时的value根本确定不了
			return it.first->second;
		}
	private:
		RBTree<K, pair<const K, T>,MapKeyOf> _t;
	};
}

这便是我们实现set和map的精髓之处,模板嵌套模板,在底层的红黑树之上,将set和map在封装一层模板,这种方法很常见,我们要仔细品味体会一下。你会发现,我们的set和map类的参数基本都使用了底层的红黑树的接口。
同时注意map的operator[],我们就遵循了我刚才说过的那个operator[]使用了insert的原理。

同时针对对应的仿函数类,只要将其作为一个模板传入到底层的红黑树类即可针对不同的类调用不同的仿函数类,从而针对不同的数据进行返回对应的数据,这样就避免了map和set数据返回不同的问题。

multimap容器:

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

注意:multimap和map的唯一不同就是:map中的key是唯一的,而multimap中key是可以重复的。
注意:
1. multimap中的key是可以重复的。
6. multimap中的元素默认将key按照小于来比较
7. multimap中没有重载operator[]操作(同学们可思考下为什么?)。
8. 使用时与map包含的头文件相同:

总结:

在本篇中我们详细介绍了set map容器的使用,模拟实现,这其中最关键的除了熟练掌握容器的各种函数使用之外,对于多层封装以及巧妙运用仿函数进行数据获取也是一个很关键的一点。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1547956.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

what is apache?

Apache 通常指 Apache Software Foundation (ASF) 或 Apache HTTP Server&#xff0c;两者都是计算机软件领域的重要实体。 Apache 软件基金会 (ASF)&#xff1a;Apache 软件基金会是一个开发开源软件项目的非营利组织。它为涵盖软件开发各个方面的广泛项目提供支持&#xff0c…

面试官:tree-shaking的原理是什么?

公众号&#xff1a;程序员白特&#xff0c;欢迎一起交流学习~ 原文&#xff1a;https://juejin.cn/post/7265125368553685050?share_token301335cf-a17e-4115-94f0-264fe0e82f07 前言 在前端面试的过程中&#xff0c;前端工程化一直是考察面试者能力的一个重点&#xff0c;而…

nginx代理服务后,有关文件的操作无法执行,nginx代理jupyter或为知笔记后无法创建文件及文件夹,无法操作文件

nginx配置 server {listen 18001; # 修改转发的接口listen [::]:18001; # 修改转发的接口server_name _;root /usr/share/nginx/html;location / {proxy_pass http://127.0.0.1:7777; # 指定自己服务地址proxy_set_header Host $host;}# Load configurat…

【C语言】Infiniband驱动__mlx4_init_one函数

一、注释 Linux内核驱动程序中的部分&#xff0c;属于Mellanox网卡驱动mlx4的初始化过程。 // Mellanox 以太网驱动主程序代码 static int __mlx4_init_one(struct pci_dev *pdev, int pci_dev_data,struct mlx4_priv *priv) {int err; // 错误码变量int nvfs[MLX4_MAX_PORTS…

【C++】从C到C++、从面向过程到面向对象(类与对象)

文章目录 C入门知识C与C的关系1. 类的引入&#xff1a;从结构体到类2. 类的声明和定义3. 类的作用域4. 类的访问限定符5. 面向对象特性之一&#xff1a;封装6. 类的实例化&#xff1a;对象7. 计算类对象的内存大小8. 成员函数中暗藏的this指针9. 类的六个默认生成的成员函数9.1…

【二叉树】Leetcode 226. 翻转二叉树【简单】

翻转二叉树 给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 示例 1&#xff1a; 输入&#xff1a;root [4,2,7,1,3,6,9] 输出&#xff1a;[4,7,2,9,6,3,1] 解题思路 二叉树翻转操作是指将二叉树中每个节点的左右子树进行交换。具体…

[Android]模拟器登录Google Play失败

问题&#xff1a; 模拟器登录Google Play失败&#xff0c;提示couldnt sign in there was a problem communicating with google servers. try again later. 原因&#xff1a; 原因是模拟器没有连接到互联网&#xff0c;打开模拟器中Google浏览器进行搜索一样不行。 解决&am…

虚拟机如何在原有磁盘上扩容

虚拟机未开启状态–菜单栏–虚拟机–快照–拍摄快照–拍摄快照– 菜单栏–虚拟机–快照–快照管理器–点击刚刚的快照1–删除–是– 文件–新建或者打开–硬盘&#xff08;以本人Win 10.64.3GL为例&#xff09;–虚拟机设置–硬件– 硬盘&#xff08;SATA&#xff09;–磁盘实…

赛氪网亮相中国人工智能产业发展联盟会议,共筑赛事生态新篇章

2024年3月14日至15日&#xff0c;备受瞩目的中国人工智能产业发展联盟&#xff08;AIIA&#xff09;第十一次全体会议在海南海口盛大召开。作为人工智能领域的重要交流与合作平台&#xff0c;此次会议吸引了300余位联盟成员单位代表齐聚一堂&#xff0c;共襄盛举。在这场人工智…

梵宁教育:助力大学生掌握设计新技能

在当今数字化、信息化的社会里&#xff0c;设计技能的重要性日益凸显。设计不仅仅局限于美术或艺术领域&#xff0c;它已经渗透到各行各业&#xff0c;从产品外观到用户界面&#xff0c;从品牌形象到营销策划&#xff0c;设计无处不在。对于大学生而言&#xff0c;掌握设计新技…

ts enum elementUI 表格列表中如何方便的标识不同类型的内容,颜色区分 enum ts + vue3

ts enum elementUI 表格列表中如何方便的标识不同类型的内容&#xff0c;颜色区分 enum ts vue3 本文内容为 TypeScript 一、基础知识 在展示列表的时候&#xff0c;列表中的某个数据可能是一个类别&#xff0c;比如&#xff1a; enum EnumOrderStatus{"未受理" …

python如何获取word文档的总页数

最近在搞AI. 遇到了一个问题&#xff0c;就是要进行doc文档的解析。并且需要展示每个文档的总页数。 利用AI. 分别尝试了chatGPT, 文心一言&#xff0c; github copilot&#xff0c;Kimi 等工具&#xff0c;给出来的答案都不尽如人意。 给的最多的查询方式就是下面这种。 这个…

时序预测 | Matlab实现GWO-BP灰狼算法优化BP神经网络时间序列预测

时序预测 | Matlab实现GWO-BP灰狼算法优化BP神经网络时间序列预测 目录 时序预测 | Matlab实现GWO-BP灰狼算法优化BP神经网络时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现GWO-BP灰狼算法优化BP神经网络时间序列预测&#xff08;完整源码和数据…

kali系统下载,并在VMware免安装式打开kali虚拟机

Kali Linux | Penetration Testing and Ethical Hacking Linux Distribution 上面连接为官网连接 此安装过程也适合其他虚拟机的安装&#xff0c;也就是你曾经安装过的虚拟机&#xff0c;会保存一推文件&#xff0c;而这推文件&#xff0c;还可以用VMware再次打开。

Unity中如何实现草的LOD

1&#xff09;Unity中如何实现草的LOD 2&#xff09;用Compute Shader处理图像数据后在安卓机上不能正常显示渲染纹理 3&#xff09;关于进游戏程序集加载的问题 4&#xff09;预制件编辑模式一直在触发自动保存 这是第379篇UWA技术知识分享的推送&#xff0c;精选了UWA社区的热…

【Linux】线程的概念{虚拟地址堆区细分/缺页中断/页/初识线程/创建线程/优缺点}

文章目录 1.前导知识1.1 虚拟地址空间的堆区1.2 缺页中断1.3ELF文件格式1.4页/页框/页帧/页表/MMU1.5虚拟地址到物理地址 2.初识Linux线程2.1之前所学的进程2.2线程的引入2.3如何理解线程2.4如何理解轻量级进程 3.创建线程3.1pthread_create()函数3.2程序测试3.3Makefile怎么写…

力扣--并查集1631.最小体力消耗路径

这题将图论和并查集联系起来。把数组每个位置看成图中的一个节点。 这段代码的主要思路是&#xff1a; 遍历地图中的每个节点&#xff0c;将每个节点与其相邻的下方节点和右方节点之间的边加入到边集合中&#xff08;因为从上到下和从下到上他们高度绝对值一样的&#xff0c;…

文件IO的方式读取jpeg图片的分辨率

1、读取jpeg图片分辨率的两种方式 1.1 使用libjpeg库 可以使用libjpeg库读取JPEG图像文件&#xff0c;并获取图像的分辨率&#xff08;宽度和高度&#xff09;&#xff0c;简单demo示例如下&#xff1a; #include <stdio.h> #include <jpeglib.h>int main() {st…

6、ChatGLM3-6B 部署实践

一、ChatGLM3-6B介绍与快速入门 ChatGLM3 是智谱AI和清华大学 KEG 实验室在2023年10月27日联合发布的新一代对话预训练模型。ChatGLM3-6B 是 ChatGLM3 系列中的开源模型&#xff0c;免费下载&#xff0c;免费的商业化使用。 该模型在保留了前两代模型对话流畅、部署门槛低等众多…

GPT2从放弃到入门(四)

引言 体验地址&#xff1a;https://huggingface.co/spaces/greyfoss/gpt2-chatbot 上篇文章我们通过Gradio作为前端轻松地连接到训练好的Chatbot&#xff0c;本文介绍如何分享你创建好的模型给你的朋友。 当你训练好的模型推送到Huggingface Hub上后&#xff0c;其实还可以进一…