红黑树模拟实现STL中的map与set

1.map

在C++标准模板库(STL)中,std::map是一种非常实用且强大的容器,它提供了键值对的存储机制。这使得std::map成为处理具有唯一关键的关联数据的理想选择。

1.1 map的特性

1、键值对存储:std::map通过键值对的形式存储数据,其中每个键都是唯一的,并且与一个值相关联。
2、自动排序:std::map内部使用一种平衡二叉搜索树(通常是红黑树)来存储元素,这使得元素根据键自动排序。
3、元素唯一性:在std::map中,键必须是唯一的。如果尝试插入一个已经存在的键,插入操作将失败。
4、直接访问:可以使用键直接访问std::map中的元素,这提供了高效的查找能力。
5、灵活的元素操作:std::map提供了丰富的元素操作,包括插入、删除、查找等。

1.2 map的性能

1、插入操作:插入操作的时间复杂度为O(log n),其中n是std::map中元素的数量。这是因为需要在平衡二叉树中找到合适的位置来插入新元素。
2、查找操作:查找操作的时间复杂度也是O(log n),由于std::map的有序性,可以快速定位到任何键。
3、删除操作:删除操作的时间复杂度同样为O(log n),需要找到要删除的元素并在保持树平衡的同时移除它。
4、遍历操作:遍历std::map的时间复杂度为O(n),因为需要访问容器中的每个元素。

1.3 C++标准库中map的基本用法

#include<iostream>
#include<map>
using namespace std;

int main()
{
	//创建一个map,键和值都是int类型
	map<int, int> myMap;

	//插入元素
	myMap.insert(make_pair(1, 100));
	myMap.insert({ 2,200 });
	myMap[3] = 300;//使用下标操作符直接插入或修改
	myMap.insert({ 4,400 });

	//访问元素
	cout << "Element with key 2:" << myMap[2] << endl;

	//迭代元素
	cout << "Iterating over elements:" << endl;
	for (const auto& pair : myMap)
	{
		cout << pair.first << "=>" << pair.second << endl;
	}

	//查找元素
	auto search = myMap.find(2);//查找键位2的元素
	if (search != myMap.end())
	{
		cout << "Found element with key 2:" << search->second << endl;
	}
	else
	{
		cout << "Element with key 2 was not found." << endl;
	}

	//删除元素
	myMap.erase(2);//删除键为2的元素
	cout << "Element with key 2 erased." << endl;

	//再次遍历,查看删除效果
	cout << "iterating over elements after deletion:" << endl;
	for (const auto& pair : myMap)
	{
		cout << pair.first << "=>" << pair.second << endl;
	}

	return 0;
}

1.4 map工作原理

std::map的内部实现通常基于红黑树,红黑树相关介绍可参考文章红黑树的实现,红黑树自身支持排序,且我们实现的红黑树支持插入键值对。

2.set

std::set是C++标准模板库(STL)中提供的有序关联容器之一。它基于红黑树(Red-Black-Tree)实现,用于存储唯一的元素,并按照元素的值进行排序。

2.1set的特性

1、唯一性:std::set中不允许存储重复的元素,每个元素都是唯一的。插入重复元素的操作会被忽略。
2、有序性:std::set中的元素是按照升序进行排序的。这种排序是通过红黑树的自平衡性质实现的,保证了插入、删除等操作的高效性。
3、插入元素:使用insert成员函数可以将元素插入到集合中,如果元素已经存在,则插入操作会被忽略。

2.2 C++标准库中set的基本用法

#include<iostream>
#include<set>
using namespace std;

int main()
{
	//创建std::set对象
	std::set<int>mySet;

	//插入元素
	mySet.insert(42);
	mySet.insert(21);
	mySet.insert(63);
	mySet.insert(21);

	//删除元素
	mySet.erase(63);

	//查找元素
	auto it = mySet.find(42);
	
	for (const auto& element : mySet)
	{
		cout << element << " ";
	}
	cout << endl;

	return 0;
}

3.map和set类模板

在同时封装set和map时,面临的第一个问题是:两者的参数不匹配。

