C++ set map 的模拟实现

set 的模拟实现

我们在很早之前就提到过,set 的底层数据结构是红黑树。红黑树的实现一般都是 key-value 的结构。但是我们在使用 set 的时候明明只传入了一个模板参数哇!我们来看库中的实现:

我们可以看到,set 的模板参数 Key 就是存储的数据类型,库中对 Key 进行了重命名:key_type 和 value_type,然后将 key_type 和 value_type 分别作为红黑树的 key 和 value。因此 set 的底层就是存储了两个 key 的红黑树。

我们仿照库里面的实现方式,就可以定义出来我们自己的 set 的结构啦:

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

namespace Tchey
{
    template<class K>
    class set
    {
    public:


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

记得复制一份我们自己实现的红黑树哦!

修改红黑树节点的定义

为了使得一个红黑树的模板类能同时适配出来 map 和 set 我们需要对红黑树节点的定义做修改!

在上面的 set 类中成员属性是这么写的:RBTree<K, K> _t。在使用 map 的时候我们知道 map 的数据是以 key-value 的形式存储的。那 map 类中成员属性该怎么写呢?是这样吗:RBTree<K, V> _t。我们先来看我们之前实现的红黑树对于节点的定义:

cpp 复制代码
template<class K, class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;

	pair<K, V> _kv;
	Color _col; //节点的颜色

	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_col(RED) //节点默认的颜色是红色
	{}
};

这是以 key-value 的形式定义的红黑树节点。如果 map 就是上面那样定义的话,那么节点中的 K 和 V 都是存储的数据。

  • 当我们在实现红黑树的迭代器的时候,重载 * 运算符的返回值就很难书写,对于 set 返回 K 就行,但是对于 map 就必须返回 pair<K, V> 无法做到统一,那么意味着你就需要对 set 写一个红黑树迭代器的实现,map 写一个红黑树迭代器的实现。就会有大量重复的代码,这是不被容忍的!
  • 其次这么写在 set 插入数据的时候就必须插入一个 pair<K, K> 导致红黑树中的节点中存储了冗余的数据。

因此我们就需要对红黑树的节点稍作修改,使得一个红黑树就能适配出 map 和 set。在我们之前实现其他容器的时候我们已经积累了相关的经验,只要不一样,直接将不一样的地方提炼成模板参数就行啦,根据传入参数的不同,实例化出来不同的类型。

于是我们对红黑树节点做出如下修改:

cpp 复制代码
template<class T>
struct RBTreeNode
{
	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;

	T _data;
	Color _col; //节点的颜色

	RBTreeNode(const T& data)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_data(data)
		,_col(RED) //节点默认的颜色是红色
	{}
};

修改红黑树的成员属性

RBTree 的实现中,成员变量是红黑树的根节点,当时我们是这样定义的:

cpp 复制代码
template<class K, class V>
class RBTree
{
    typedef RBTreeNode<K, V> Node;
    Node* _root;
}

现在我们修改了红黑树节点的定义自然不能这么写了!联想到 set 中是这么使用红黑树的:RBTree<K, K> _t 看上去定义 Node 既可以传 K 也可以传 V。但是为了适配 map,我们会选择传递 V 过去,像下面这样:

cpp 复制代码
template<class K, class V>
class RBTree
{
    typedef RBTreeNode<V> Node;
    Node* _root;
}

改到这里,想必你就明白了 map 中的红黑树应该怎么定义了:真正的数据类型是 RBTree 的第二个模板参数,因此 map 中应该这么使用红黑树:RBTree<K, pair<K, V>> _t

通过这样的方式构造出来的红黑树节点,里面的数据既不会冗余,也可以做到同时适配 map 和 set。

迭代器实现

红黑树,节点的物理空间不连续,无法使用原生指针作为红黑树的迭代器,需要对节点进行封装,构造一个迭代器类!同 list 的迭代器,因为我们不仅要实现普通的迭代器,还需要实现 const 的迭代器,所以还需要加上两个模板参数,用来控制 operator*operator-> 返回不同的值。

下面就是最基本的结构啦:

cpp 复制代码
template<class T, class Ref, class Ptr>
struct __TreeIterator
{
	typedef RBTreeNodeL<T> Node;
	typedef __TreeIterator<T, Ref, Ptr> self;

	Node* _node;
};

构造函数

在等会实现 begin,end 等接口时都需要构造迭代器返回。迭代器的构造显然是通过节点的指针来的!

cpp 复制代码
template<class T, class Ref, class Ptr>
struct __TreeIterator
{
	typedef RBTreeNodeL<T> Node;
	typedef __TreeIterator<T, Ref, Ptr> self;
	__TreeIterator(Node* node)
		:_node(node)
	{}

