C++map与set模拟实现

Posted 可乐不解渴

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++map与set模拟实现相关的知识,希望对你有一定的参考价值。

C++map与set模拟实现

在之前的博客中我们提到了map与set在stl的源码中都是由同一棵红黑树来实现,而我们也模拟stl中的实现方式来分别实现map的K-V与set的K模型。
其中未封装红黑树代码如下:红黑树代码自提链接

红黑树的改造

由于我们需要用同一颗红黑树来实现map的K-V与set的K模型,并且我们的红黑树默认实现的K-V的模型,为了同样适应K的模型,我们需要对红黑树进行一些调整。

  1. 首先我们将红黑树第二个模板参数类型的名字改为T,这是为了与之前的K-V模型进行区分。
    并且这个T就不再是之前单单的表示Value了,这个T可能代表的是Key,也有可能是pair<Key,Value>共同构成的键值对。
template<class K, class T>
class RBTree

	...
private:
	node* m_root;	//根结点
;
  1. 其次是需要控制map和set传入底层红黑树的模板参数,即map传给RBTree的T模板参数传入pair<K, V>。
template<class K, class V>
class map

public:
	...
private:
	RBTree<K, pair<K, V>> m_tree;
;

而set传给RBTree的T模板参数传入K。

template<class K>
class set

public:
	...
private:
	RBTree<K, K> m_tree;
;
可能有人会问能不能不要红黑树的第一个模板参数,只保留第二个模板参数呢?

答:不能。
当是set的时候,K和T都代表键值Key,去掉第一个模板参数是可以的。
但如果是map时,K代表键值Key,T代表由Key和Value构成的键值对。如果去掉第一个参数的话,那么就无法直到
Key的类型。

红黑树结点改造

由于我们知道红黑树的第二个参数现在表示为存储的数据类型,那么树结点模板参数与存储的值也要相应改变,模板参数由原来的K和V改成T,让这个T类型的参数来表示结点存储值的类型,即现在的结点数据由从前的pair<K,V> m_kv改变为 T m_data。

具体层次关系如下所示:

结点修改后的代码如下所示:

//节点的颜色
enum class Colour

	RED,
	BLACK
;

template<class T>
struct RBTreeNode	//三叉链

	RBTreeNode<T>* m_left;	//左节点
	RBTreeNode<T>* m_right;   //右节点
	RBTreeNode<T>* m_parent;  //节点的父结点(红黑树需要 旋转 ,为了实现简单给出的该字段)

	T m_data; //节点数据
	Colour m_c;	//节点颜色


	RBTreeNode(const T& data)
		: m_left(nullptr), m_right(nullptr), m_parent(nullptr)
		, m_data(data),m_c(Colour::RED)
	
;

红黑树模板参数增加仿函数类型

现在由于结点当中存储的是T类型,并且这个T类型可能是Key值,也可能是<Key, Value>键值对。
那么当我们需要利用T这个值进行结点的键值比较时,应该如何获取结点的键值呢?

答:我们可以再给红黑树中再增加一个模板参数,通过上层map与set传入仿函数给RBTree的第三个仿函数类型,
我就可以利用仿函数对象来调用重载的operator(),来获取到对应的Key值。

由于我们知道底层的红黑树它并不知道上层容器现在是map还是set,因此当需要进行两个结点键值的比较时,底层的红黑树都会通过传入的仿函数来获取键值Key,进而进行两个结点键值的比较。

	template<classs K, class V>
	class map
	
	public:
		class MapKeyOfCompare    //内部类
		
		public:
			const K& operator()(const pair<K,V>&kv)
			
				return kv.first;
			
		;
		...
	private:
		RBTree <K, pair<K, V>, MapKeyOfCompare>m_tree;
	;

但是对于set容器来说,由于set是Key的模型,当set传给RBTree这个类时,其实可以不用传仿函数,直接进行比较,但为了与map使用同一颗封装好的红黑树,这里我们也与map一样统一传入仿函数模板参数。

	template<class K>
	class set
	
	public:
		class SetKeyOfCompare   //内部类
		
		public:
			const K& operator()(const K& k)
			
				return k;
			
		;
		...
	private:
		RBTree <K, K, SetKeyOfCompare>m_tree;
	;

迭代器

这里红黑树的迭代器实际上就是对红黑树结点的指针进行了封装。

正向迭代器

typedef Treeiterator<T,T&,T*> iterator;  //普通迭代器
typedef Treeiterator<T, const T&, const T*> const_iterator; //常量迭代器
可能有人会疑惑,上面这两个迭代器不都差不多嘛,只是名字不一样,为什么不直接在类型上加const,
而是在模板参数上指定加上const属性呢?

答:我们要知道由于const对象只能调用常函数,但是像平时我们使用std::vector等容器时是不是可以支持 ++、-- 
等运算符重载呢?如果是const对象,它只能调用常函数,一旦加上变成const函数,那我们直接再类型上加的话
const迭代器就不能进行++、--、* 等,而我们要达到的效果是可以进行++、--等,但仅仅是不能修改其元素的值而
已。所以我们这里封装了一个模板指针。我们通过模板的参数不同来控制它的读取操作。