set只需要key,map则需要key和value。用红黑树同时封装出set和map时,set传给value的是一个value,map传给value的是一个pair,set和map传给红黑树的value决定了这棵树里面存的节点值类型。上层容器不同,底层红黑树的key和value也不同。

参考STL库中的解决方案:不论是k还是k和v,都看作是value_type,获取key值时再使用别的方法解决。如下图所示。其中rb_tree的参数3就是获取key的方式,也就是上文提到的解决办法,后文会有介绍。参数4是比较方式,参数5是空间配置器。

能否省略 参数1 key_type?

对于set来说,可以省略 参数1 key_type,因为冗余了。但是对于map来说,不能省略参数1。因为map中的函数参数类型为key_type,省略后就无法确定参数类型了,比如Find、Erase中都需要key_type这个类型。

在上层容器set中,K和T都代表Key,底层红黑树节点当中存储K和T都是一样的;map中,K代表键值Key,T代表由Key和Value构成的键值对pair,底层红黑树中只能存储T。所以红黑树为了满足同时支持set和map,节点当中存储T。这就需要对红黑树进行改动。

4.红黑树节点的定义

4.1 红黑树节点的修改

原来红黑树节点的定义:

template<class K, class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;//节点的左孩子
	RBTreeNode<K, V>* _right;//节点的右孩子
	RBTreeNode<K, V>* _parent;//节点的父亲节点(红黑树需要旋转,为了实现简单给出该字段)

	pair<K, V> _kv;//节点的值域

	Colour _col;//节点的颜色

	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _col(RED)
	{}
};

这里将红黑树节点中的K-V键值对pair<K,V>修改成类型T,T类型的_data是pair键值对还是单个的值,视情况而定。如果是map的需求,那么就是pair;如果是set的需求,那么就是一个K。如下所示:

template<class T>
struct RBTreeNode
{
	RBTreeNode<T>* _left;//节点的左孩子
	RBTreeNode<T>* _right;//节点的右孩子
	RBTreeNode<T>* _parent;//节点的父亲节点(红黑树需要旋转,为了实现简单给出该字段)

	T _data;//节点的值域

	Colour _col;//节点的颜色

	RBTreeNode(const T& data)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _data(data)
		, _col(RED)
	{}
};

4.2 仿函数

(1)节点比较大小时存在的问题

往红黑树中插入节点时,需要比较节点的大小,我们知道map中的元素是pair类型的关键字-值(key-value)对,关键字起到索引的作用,值则表示与索引相关联的数据。而在set中每个元素只包含一个关键字。比如文章红黑树中实现的查找元素的函数Find,该函数的形参是一个pair类型的关键字-值对,使用关键字first来比较大小。map完全可以借助该红黑树来实现,但是set中的元素只包含一个关键字,故传入到底层的红黑树的Find函数中的参数不是pair类型,所以不能借助该红黑树来实现set。凡是涉及到获取关键字的地方都有这个问题,因为对于map和set来说传入形参中的_data是不确定的,对于这种不确定的类型,一般使用仿函数来解决。

//2、红黑树查找节点
Node* Find(const pair<K, V>& kv)
{
	Node* cur = _root;
	while (cur)
	{
		if (kv.first > cur->_kv.first)
		{
			cur = cur->_right;
		}
		else if (kv.first < cur->_kv.first)
		{
			cur = cur->_left;
		}
		else
		{
			return cur;
		}
	}
 
	return nullptr;
}

(2)解决不同类型的关键字获取的问题

现在可以研究stl库中rb_tree的参数3了,它是一个函数对象,可以传递仿函数,用来从不同的T中获取key值。

set和map有自己各自的仿函数,这样底层的红黑树就能更具仿函数分别获取set和map的关键字。

①map的仿函数

