C++修炼:map和set的封装

Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!

我的博客: <但凡.

我的专栏: 《编程之路》《数据结构与算法之美》《题海拾贝》《C++修炼之路》

这期我们模拟实现一下基于红黑树的map和set。

目录

1、封装set和map,解决KeyOfT

2、iterator

3、key不支持修改的问题

4、operator[]


1、封装set和map,解决KeyOfT

首先我们先把set和map的大类实现一下:

map:

cpp 复制代码
#pragma once
#include"RBTree.h"

template<class K,class V>
class map
{
public:

private:
	RBTree<K, pair<K, V>> _t;

};

set:

cpp 复制代码
#pragma once
#include"RBTree.h"

template<class K>
class set
{
public:

private:
	RBTree<K, K> _t;
};

对于传入红黑树的两个参数,第一个我们传入关键值key,第二个传入我们存储的实际类型,如果是key就再传入一次,如果是key_value就传入pair类型。

我们需要一个KeyOfT,将key_value里面的key值取出来,因为我们不知道存储的值到底是key还是key_value类型,所以对于map和set我们都需要一个对应的KeyOfT。

map:

cpp 复制代码
struct MapKeyOfT
{
	const K& operator()(const pair<K, V>& kv)
	{
		return kv.first;
	}
};

set:

cpp 复制代码
struct SetKeyOfT
{
	const K& operator()(const K& key)
	{
		return key;
	}
};

这样的话,我们在传入红黑树时需要往模板中传入三个参数,所以我们对红黑树进行改造:

cpp 复制代码
template<class K,class T,class KeyOfT>
class RBTree
{
	typedef  RBTreeNode<T> Node;
	
public:
	
	pair<Iterator,bool> Insert(const T& data)
	{
		if (_root == nullptr)
		{
			_root = new Node(data);
			_root->_col = BLACK;//根节点必须是黑
			return { Iterator(_root,_root),true };
		}
		KeyOfT kot;
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			//这访函数返回T值
			if (kot(cur->_data) < kot(data))
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (kot(cur->_data) > kot(data))
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return { Iterator(cur,_root),false };//隐式类型转换
			}
		}

		cur = new Node(data);
		Node* newnode = cur;
		cur->_col = RED;//新增的必须是red

		if (kot(parent->_data) < kot(data))
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_parent = parent;

		while (parent && parent->_col == RED)//如果满足这个情况要一直向上走
		{
			Node* grandfather = parent->_parent;
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				

				if (uncle && uncle->_col == RED)//仅调色
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					cur = grandfather;

					parent=cur->_parent;
				}
				else
				{
					//u不存在或u存在且为黑
					//单边高
					if (cur == parent->_left)
					{
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					//对应的是AVL树左边右边高的情况
					else
					{
						RotateL(parent);
						RotateR(grandfather);

						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
			else
			{
				Node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED)
				{
					uncle->_col = BLACK;
					parent->_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;
				}
			}
		}
		_root->_col = BLACK;

		return { Iterator(newnode,_root),false };
	}
	
    ......
	Iterator Find(const K& key)
	{
		KeyOfT kot;
		Node* cur = _root;
		while (cur)
		{
			if (kot(cur->_data) < key)
			{
				cur = cur->_right;
			}
			else if (kot(cur->_data) > key)
			{
				cur = cur->_left;
			}
			else
			{
				return Iterator(cur,_root);
			}
		}
		return End();
	}

	private:
		
		......
private:
		Node* _root = nullptr;
};

为了节省篇幅我把所有不涉及改动的函数都先去掉了。另外insert里面涉及的迭代器我们之后再说。

2、iterator

**接着我们实现map和set的迭代器。**首先我们在红黑树所在的头文件中把迭代器定义出来:

cpp 复制代码
template<class T,class ptr,class ref>
class RBTreeIterator
{
public:
	typedef RBTreeNode<T> Node;
	typedef RBTreeIterator<T,ptr,ref> Self;