根据上面的阐述,我们就可以写出大致的迭代器基本定义。
具体代码如下所示:

template<class T,class Ref,class Ptr>
class Treeiterator

public:
	typedef RBTreeNode<T>node;  //结点类型重命名为node
	typedef Ref reference;
	typedef Ptr pointer;
public:
	Treeiterator(node* Pnode):m_node(Pnode)
	
private:
	node* m_node;
;

🗼迭代器构造函数

就是将一个结点的指针传给迭代器构造函数,并赋给我们迭代器的成员变量m_node。

	Treeiterator(node* Pnode):m_node(Pnode)
	

🧸迭代器++ - -运算符重载

其中++与- -运算符都是分别要满足正向中序遍历与反向中序遍历的。
即正向中序遍历默认是升序的,而反向中序遍历是降序。

其中迭代器++运算符重载思路是根据红黑树的begin()是指向的是红黑树的最左结点,那么我们此时最左结点开始左孩子一定为nullptr,此时遍历完了最左结点。那么就要开始遍历这个结点的右孩子的最左结点,如果右孩子也是为nullptr我们才需要往上更新继续遍历。

	self& operator++() //前置++
	
		if (m_node->m_right != nullptr)
		
			node* subLeft = m_node->m_right;
			while (subLeft->m_left != nullptr)
			
				subLeft=subLeft->m_left;
			
			m_node = subLeft;
		
		else
		
			node* cur = m_node;
			node* parent = cur->m_parent;
			while (parent != nullptr&& parent->m_right == cur )
			
				cur = parent;
				parent = parent->m_parent;
			
			m_node = parent;
		
		return *this;
	

	self operator++(int)  //后置++
	
		self temp(*this);
		this->operator++(); //调用前置++来实现后置++
		return temp;
	

与++同样的原理,++是从最左结点开始,相反那么- - 就是从最右结点开始倒序遍历,此时最右结点右孩子一定为nullptr,此时遍历完了最右结点。那么就要开始遍历这个结点的左孩子的最右结点,如果左孩子也是为nullptr我们才需要往上更新继续遍历。

	self& operator--()  //前置--
	
		if (m_node->m_left != nullptr)
		
			node* subright = m_node->m_left;
			while (subright->m_right != nullptr)
			
				subright = subright->m_right;
			
			m_node = subright;
		
		else
		
			node* cur = m_node;
			node* parent = cur->m_parent;
			while (parent != nullptr && parent->m_left == cur)
			
				cur = parent;
				parent = parent->m_parent;
			
			m_node = parent;
		
		return *this;
	

	self operator--(int)  //后置--
	
		self temp(*this);
		this->operator--(); //调用前置--来实现后置--
		return temp;
	

🗽迭代器 * 运算符重载

由于我们知道Ref是有两种类型的,一种是T&,另一种是const T&,所以即便是当我们的对象是const对象时,我们也可以控制它不让外部去修改。

	Ref operator*()
	
		return m_node->m_data;
	

🏝迭代器关系运算符重载

因为我们要实现迭代器的相关遍历操作,故我们要重载operator == 与 operator !=。
例如下面的代码:
ZJ::map<int,int>::iterator it = m.begin();
it != m.end()

	bool operator!=(const Treeiterator<T,Ref,Ptr>& obj) //重载!=
	
		return m_node != obj.m_node;
	

	bool operator==(const Treeiterator<T, Ref, Ptr>& obj) //重载==
	
		return m_node == obj.m_node;
	

🪐迭代器 -> 运算符重载

为什么要重载->运算符呢?这是因为如果我们RBTree中存储的是自定义类型时,我们的迭代器无法使用->去得到其成员。这里的Ptr也是有两种类型的,一种是T*,另一种是const T*,与上面的同理即便是当我们的对象是const对象时,我们也可以控制它不让外部去修改。

	Ptr operator->()
	
		return &m_node->m_data;
	

小结

到了这里可能会有人会问为什么我们不写迭代器的拷贝构造函数和析构函数呢?

答:这是因为我们的迭代器只是用来遍历容器中的部分或全部元素,方便我们不用去理解底层,就能得到元素的各个
值。每个迭代器对象代表的是容器中的确定的地址,并且这些结点元素析构和拷贝并不归我们管,结点应该归我们的
RBTree管,所以编译器默认提供的浅拷贝就已经足够了。

反向迭代器

这里的方向迭代器实际上就是正向迭代器的一个封装,红黑树的反向迭代器与stl中的priority_queue、stack和queue等一样就是一个适配器。

//迭代器适配器
template<class iterator>
class reverseIterator

public:
	typedef typename iterator::reference Ref;
	typedef typename iterator::pointer Ptr;
public:
	
	reverseIterator(iterator it) :m_it(it)
	
	
	Ref operator *()
	
		return *m_it;
	

	Ptr operator->()
	
		return m_it.operator->();
	

	reverseIterator<iterator>& operator++() //前置++
	
		--m_it;
		return *this;
	

	reverseIterator<iterator> operator++(int) //后置++
	
		reverseIterator<iterator> temp(*this);
		--m_it;
		return temp;
	

	reverseIterator<iterator>& operator--()  //前置--
	
		++m_it;
		return *this;
	

	reverseIterator<iterator> operator--(int) //后置--
	
		reverseIterator<iterator> temp(*this);
		++m_it;
		return temp;
	
	bool operator!=(const reverseIterator<iterator>& obj)
	
		return m_it!=obj.m_it;
	

	bool operator==(const reverseIterator<iterator>& obj)
	
		return m_it == obj.m_it;
	

private:
	iterator m_it;
;

封装好的红黑树及迭代器

其中在三个类中分别有下图的代码可能会有人迷惑,我在这阐述一下。

为什么这里给这个类型重命名需要加上typename或者class关键字呢?

答:这是因为当前这个类还未实例化时,又要将该类的模板参数传入到下一层的模板中,导致编译器找不到具体实例
化,故加上这个关键字是为了告诉编译器,你有这个东西但是需要进行实例化之后时候再来检查。

封装好的红黑树完整代码如下:

#pragma once
#include<iostream>
#include<stack>
#include<assert.h>
using namespace std;

//节点的颜色
enum class Colour

	RED,
	BLACK
;

template<class T>
struct RBTreeNode	//三叉链

	RBTreeNode<T>* m_left;	//左节点
	RBTreeNode<T>* m_right;   //右节点
	RBTreeNode<T>* m_parent;  //节点的父结点(红黑树需要 旋转 ,为了实现简单给出的该字段)

	T m_data; //节点数据
	Colour m_c;	//节点颜色


	RBTreeNode(const T& data)
		: m_left(nullptr), m_right(nullptr), m_parent(nullptr)
		, m_data(data),m_c(Colour::RED)
	
;

template<class T,class Ref,class Ptr>
class Treeiterator

public:
	typedef RBTreeNode<T>node; //结点类型重命名为node
	typedef Ref reference;     
	typedef Ptr pointer;
	typedef Treeiterator<T, Ref, Ptr> self;
public:


	Treeiterator(node* Pnode):m_node(Pnode)
	

	Ref operator*()
	
		return m_node->m_data;
	
	Ptr operator->()
	
		return &m_node->m_data;
	


	self& operator++() //前置++
	
		if (m_node->m_right != nullptr)
		
			node* subLeft = m_node->m_right;
			while (subLeft->m_left != nullptr)
			
				subLeft=subLeft->m_left;
			
			m_node = subLeft;
		
		else
		
			node* cur = m_node;
			node* parent = cur->m_parent;
			while (parent != nullptr&& parent->m_right == cur )
			
				cur = parent;
				parent = parent->m_parent;
			
			m_node = parent;
		
		return *this;
	

	self operator++(int)  //后置++
	
		self temp(*this);
		this->operator++();
		return temp;
	

	self& operator--()  //前置--
	
		if (m_node->m_left != nullptr)
		
			node* subright = m_node->m_left;
			while (subright->m_right != nullptr)
			
				subright = subright->m_right;
			
			m_node = subright;
		
		else
		
			node* cur = m_node;
			node* parent = cur->m_parent;
			while (parent != nullptr && parent->m_left == cur)
			
				cur = parent;
				parent = parent->m_parent;
			
			m_node = parent;
		
		return *this;
	

	self operator--(int)  //后置--
	
		self temp(*this);
		this->operator--();
		return temp;
	

	bool operator!=(const Treeiterator<T,Ref,Ptr>& obj)
	
		return m_node != obj.m_node;
	

	bool operator==(const Treeiterator<T, Ref, Ptr>& obj)
	
		return m_node == obj.m_node;
	
private:
	node* m_node;

;

//迭代器适配器
template<class iterator>
class reverseIterator

public:
	typedef typename iterator::reference Ref;
	typedef typename iterator::pointer Ptr;
public:
	
	reverseIterator(iterator it) :m_it(it)
	
	
	Ref operator *()
	
		return *m_it;
	

	Ptr operator->()
	
		return m_it.operator->();
	

	reverseIterator<iterator>& operator++()
	
		--m_it;
		return *this;
	

	reverseIterator<iterator> operator++(int)
	
		reverseIterator<iterator> temp(*this);
		--m_it;
		return temp;
	

	reverseIterator<iterator>& operator--()
	
		++m_it;
		return *this;
	

	reverseIterator<iterator> operator--(int)
	
		reverseIterator<iterator> temp(*this);
		++m_it;
		return temp;
	
	bool operator!=(const reverseIterator<iterator>& obj)
	
		return m_it!=obj.m_it;
	

	bool operator==(const reverseIterator<iterator>& obj)
	
		return m_it == obj.m_it;
	

private:
	iterator m_it;
;

template<class K,class <

以上是关于C++map与set模拟实现的主要内容,如果未能解决你的问题,请参考以下文章

C++map与set模拟实现

[C/C++]详解STL容器9-基于红黑树模拟实现map和set

[C/C++]详解STL容器9-基于红黑树模拟实现map和set

[C/C++]详解STL容器9-基于红黑树模拟实现map和set

[C/C++]详解STL容器9-基于红黑树模拟实现map和set

set和map容器