template<class K, class V>
class MyMap
{
	struct MapKeyOfT
	{
		const K& operator()(const pair<K, V>& kv)
		{
			return kv.first;
		}
	};

public:
	bool Insert(const pair<K, V>& kv)
	{
		return _t.Insert(kv);
	}

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

②set的仿函数

template<class K>
class MySet
{
	struct SetKeyOfT
	{
		const K& operator()(const K& k)
		{
			return k;
		}
	};

public:
	bool Insert(const K& k)
	{
		return _t.Insert(k);
	}

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

当我们得到不同的关键字的获取方式后,就可以更改红黑树中相应的代码了,比如查找函数。

//2、红黑树查找节点
	Node* Find(const T& data)
	{
		KOfT koft;
		Node* cur = _root;
		while (cur)
		{
			if (koft(data) > koft(cur->_data))
			{
				cur = cur->_right;
			}
			else if (koft(data) < koft(cur->_data))
			{
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}

		return nullptr;
	}

5.红黑树的迭代器

map和set迭代器的实现本质是红黑树迭代器的实现,迭代器的实现模板类型、模板类型引用、模板类型指针。将红黑树的节点再一次封装,构建一个单独的迭代器类。因为节点的模板参数有K和V,所以迭代器类也需要这两个参数。不同的迭代器传递不同的参数,额外增加Ref和Ptr的目的是为了让普通迭代器和const迭代器能使用同一个迭代器类。其中Ref和Ptr具体是什么类型,取决于调用方传递的参数。

template<class T,class Ref,class Ptr>
struct __TreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef __TreeIterator<T> Self;
	Node* _node;

	__TreeIterator(Node* node)
		:_node(node)
	{}

	Ref operator*()
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &_node->_data;
	}

5.1 移动操作

红黑树的迭代器是一个双向迭代器,只支持++和--操作,树形结构的容器在进行遍历时,默认按中序遍历的顺序进行迭代器移动,因为这样遍历二叉搜索树后,结果为有序。如下图中的二叉树遍历的结果为:5 6 7 8 10 11 12 13 15。

++移动的思路:
1.判断当前节点的右子树是否存在,如果存在,则移动至右子树的最左节点;
2.如果不存在,则移动至当前路径中 孩子节点为左孩子的父亲节点;
3.如果父亲为空,则下一个节点就是空。

具体过程如下:it在节点5位置时,++是走到它的父亲节点6;但是it在节点7的位置时,++是走到它的祖先节点8的位置。it指向的节点5的右为空,节点5的右为空,表明节点5已经被访问完了。由于节点5是其父亲节点的左子树,节点5访问完了,此时该访问节点5的父亲节点,即节点6。此时节点6的右不为空,此时要访问节点6的右子树节点7。节点7的左子树为空,接着访问节点7,下一步访问节点7的右子树,节点7的右子树为空,表明节点7访问完了,此时节点6也访问完了,节点6是其父亲节点的左子树,接着访问节点6的父亲节点,即节点8。最后访问完之后,it指向NULL,则结束。

这里解释两个问题:
1、为什么右子树不为时,要访问右子树的最左节点?
因为此时是中序遍历,路径为左-根-右,如果右边路径存在,就要从它的最左节点开始访问。

2、为什么右子树为空时,要访问当前路径中孩子节点为左孩子的父亲节点?
因为孩子节点为右孩子的父亲节点已经被访问过了。

Self& operator++()
{
	//1、如果节点的右子树不为空,中序的下一个节点就是右子树的最左节点
	//2、如果右为空,表示_node所在的子树已经访问完成,下一个节点在它的祖先中去找;
	//
	if (_node->_right)
	{
		Node* subLeft = _node->_right;
		while (subLeft->_left)
		{
			subLeft = subLeft->_left;
		}

		_node = subLeft;
	}
	else
	{
		Node* cur = _node;
		Node* parent = cur->_parent;
		while (parent&&cur==parent->_right)
		{
			cur = cur->_parent;
			parent = parent->_parent;
		}
		_node = parent;
	}

	return *this;
}

6.map模拟实现

template<class K, class V>
class MyMap
{
	struct MapKeyOfT
	{
		const K& operator()(const pair<const K, V>& kv)
		{
			return kv.first;
		}
	};

public:
	typedef typename RBTree<K, pair<const K,V>, MapKeyOfT>::iterator iterator;