	Node* _node;
};

Ref operator*() 和 Ptr operator->()

有了前面的铺垫,我们知道 operator*operator-> 是可以直接适配 map 和 set 的,直接上手写就行啦。

cpp 复制代码
Ref operator*()
{
    return _node->_data;
}

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

bool operator!=(const self& s) const

判断两个迭代器是不是不想等,就是判断节点的指针是否相等!

cpp 复制代码
bool operator!=(const self& s) const
{
    return _node != s._node;
}

self& operator++()

我们通过例子来看看 operator++() 的逻辑是怎么样的?在 set 的学习部分我们知道了,使用迭代器遍历 set 之后得到的是一个有序的序列,显然迭代器遍历的顺序就是走的中序遍历。

如下图所示的例子:iterator 对应的节点是 13 那么加加之后显然就是 15。我们来抽一下,假设 15 是一颗红黑树,而不是单纯的一个节点!那么 13 这个节点对应的迭代器加加之后,就是右子树对应的最左节点。

经过这么抽象,我们就得到了 operator++ 的关键操作之一:如果当前迭代器对应的节点的右子树不为空,那么加加之后的结果就是右子树的最小节点(右子树的最左节点)。

那么如果右子树为空怎么办呢?

我们来看下面的情况,iterator 对应的节点值为 22,那么这个位置的迭代器加加之后显然就是 25 这个节点对应的迭代器。根据这个现象,如果当前 iterator 对应节点的右子树为空,那么就是父节点对应位置的迭代器嘛?

答案是不完全是哦!我们来看下面的情况:

这个 iterator 对应的节点为 15,那么加加之后该指向哪个节点呢?是 13 吗?根据中序遍历的结果来看,加加之后正确的结果应该是 17 这个节点的迭代器嘛!

显然,当右子树为空的时候,父节点不一定是当前位置的迭代器加加之后的结果。只有当 当前节点位于父节点的左侧的时候,加加的结果才是父节点对应的迭代器。如果当前节点位于父节点的右侧,那么就要向上跟新 cur(当前节点) 和 parent(父节点) 直到 cur(当前节点) 位于 parent(父节点) 的左侧,此时的父节点就是加加之后的结果。

cpp 复制代码
Self& operator++()
{
    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--()

减减的逻辑和加加的逻辑差不多!减减就是先判断左子树是不是为 nullptr,如果不为空那么就找到左子树的最右节点(最大节点)。要是当前节点的左子树为空:如果当前节点位于父节点的右侧,那么父节点就是减减之后的结果;如果当前节点位于父节点的左侧,那么就需要向上更新 cur(当前节点) 和 parent(父节点) 直到 cur 位于 parent 的右侧,此时 parent 对应位置的迭代器就是减减之后的结果。

cpp 复制代码
Self& operator--()
{
    if (_node->_left) //如果左子树不为空,找到左子树的最大节点(最右节点)
    {
        Node* subRight = _node->_left;
        while (subRight->_right)
        {
            subRight = subRight->_right;
        }

        _node = subRight;
    }
    else
    {
        // 当前节点的左子树为空
        Node* cur = _node;
        Node* parent = cur->_parent;
        //找到cur 位于 parent 右侧的那个节点,这个 parent 就是减减之后的结果
        while (parent && cur == parent->_left)
        {
            cur = cur->_parent;
            parent = parent->_parent;
        }
        _node = parent;
    }

    return *this;
}

红黑树的 begin()

begin 返回的迭代器就是整颗红黑树中最小的那个节点,也就是整颗红黑树中最左侧的那个节点。

cpp 复制代码
iterator begin()
{
    Node* cur = _root;
    while(cur && cur->_left)
    {
        cur = cur->_left;
    }
    return cur; //单参数的构造函数支持隐士类型转化
}

//这是 const 的版本
const_iterator end() const
{
    return nullptr;
}

红黑树的 end()

end 返回的迭代器可以是用 nullptr 构造出来的迭代器。

cpp 复制代码
iterator end()
{
    return nullptr;
}

const_iterator end() const
{
    return nullptr;
}

set 的 begin 和 end

还记得我们在 set 的使用部分讲的,set 中的元素为什么不能被修改嘛。因为无论是普通的迭代器还是 const 迭代器,本质上都是红黑树的 const 迭代器,所以通过迭代器来修改 set 中的值是不现实的。根据这个逻辑,我们自己就能实现 set 的 begin 和 end 啦。

cpp 复制代码
iterator begin()
{
    return _t.begin();
}

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

const_iterator begin() const
{
    return _t.begin();
}

const_iterator end() const
{
    return _t.end();
}

因为在 set 中无论是 iterator 还是 const_iterator 实际上都是 const_iterator,所以不加 const 版本的 begin()end() 可以不写的。

红黑树的 pair<iterator, bool> Insert(const T& data)

我们之前实现的红黑树 insert 的返回值都是 bool 类型的,我们需要在每一个返回的位置都进行处理。

第一个要处理的位置:寻找插入节点的比较方式。之前的比较是已经确定了插入的数据是一个 pair,因此用插入数据的 first 和 节点存储的 pair 的 first 比较就可以确定插入位置。


然而,为了同时适配 map 和 set 我们的存储类型已经变成了一个模板参数 T。对于 set,红黑树节点存储 的数据类型 T 就是实例化 set 是传入的那个一个类型;对于 map,红黑树节点存储的数据类型就是一个 pair,因此在比较的时候就没有办法做到统一,怎么解决呢?

答案就是仿函数!!!

我们可以给 RBTree 增加一个模板参数 KeyOfT,来获取到不同 T 对应的 key 值。你可能会说 RBTree 不是有一个 K 的模板参数嘛,能直接用到这里嘛,当然是不能的哦!K 仅仅是一个类型,不是节点存储的数据哦!


我们来看看 set 怎么实例化红黑树的:

cpp 复制代码
template<class K>
class set
{
    struct SetKeyOfT
    {
        const K& operator()(const K& key)
        {
            return key;
        }
    };
public:

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

调用 operator() 的时候,根据如果传入的是 K 类型的数据,直接返回传入的 key 就行啦。但是如果传入的是 pair<K, V> 类型的数据,那么就需要返回传入数据的 first 这才是我们比较需要的 Key 值。这个处理逻辑就是 map 的啦!

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

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

处理之后的结果就是这样啦,接下来我们只需要在 set 的实现中复用红黑树的 Insert 接口就行啦。

cpp 复制代码
template<class K, class T, class KeyOfT>
class RBTree
{
	typedef RBTreeNode<T> Node;
public:
	typedef __TreeIterator<T, T&, T*> iterator;
	typedef __TreeIterator<T, const T&, const T*> const_iterator;

public:
	
	iterator begin()
	{
		Node* cur = _root;
		while(cur && cur->_left)
		{
			cur = cur->_left;
		}
		return cur; //单参数的构造函数支持隐士类型转化
	}

	const_iterator begin() const
	{
		Node* cur = _root;
		while(cur && cur->_left)
		{
			cur = cur->_left;
		}
		return cur; //单参数的构造函数支持隐士类型转化
	}

	iterator end()
	{
		return nullptr;
	}

	const_iterator end() const
	{
		return nullptr;
	}

	
	pair<iterator, bool> Insert(const T& data)
	{
		//插入的时候如果根节点为 nullptr, 申请一个节点,将该节点的颜色改为黑色之后作为根节点就行啦
		if (_root == nullptr)
		{
			_root = new Node(data);
			_root->_col = BLACK;
			return make_pair(iterator(_root), true);
		}
		//记录父节点
		Node* parent = nullptr;
		Node* cur = _root;
		KeyOfT kot; //实例化一个对象用来调用 operator()
		while (cur) //找到新节点的插入位置
		{
			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 make_pair(iterator(cur), false);
			}
		}
		//申请新的节点
		cur = new Node(data);

		Node* newNode = cur; //记录一下新插入的节点,方便构造 

		//解耦操作
		cur->_col = RED;
		//确定新的节点插入到那个位置,左孩子还是右孩子
		if (kot(parent->_data) < kot(data))
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		//向上链接父节点
		cur->_parent = parent;

		//调整节点的颜色,使之满足红黑树的性质,只有 parent 的颜色为红才会调整节点的颜色,parent 为黑就直接完成插入了嘛
		while (parent && parent->_col == RED)
		{
			//祖父节点
			Node* grandfather = parent->_parent;
			//这里根据父节点在祖父节点的位置进行分类,因为我们要确定 uncle 的位置嘛,这样分类代码比较简洁
			//parent 位于 grandparent 的左侧
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				// uncle 存在且为红
				if (uncle && uncle->_col == RED)
				{
					// 变色
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续向上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else // uncle 不存在 或 uncle 存在且为黑
				{
					if (cur == parent->_left)
					{
						//     g
						//   p
						// c
						//右单旋
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						//     g
						//   p
						//		c
						//左右双旋
						RotateL(parent);
						RotateR(grandfather);

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

					break; //一旦经过旋转调整颜色之后就一定满足红黑树的性质了,直接结束循环
				}
			}
			else // parent == grandfather->_right
			{
				//找到叔叔节点
				Node* uncle = grandfather->_left;
				// uncle 存在且为红
				if (uncle && uncle->_col == RED)
				{
					// 变色
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续向上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (cur == parent->_right)
					{
						// g
						//	  p
						//       c
						//左单旋
						RotateL(grandfather);
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						// g
						//	  p
						// c
						//右左双旭那
						RotateR(parent);
						RotateL(grandfather);
						//调整颜色
						cur->_col = BLACK;
						grandfather->_col = RED;
					}

					break; //一旦经过旋转调整颜色之后就一定满足红黑树的性质了,直接结束循环
				}
			}
		}

		_root->_col = BLACK; // 无论根节点的颜色是否在调整的过程中变成了红色,最后我们都将根节点的颜色变为黑色,方便写代码

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

set 的 insert()

你可能会说,set 里面的 insert 直接复用红黑树里面的 insert 不就行了吗?我们直接来试试:

我们发现无法编译通过,说什么无法转化。这是什么原因呢?其实就是 set 的 iterator 有问题。因为 set 的 iterator 就是 const_iterator 调用红黑树里面的 insert 接口,返回的是一个普通的迭代器,而 set 中的insert 返回的实际上是一个 const_iterator。虽然红黑树中的 iterator 与 const_iterator 来自与同一个类模板,但是因为传入模板参数的不同,压根就是两个不同的类型,不能转化实属正常。

你会怎么解决这个问题呢?可以将红黑树中的 insert 的返回值改成 const_iterator 嘛?这么改的确解决了 set 的问题。map 怎么办?要是 map 调用红黑树中的 insert 得到的就是个 const_iterator,但是 map 需要的是 iterator 哇!这种办法行不通。

看来就只能提供一个 iterator 到 const_iterator 的转换函数啦!怎么实现呢?

cpp 复制代码
template<class T, class Ref, class Ptr>
struct __TreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef __TreeIterator<T, Ref, Ptr> self;
	__TreeIterator(Node* node)
		:_node(node)
	{}

	typedef __TreeIterator<T, T&, T*> iterator;

	__TreeIterator(const iterator& it)
		:_node(it._node)
	{}
}

我们增加了一个看上去很像拷贝构造的构造函数。__TreeIterator 中的 iterator 模板参数的类型是 __TreeIterator<T, T&, T*>。说明这个 iterator 无论模板参数 RefPtr 传入什么都是非 const 的iterator。当 RefPtr 分别传入 T&T* ,那么 __TreeIterator(const iterator& it) 就是一个拷贝构造函数,因为 iterator 与类实例化出来的对象时一个类型嘛;当 RefPtr 分别传入 const T&const T* ,那么 __TreeIterator(const iterator& it) 就是一个将非 const 的迭代器转化为 const 迭代器的构造函数。是不是相当美妙。

通过添加这个看上去十分像拷贝构造的函数,就可以实现同时适配 map 和 set 啦!

测试通过,set 模拟实现完成。

map 的模拟实现

在 set 的模拟实现部分,我们已经将红黑树改成同时适配 map 和 set 的结构了,因此 map 的实现直接调用红黑树对应的接口就好啦!

V& operator[]

map 的使用部分浅浅的讲过。opertor[] 就是先调用 insert 函数,如果说插入成功返回插入成功的节点对应的 value 值就可以;如果插入失败,返回与新插入节点 key 值相同的那个节点对应的 value 值就行。这恰好根 insert 的返回值对应上了,我们只需要返回 insert 的返回值中 first 迭代器对应节点的 value 值就大功告成了。

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

map 的代码

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

namespace Tchey
{
    template<class K, class V>
    class map
    {
        struct MapKeyOfT
        {
            const K& operator()(const pair<K, V>& kv)
            {
                return kv.first;
            }
        };
    
    typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::iterator iterator;
    typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::const_iterator const_iterator;

    public:
        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, V>& kv)
        {
            return _t.Insert(kv);
        }
        
        V& operator[](const K& key)
        {
            pair<iterator, bool> ret = insert(make_pair(key, V()));
            return ret.first->second;
        }

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

测试:

到这里 map 和 set 的模拟实现就完成啦,还是比较有意思的,对吧。​😜

相关推荐
浮生如梦_1 小时前
Halcon基于laws纹理特征的SVM分类
图像处理·人工智能·算法·支持向量机·计算机视觉·分类·视觉检测
XiaoLeisj2 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
励志成为嵌入式工程师3 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉3 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer3 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq3 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
wheeldown4 小时前
【数据结构】选择排序
数据结构·算法·排序算法
记录成长java5 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山5 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
青花瓷5 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode