C++进阶 | [4.4] 红黑树封装 map 和 set

摘要:实现红黑树的迭代器,用红黑树封装 map 和 set

Red/Black Tree Visualization (usfca.edu) 👈这是一个红黑树的可视化网站


1. 框架

  • 红黑树 · 类模板参数解读:
    template<class Key, class Type, class KeyofValue, ...>
    class RBTree{...};

    Key:有些成员函数需要用到K值的类型(库里面的实现是有这个模版参数的,因为有些函数会需要用到这个数据类型,但本文所展示的模拟实现不会完整地实现所有成员函数,由于模拟实现中不需要用到这个 K 值的类型,所以下文中的实现不含这个模版参数
    Type:数据类型(例如,map对象中存储的数据类型为pair<K,V>
    KeyofValue:某个类,用途:仿函数,用来去获取数据中的Key值。 因为对于红黑树(BST)可能是Key模型或KV模型(详情参看“搜索二叉树的两种模型”一文),而默认是通过比较Key值进行插入。例如,如果红黑树的节点中存储的数据类型是一个 pair 类型,插入的时候要去比较数据大小,找到合适的位置插入,此时就需要取到 pair 类型的 first 成员变量的值去进行比较。

    ps.关于类的模板参数是否有必要存在的关键:该类用不用得到这个Type. 例如,如果 class RBTree 中 find函数实现中需要用到 Key 这个数据类型(Type),则就需要写上这个模板参数。如果用不到就不用写。


  • 模拟实现的红黑树(整体框架):
    enum Colour
    {
    	RED, BLACK
    };
    
    template<class T>
    struct RBTreeNode
    {
    	RBTreeNode(T data = T())
    		:_data(data)
    		, _pLeft(nullptr)
    		, _pRight(nullptr)
    		, _pParent(nullptr)
    		, _col(RED)//默认新创建的节点都是红色的
    	{}
    	T _data;
    	RBTreeNode<T>* _pLeft;
    	RBTreeNode<T>* _pRight;
    	RBTreeNode<T>* _pParent;
    	Colour _col;
    };
    
    
    // T: 可能是键值对<key,value>
    //    可能是一个key
    // 不论节点中存储的是<key, value> || key, 都是按照key来进行比较的
    // KeyOfValue: 提取data中的Key
    template<class T, class KeyOfValue>
    class RBTree
    {
    	typedef RBTreeNode<T> Node;
    public:
    	RBTree()
    		: _size(0)
    	{
    		_pHead = new Node;
    		_pHead->_pLeft = _pHead;
    		_pHead->_pRight = _pHead;
    		_pHead->_pParent = nullptr; //将头结点的pParent节点置空
    	}
    
    private:
    	Node* _pHead;
    	size_t _size; //节点个数
    	KeyOfValue _GetKey;
    };
    
  • setRBTree< K, KeyofSet<K>>
    namespace RoundBottle 
    {
        template<class K>
        struct KeyofSet {...};
    
        template<class K>
        class set
        {
        private:
            RBTree<K,K,KeyofSet<K>> _rb;
        };
    }

    ps.如上,set 只用的到一个数据类型(K),因此只需要写一个模板参数。

  • mapRBTree< pair<K,V>, KeyofMap<K>>
    namespace RoundBottle
    {
    	template<class K, class V>
    	struct KeyofMap
    	{
    		const K& operator()(const pair<K, V>& data)
    		{
    			return data.first;
    		}
    	};
    
    	template<class K, class V>
    	class map
    	{
    	private:
    		RBTree<pair<K, V>, KeyofMap<K, V>> _rb;
    	};
    }

2. 对红黑树的 insert 函数进行修改

红黑树首先遵循搜索二叉树的规则进行插入,必然存在通过数据比较大小寻找插入位置,而所有的数据比较都需要通过仿函数去取得。因此,必须做出如下修改:

  1. 增加成员变量:KeyofValue 模板参数类型的对象(e.g.KeyOfValue _GetKey;
  2. 通过仿函数进行数据比较: _GetKey(data)  即 _GetKey.operator(data) . 这个通过自己去实现具体的仿函数来控制operator()函数的执行结果。

仿函数已经反复使用过了,不多赘述。另外,insert 函数的完整实现代码比较长,修改点也不多,就不在此处展开了


3. 迭代器_iterator

template<class T>
struct RBTreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef RBTreeIterator<T> Self;

	RBTreeIterator(Node* pNode)
		: _pNode(pNode)
	{}

	// 让迭代器具有类似指针的行为
	T& operator*();
	T* operator->();
	
	Self& operator++();
	Self operator++(int);
	
	Self& operator--();
	Self operator--(int);

	// 让迭代器可以比较
	bool operator!=(const Self& s)const;
	bool operator==(const Self& s)const;

private:
	Node* _pNode;
};

此处重点讲解operator++,operator--与之类似。其他操作符重载都很简单,不多赘述。

  • operator++()

红黑树是BST的一种,访问的逻辑为:左子树 → 根 → 右子树。最终是一个升序/降序的整体顺序。

首先,核心思路:“往右走”。从这个核心思路中可以提炼出关键步骤为 找到该节点的“最近邻右节点”

其次,将关键步骤分为三种情况:上右;下右;到达最右。
①如果该节点没有右子树。则顺着上级节点去找,满足如上图所说的条件“↙” (具体可看图中 0007 → 0009 这个例子的讲解) 的节点即为下一个要访问的节点;(上右
②如果该节点有右子树。则下一个节点为该节点右子树的最左节点;(下右
③上述两种情况都不符合,即该节点的上右方向和下右方向都没有节点了,即该节点的右边没有节点了,即该节点为红黑树的最后一个有效节点

operator-- 相比于 operator++ 就是方向不同,即“往走”。同样的,分为三种情况:上左;下左;到达最左。

代码示例:

Self& operator++()
{
	if (_pNode->_pRight)//如果右子树不为空 → 下右 → 右子树最左节点
	{
		Node* pCur = _pNode->_pRight;
		while (pCur->_pLeft)
		{
			pCur = pCur->_pLeft;
		}
		*this = Self(pCur);
		return *this;
	}
	else //如果右子树为空 → 上右 
	{
		Node* pParent = _pNode->_pParent;
		Node* pCur = _pNode;
		while (pParent && pParent->_pRight == pCur)//如果当前节点与上级节点是“右”的关系就不断向上找,并排除上级节点走到头节点的情况
		{
			pCur = pParent;
			pParent = pCur->_pParent;
		}

		//if (pParent == nullptr)//循环结束,可能是一直走到头结点都没找到“上右方向”的节点
		//{
		//	return Self(nullptr);
		//}
		//else//循环结束,可能是 pCur == pParent->_pLeft
		//{
		//	return Self(pParent);
		//}
		*this = Self(pParent);
		return *this;//如果pParent为空则最后会返回由空指针构造的迭代器对象
	}
}

迭代器完整代码:

template<class T>
struct RBTreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef RBTreeIterator<T> Self;

	RBTreeIterator(Node* pNode)
		: _pNode(pNode)
	{}

	// 让迭代器具有类似指针的行为
	T& operator*()
	{
		return _pNode->_data;
	}
	T* operator->()
	{
		return &(_pNode->_data);
	}

	// 前置/后置++  
	Self& operator++()
	{
		if (_pNode->_pRight)//如果右子树不为空 → 下右 → 右子树最左节点
		{
			Node* pCur = _pNode->_pRight;
			while (pCur->_pLeft)
			{
				pCur = pCur->_pLeft;
			}
			*this = Self(pCur);
			return *this;
		}
		else //如果右子树为空 → 上右 
		{
			Node* pParent = _pNode->_pParent;
			Node* pCur = _pNode;
			while (pParent && pParent->_pRight == pCur)
				//如果当前节点与上级节点是“右”的关系就不断向上找,并排除上级节点走到头节点的情况
			{
				pCur = pParent;
				pParent = pCur->_pParent;
			}

			//if (pParent == nullptr)//循环结束,可能是一直走到头结点都没找到“上右方向”的节点
			//{
			//	return Self(nullptr);
			//}
			//else//循环结束,可能是 pCur == pParent->_pLeft
			//{
			//	return Self(pParent);
			//}
			*this = Self(pParent);
			return *this;//如果pParent为空则最后会返回由空指针构造的迭代器对象
		}
	}
	Self operator++(int)
	{
		Self retval = *this;
		operator++();//this->operator++();
		return retval;
	}
	// 前置/后置-- 
	Self& operator--()
	{
		if (_pNode->_pLeft)//如果left子树不为空 → 下left → left子树最right节点
		{
			Node* pCur = _pNode->_pLeft;
			while (pCur->_pRight)
			{
				pCur = pCur->_pRight;
			}
			*this = Self(pCur);
			return *this;;
		}
		else //如果left子树为空 → 上left
		{
			Node* pParent = _pNode->_pParent, *pCur = _pNode;
			while (pParent->_pLeft == pCur && pParent)
				//如果当前节点与上级节点是“left”的关系就不断向上找,并排除上级节点走到头节点的情况
			{
				pCur = pParent;
				pParent = pCur->_pParent;
			}

			//if (pParent == nullptr)//循环结束,可能是一直走到头结点都没找到“上left方向”的节点
			//{
			//	return Self(nullptr);
			//}
			//else//循环结束,可能是 pCur == pParent->_pRight
			//{
			//	return Self(pParent);
			//}

			*this = Self(pParent);//如果pParent为空则最后会返回由空指针构造的迭代器对象
			return *this;
		}
	}
	Self operator--(int)
	{
		Self retval = *this;
		operator--();
		return retval;
	}

	// 让迭代器可以比较
	bool operator!=(const Self& s)const
	{
		return _pNode != s._pNode;
	}
	bool operator==(const Self& s)const
	{
		return _pNode == s._pNode;
	}

private:
	Node* _pNode;
};

4. 封装 set 和 map

1)迭代器封装

通过成员变量去复用红黑树的(成员)函数进行封装。

  • RBTree:
    typedef RBTreeIterator<T> iterator;
    iterator Begin() {...};
    iteraotr End() {...};
  • set:
    typedef typename RBTree<K, KeyofSet<K>>::iterator iterator;
    iterator begin() { return _rb.Begin(); }
    iterator end() { return _rb.End(); }
  • map:
    typedef typename RBTree<pair<const K, V>, KeyofMap<K, V>>::iterator iterator;
    iterator begin() { return _rb.Begin(); }
    iterator end() { return _rb.End(); }

*注:typename

typename 这个关键词,在关于 list的模拟实现 一文中就已经用到过。

作用:明确标识某个标识符是一个类型,以避免编译器的误解。

例如:如下段代码,RBTree<K>::interator 依赖于模板参数 K . 只有在具体实例化模板 K,确定了的具体类型之后,才能明确 RBTree<K>::interator 到底是不是一个类型。譬如,如果 K 最终被实例化为 int ,那么编译器实例化之后,去 RBTree<int> “找” interator能确认这是一个类型。使用 typename 为了告诉编译器无论 K 具体是什么类型, RBTree<K>::interator 在这里应该被理解为一个类型,而不是其他可能的东西(比如成员变量或函数等)。

template<class K>
class set{
    typedef typename RBTree<K>::iterator iterator;
};

2)封装实现map的operator[]

首先,要实现 operator[] 的功能,就要去复用 insert 函数,这就要求 insert 函数的返回一个 pair<iterator, bool> 的类型。

因此,先对红黑树的 insert 函数进行修改:
①更改函数返回值类型 pair<iterator, bool>
②函数体内:return make_pair(xxx, xxx);

基于上述操作,最终可以很容易的实现 map 的 operator[] 重载,代码如下:

namespace RoundBottle
{
	template<class K, class V>
	struct KeyofMap
	{
		const K& operator()(const pair<K, V>& data) { return data.first; }
	};

	template<class K, class V>
	class map
	{
	public:
		typedef typename RBTree<pair<K, V>, KeyofMap<K, V>>::iterator iterator;

		iterator begin() { return _rb.Begin(); }
		iterator end() { return _rb.End(); }

		pair<iterator, bool> insert(const pair<K, V>& data)
		{
			return _rb.Insert(data);
		}

		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = insert(make_pair(key, V()));
			return (ret.first)->second;
		}

	private:
		RBTree<pair<K, V>, KeyofMap<K, V>> _rb;
	};
}