	RBTreeIterator(Node* node, Node* root)
		:_node(node),
		_root(root)
	{}
	ref operator*() 
	{
		return _node->_data;
	}
	ptr operator->()
	{
		return &_node->_data;
	}
	bool operator==(const Self& s) const
	{
		return _node == s._node;
	}
	bool operator!=(const Self& s) const
	{
		return _node != s._node;
	}
	Self& operator++()
	{
		if (_node->_right)
		{
			//节点右不为空,下一个节点就是右子树中序第一个(最左节点
			Node* minleft = _node->_right;
			while (minleft->_left)
			{
				minleft = minleft->_left;
			}
			_node = minleft;
		}
		else
		{
			//节点右为空,找找祖先,并且该祖先是其父亲的左
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && cur == parent->_right)//parent是空也结束
			{
				cur = parent;
				parent = parent->_parent;
			}
			//如果parent为nullptr,nullptr为end()
			_node = parent;
		}
		return *this;
	}
	Self& operator--()
	{
		if (_node == nullptr) // end()
		{
			// --end(),特殊处理,⾛到中序最后⼀个结点,整棵树的最右结点 
			Node* rightMost = _root;
			while (rightMost && rightMost->_right)
			{
				rightMost = rightMost->_right;
			}

			_node = rightMost;
		}
		else if (_node->_left)
		{
			// 左⼦树不为空,中序左⼦树最后⼀个 
			Node* rightMost = _node->_left;
			while (rightMost->_right)
			{
				rightMost = rightMost->_right;
			}

			_node = rightMost;
		}
		else
		{
			// 孩⼦是⽗亲右的那个祖先 
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && cur == parent->_left)
			{
				cur = parent;
				parent = cur->_parent;
			}

			_node = parent;
		}

		return *this;
	}

private:
	Node* _node;
	Node* _root;
};

对于迭代器这个类,++,--两个操作需要特殊说明一下。

对于++操作,首先,因为我们map和set++是有序数的下一个,对应的就是红黑树的中序遍历的下一个节点。那么我们不可能因为一个++就遍历整个红黑树去找下一个节点吧?所以我们分情况讨论一下,看看红黑树的下一个节点理应出现在哪。**如果当前节点右子树不为空,下一个节点就应该是右子树的最左节点。如果当前节点的右子树为空,下一个节点就应该是父节点,并且该父节点一定是他的父节点(父节点的父节点)的左子树。**如果一直查找直到nullptr,就说明++之后应该是end,那就返回nullptr就可以了。

对于--操作,首先我们要对当前节点是end()的情况做特殊处理,这个节点的上一个节点应该是整棵树的最右节点。第二种情况就是左子树不为空,如果左子树不为空,说明他的上一个节点为左子树的最右节点。第三种情况就是左子树为空,此时他的上一个节点应该是父节点,并且此父节点一定是他的父节点(父节点的父节点)的右子节点。

接下来我们对红黑树进行改造,新增迭代器的部分:

cpp 复制代码
typedef  RBTreeIterator<T,T*,T&> Iterator;
typedef  RBTreeIterator<T,const T*,const T&> ConstIterator;
Iterator Begin()
{
	Node* minleft = _root;
	while (minleft && minleft->_left)//这棵树可能是空的
	{
		minleft = minleft->_left;
	}
	return Iterator(minleft,_root);
}
Iterator End()
{
	return Iterator(nullptr,_root);
}
ConstIterator Begin() const
{
	Node* minleft = _root;
	while (minleft && minleft->_left)//这棵树可能是空的
	{
		minleft = minleft->_left;
	}
	return ConstIterator(minleft, _root);
}
ConstIterator End() const
{
	return ConstIterator(nullptr, _root);
}

接下来我们对insert进行改造,使他的返回值和源码一样:

cpp 复制代码
pair<Iterator,bool> Insert(const T& data)
{
	if (_root == nullptr)
	{
		_root = new Node(data);
		_root->_col = BLACK;//根节点必须是黑
		return { Iterator(_root,_root),true };
	}
	KeyOfT kot;
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		//这访函数返回T值
		if (kot(cur->_data) < kot(data))
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (kot(cur->_data) > kot(data))
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			return { Iterator(cur,_root),false };//隐式类型转换
		}
	}

	cur = new Node(data);
	Node* newnode = cur;
	cur->_col = RED;//新增的必须是red

	if (kot(parent->_data) < kot(data))
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
	cur->_parent = parent;
	while (parent && parent->_col == RED)//如果满足这个情况要一直向上走
	{
		Node* grandfather = parent->_parent;
		if (parent == grandfather->_left)
		{
			Node* uncle = grandfather->_right;
			

			if (uncle && uncle->_col == RED)//仅调色
			{
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;

				cur = grandfather;//继续向上调整

				parent=cur->_parent;
			}
			else
			{
				//单边高
				if (cur == parent->_left)
				{
					RotateL(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)
			{
				uncle->_col = BLACK;
				parent->_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;
			}
		}
	}

	_root->_col = BLACK;

	return { Iterator(newnode,_root),false };
}

接下来我们再去map和set的头文件中,对迭代器进行进一步的封装:

map:

cpp 复制代码
typedef typename RBTree < K, pair<K, V>, MapKeyOfT>::Iterator iterator;
typedef typename RBTree < K, pair<K, V>, MapKeyOfT>::ConstIterator const_iterator;
pair<iterator,bool> insert(const pair<K, V>& kv)
{
	return _t.Insert(kv);
}
iterator begin()
{
	return _t.Begin();
}
iterator end()
{
	return _t.End();
}
const_iterator begin() const 
{
	return _t.Begin();
}
const_iterator end() const 
{
	return _t.End();
}
iterator find(const K& key)
{
	return _t.Find(key);
}

set:

cpp 复制代码
typedef typename RBTree < K, K, SetKeyOfT>::Iterator iterator;
typedef typename RBTree < K, K, SetKeyOfT>::ConstIterator const_iterator;
pair<iterator, bool> insert(const K& key)
{ 
	return _t.Insert(key);
}
iterator begin()
{
	return _t.Begin();
}
iterator end()
{
	return _t.End();
}
const_iterator begin() const
{
	return _t.Begin();
}
const_iterator end() const
{
	return _t.End();
}
iterator find(const K& key)
{
	return _t.Find(key);
}

注意这里我们typedef的迭代器一定要是在红黑树的大类中我们已经封装好的迭代器,不然没有对应的Begin,End的操作。并且我们要用typename标记,不然编译器不知道iterator到底是一个类还是一个变量。

3、key不支持修改的问题

这个问题就很好解决了,对于set,我们只需要在第二个参数加上const 就行了:

cpp 复制代码
RBTree<K,const K, SetKeyOfT> _t;

对于map,我们在第二个参数pair中的key值之前加上const:

cpp 复制代码
RBTree<K, pair<const K, V>, MapKeyOfT> _t;

4、operator[]

**这里的[]本质还是对insert的复用,因为[]本身也可以插入,如果插入成功,就返回新插入的值对应的value值,如果插入失败,就返回原有的该值的value值(具体可以看map和set的使用这一篇)。**那这个操作完全可以通过复用insert来实现:

map:

cpp 复制代码
V& operator[](const K& key)
{
	pair<iterator, bool> t = insert({ key ,V()});//int的缺省值是0
	auto it = t.first;
	return it->second;//这里会省略一个->
}

set:

set不支持operator[]。

好了,到这里我们就基本实现完了map和set,现在解决大部分读者在学习这里的时候会遇到的一个疑问。为啥模板类中要传入一个K再传入一个T呢?假设在set中,我们存储的值类型为int,那么传两个int进来不是妥妥的重复吗?

**首先,我们模拟实现要和源码尽可能保持一致,源码是这么写的,所以我们也这么写。**那么源码为什么要这么写呢?

确实,对于set来说就是重复传了。但是对于map来说,第一个K的作用是指示key值的类型。因为find和erase操作的函数参数都是K类型的。也就是说**我们得用一个类型来指示我们形参是什么类型的。**假设map中我们只传一个pair进来,我们根本无法确定key值的类型是什么。

并且,本着能用一套逻辑就用一套逻辑的原则,通过传一个key的类型来解决这个问题,总比写两套逻辑,一个是key类型红黑树,一个是key_value类型红黑树要方便得多。

完整代码放在我的gitee了:

RBTree · 主线学习简单项目 - 码云 - 开源中国

好了,今天的内容就分享到这,我们下期再见!

相关推荐
UpUpUp……1 分钟前
C++复习
开发语言·c++·笔记
BC的小新7 分钟前
C++ Stack&Queue
c++
charlie11451419140 分钟前
从C++编程入手设计模式1——单例模式
c++·单例模式·设计模式·架构·线程安全
菠萝012 小时前
分布式CAP理论
数据库·c++·分布式·后端
1白天的黑夜14 小时前
动态规划-152.乘积最大子数组-力扣(LeetCode)
c++·算法·leetcode·动态规划
?!7145 小时前
网络编程之网络编程预备知识
linux·网络·c++
Tony__Ferguson5 小时前
数据结构——优先级队列(PriorityQueue)
android·java·数据结构
理论最高的吻5 小时前
1614. 括号的最大嵌套深度【 力扣(LeetCode) 】
c++·算法·leetcode·职场和发展·字符串··字符匹配
Daking-6 小时前
「动态规划::状压DP」网格图递推 / AcWing 292|327(C++)
c++·算法·动态规划
hjjdebug6 小时前
c/c++怎样编写可变参数函数.
c++·args...·可变参数函数