	iterator begin()
	{
		return _t.begin();
	}

	iterator end()
	{
		return _t.end();
	}

	pair<iterator,bool> Insert(const pair<const K, V>& kv)
	{
		return _t.Insert(kv);
	}

	//[]返回key对应的value值
	V& operator[](const K& key)
	{
		//这里的插入操作可能会成功,也可能会失败,如果key已经存在则失败;
		//无论是插入成功还是插入失败,都会返回节点对应的迭代器,所以就能拿到该节点对应的value
		//V()是缺省值,如果插入的是int即为0;如果是string,那么就构造一个空字符串对象
		pair<iterator, bool> ret = _t.Insert(make_pair(key, V()));
		return ret.first->second;
	}

private:
	RBTree<K, pair<const K, V>,MapKeyOfT> _t;
};

7.set模拟实现

template<class K>
class MySet
{
	struct SetKeyOfT
	{
		const K& operator()(const K& k)
		{
			return k;
		}
	};

public:
	typedef typename RBTree<K, K, SetKeyOfT>::iterator iterator;

	iterator begin()
	{
		return _t.begin();
	}

	iterator end()
	{
		return _t.end();
	}

	pair<iterator, bool> Insert(const K& k)
	{
		return _t.Insert(k);
	}

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

8.RBTree完整代码

#include<iostream>

using namespace std;

enum Colour
{
	BLACK,
	RED,
};


template<class T>
struct RBTreeNode
{
	RBTreeNode<T>* _left;//节点的左孩子
	RBTreeNode<T>* _right;//节点的右孩子
	RBTreeNode<T>* _parent;//节点的父亲节点(红黑树需要旋转,为了实现简单给出该字段)

	T _data;//节点的值域,_data可能是一个key值,也可能是一个K-V键值对

	Colour _col;//节点的颜色

	RBTreeNode(const T& data)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _data(data)
		, _col(RED)
	{}
};

template<class T, class Ref, class Ptr>
struct __TreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef __TreeIterator<T, Ref, Ptr> Self;
	Node* _node;

	//节点指针构造迭代器
	__TreeIterator(Node* node)
		:_node(node)
	{}

	//使用普通迭代器构造const迭代器的构造函数
	__TreeIterator(const __TreeIterator<T, T&, T*>& it)
		:_node(it._node)
	{}

	Ref operator*()
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &_node->_data;
	}

	bool operator!=(const Self& s)
	{
		return _node != s._node;
	}

	bool operator==(const Self& s)
	{
		return _node == s._node;
	}

	Self& operator++()
	{
		//1、如果节点的右子树不为空,中序的下一个节点就是右子树的最左节点
		//2、如果右为空,表示_node所在的子树已经访问完成,下一个节点在它的祖先中去找;
		//
		if (_node->_right)
		{
			Node* subLeft = _node->_right;
			while (subLeft->_left)
			{
				subLeft = subLeft->_left;
			}

			_node = subLeft;
		}
		else
		{
			Node* cur = _node;
			Node* parent = cur->_parent;
			while (parent && cur == parent->_right)
			{
				cur = cur->_parent;
				parent = parent->_parent;
			}
			_node = parent;
		}

		return *this;
	}

	Self& operator--()
	{

	}
};

template<class K, class T, class KOfT>
class RBTree
{
	typedef RBTreeNode<T> Node;
public:
	typedef __TreeIterator<T, T&, T*> iterator;
	typedef __TreeIterator<T, const T&, const T*> const_iterator;

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

		return iterator(cur);
	}

	iterator end()
	{
		return iterator(nullptr);
	}

	//1、红黑树插入节点
	pair<iterator, bool> Insert(const T& data)
	{
		//1、按二叉搜索树的规则插入节点
		//如果二叉树为空,则将新插入的节点作为根节点
		if (_root == nullptr)
		{
			_root = new Node(data);
			_root->_col = BLACK;//红黑树的根节点为黑色
			return make_pair(iterator(_root), true);
		}

		KOfT koft;
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (koft(data) > koft(cur->_data))
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (koft(data) < koft(cur->_data))
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return make_pair(iterator(cur), false);
			}
		}

		cur = new Node(data);
		Node* newnode = cur;
		if (koft(cur->_data) > koft(parent->_data))
		{
			parent->_right = cur;
			cur->_parent = parent;
		}
		else if (koft(cur->_data) < koft(parent->_data))
		{
			parent->_left = cur;
			cur->_parent = parent;
		}

		//cur->_col = RED;
		//这里默认新插入的节点是红色节点,为什么呢?
		//因为插入新节点时,就涉及破坏规则2(红黑树中没有连续的红节点);还是规则3(红黑树每条路径都有相同数量的黑节点)。
		//首先,插入红色节点时,不一定会破坏规则2(如果插入节点的父亲节点是黑色节点);即使破坏了规则2,新插入的节点是
		//红色节点也只会影响一条路径。
		//如果新插入的节点是黑色的,会影响二叉树的所有路径,因为红黑树的每条路径都要有相同数量的黑色节点。

		//情况1:cur节点为红色、parent节点为红色、grandfather为黑色、uncle节点存在且为红色;
		//情况2:uncle节点不存在
		//情况3:uncle节点存在且为黑
		while (parent && parent->_col == RED)
		{
			//cur节点的父亲节点parent是红色,此时parent节点不可能是根节点
			//此时看cur节点的叔叔节点uncle的颜色。
			Node* grandfather = parent->_parent;
			if (grandfather->_left == parent)
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					//如果grandfather不是根节点,继续往上处理。
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					//情况3:双旋;先parent节点左旋,转换为情况2
					if (parent->_right == cur)
					{
						RotateL(parent);
						swap(parent, cur);
					}

					//情况2:情况2也可能是情况3进行左单旋之后,需要再进行右单旋
					RotateR(grandfather);
					grandfather->_col = RED;
					parent->_col = BLACK;

					break;
				}
			}
			else
			{
				Node* uncle = grandfather->_left;
				//情况1:uncle存在、且为红
				//情况2 or 情况3:uncle不存在 or uncle存在、且为黑
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					//如果grandfather不是根节点,继续往上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					//情况3
					if (cur == parent->_left)
					{
						RotateR(parent);
						swap(cur, parent);
					}

					//情况2
					RotateL(grandfather);

					grandfather->_col = RED;
					parent->_col = BLACK;
				}
			}
		}

		//最终将根节点变为黑色
		_root->_col = BLACK;

		return make_pair(iterator(newnode), true);
	}



	//2、红黑树查找节点
	iterator Find(const T& data)
	{
		KOfT koft;
		Node* cur = _root;
		while (cur)
		{
			if (koft(data) > koft(cur->_data))
			{
				cur = cur->_right;
			}
			else if (koft(data) < koft(cur->_data))
			{
				cur = cur->_left;
			}
			else
			{
				return iterator(cur);
			}
		}

		return iterator(nullptr);
	}

	//中序遍历
	void InOrder()
	{
		_InOrder(_root);
	}

private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}

	//左单旋
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

		subR->_left = parent;
		Node* grandfather = parent->_parent;
		parent->_parent = subR;

		//如果原来parent是这棵树的根节点,左旋转完成后subR节点变成这棵树的根节点
		if (grandfather == nullptr)
		{
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{
			if (parent == grandfather->_left)
			{
				grandfather->_left = subR;
			}
			else if (parent == grandfather->_right)
			{
				grandfather->_right = subR;
			}

			subR->_parent = grandfather;
		}
	}

	//右单旋
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;

		subL->_right = parent;
		Node* grandfather = parent->_parent;
		parent->_parent = subL;

		if (grandfather == nullptr)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (grandfather->_left == parent)
			{
				grandfather->_left = subL;
			}
			else if (grandfather->_right == parent)
			{
				grandfather->_right = subL;
			}

			subL->_parent = grandfather;
		}
	}

	Node* _root = nullptr;
};

完整代码可参考:set/map的模拟实现

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值