5. Key 值可被修改的问题

按规定,set 和 map 对象中的 Key 值都是不可以被修改的,但基于上述迭代器的实现,Key 值可以被修改。为了解决这一问题,需要进一步优化迭代器的实现。

首先,我们需要实现红黑树的 const_iterator (注意:再次提醒 const_iterator 不是 const 修饰的 iterator,const 真正修饰的是 T,T 是一个模板参数,代指一个数据类型,代指红黑树每个节点中所存储的数据的类型!)

则有:RBtreeIterator<T> → RBtreeIterator<T, T&, T*> and RBtreeIterator<T, const T&,const  T*>

增加 class RBtreeIterator 的模板参数,并相应地修改 opreaotr*() 与 operator->() 的返回值类型(这个操作在以往模拟实现的const迭代器中已经讲解过,此处简要带过)

  • 红黑树
typedef RBtreeIterator<T, T&, T*> iterator;
typedef RBtreeIterator<const T, const T&, const T*> const_iterator;

iterator Begin() {...}
iterator End() {...}
const_iterator Begin() const {...}
const_iterator End() const {...}

⬆️🟠注意:同一作用域,函数名相同,满足一定条件即构成函数重载。注意仅函数返回值类型不同能构成函数重载。成员函数有隐藏的参数——this指针。上述写法中函数参数类型不同而构成函数重载。如果没有 const 修饰 this指针,上述函数是不能构成函数重载的!(或者要么就用不同的函数名,例如用 Begin() 和 cBegin()

  • set:如下代码示例,可以选择使用 const_iterator 来实现 set 的迭代器。这样就可以保护 key 值不被修改。
namespace RoundBottle
{
	template<class K>
	struct KeyofSet
	{
		const K& operator()(const K& data) { return data; }
	};

	template<class K>
	class set
	{
	public:
		typedef typename RBTree<K, KeyofSet<K>>::const_iterator iterator;
		iterator begin()const { return _rb.Begin(); }
		iterator end()const { return _rb.End(); }

		pair<iterator, bool> insert(const K& data) { return _rb.Insert(data); }

	private:
		RBTree<K, KeyofSet<K>> _rb;
	};
}

* 问题:insert 函数返回值类型不匹配

上述代码会带来一个问题:insert 函数返回值类型不匹配。
如上,set 的 insert 函数是复用的红黑树的插入函数,红黑树的 Insert 函数的返回值是 pair<iterator, bool>  →  pair<RBTreeIterator<T, T&, T*>, bool>,而 set 的 insert 函数的返回值为 pair<iterator, bool>  →  pair<RBTree<K, KeyofSet<K>>::const_iterator>, bool>  →  pair<RBTreeIterator<const T, const T&, const T*>, bool>,而RBTreeIterator<T, T&, T*> RBTreeIterator<const T, const T&, const T*>是两个完全不同的类型,例如 vector<int> 和 vector<string>,对于类模板,传不同的模板参数就会实例化出不同的类型。因此,这两个完全不同类型类型之间是无法转换的。

解决方案:将红黑树的 Insert 函数的返回值类型更改为 pair<Node*, bool>
为什么 pair<Node*, bool> → pair<RBTreeIterator<const T, const T&, const T*>, bool> 这个类型转化完成呢?
答:因为 pair 的构造函数中重载了类模板的拷贝构造函数。

template<class U, class V> pair (const pair<U,V>& pr);

即相当于👇 如下代码。如果 U 类型的数据可给 T1 类型复制,V 类型的数据可以给 T2 赋值,则可以通过拷贝构造完成“类型转化”。针对上述问题,pair<Node*, bool> → pair<RBTreeIterator<const T, const T&, const T*>, bool>  即 U 为 Node*,V 为 bool;T1 为 RBTreeIterator<const T, const T&, const T*>,T2 为 bool. 因此,关键在于 Node* → RBTreeIterator<const T, const T&, const T*> 这个转化是可以实现的。(具体可去看 class RBTreeIterator 的实现

tempalte<class T1, class T2>
class pair
{
    template<class U, class V> 
    pair (const pair<U,V>& pr)
        :first(pr.first), second(pr.second)
    {...}
private:
    T1 first;
    T2 second;
}

  •  map:map的处理就比较简单了,直接将 RBTree<pair<K, V>, KeyofMap<K, V>> _rb 改为 RBTree<pair<const K, V>, KeyofMap<K, V>> _rb;
typedef typename RBTree<pair<const K, V>, KeyofMap<K, V>>::iterator iterator;
typedef typename RBTree<pair<const K, V>, KeyofMap<K, V>>::const_iterator const_iterator;

iterator begin() { return _rb.Begin(); }
iterator end() { return _rb.End(); }

const_iterator begin()const { return _rb.Begin(); }
const_iterator end()const { return _rb.End(); }

RBTree<pair<const K, V>, KeyofMap<K, V>> _rb;

综上,基本的封装就完成了。


END

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

畋坪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值