红黑树同时封装实现 map 和 set——红黑树の华丽二重奏_map、set是怎么实现的,红黑树是怎么能够同时实现这两种容器 为什么使用红黑树

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新大数据全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注大数据)
img

正文

	{
		if (parent == parentParent->_left)
		{
			parentParent->_left = subR;
		}
		else
		{
			parentParent->_right = subR;
		}
		subR->_parent = parentParent;
	}
}

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

	//建立subLR与parent之间的联系
	parent->_left = subLR;
	if (subLR)
		subLR->_parent = parent;

	//建立parent与subL之间的联系
	subL->_right = parent;
	parent->_parent = subL;

	//建立subL与parentParent之间的联系
	if (parentParent == nullptr)
	{
		_root = subL;
		_root->_parent = nullptr;
	}
	else
	{
		if (parent == parentParent->_left)
		{
			parentParent->_left = subL;
		}
		else
		{
			parentParent->_right = subL;
		}
		subL->_parent = parentParent;
	}
}

//左右双旋
void RotateLR(Node\* parent)
{
	RotateL(parent->_left);
	RotateR(parent);
}

//右左双旋
void RotateRL(Node\* parent)
{
	RotateR(parent->_right);
	RotateL(parent);
}

Node\* _root; //红黑树的根结点

};


红黑树对两者进行适配的话有点差强人意,我们不妨好好利用红黑树的 KV 模型:



template<class Key,class Value>
class RBTree


对于 set 只有一个 key 我们可以将 kv 同时化为 set 的 key,stl 源码的底层表达出来就是:



typedef Key key_type;
typedef Key value_type;


所以 set 定义出来就是:



template
class set
{
public:
//…
private:
RBTree<K, K> _t;
};


而 map 里面也是一个 KV 模型使用键值对,因此我们不妨将 v 用来表示键值对 pair<K,V>,底层表达出来就是:



typedef Key key_type;
typedef pair<const Key,T> value_type;


所以 map 定义出来就是:



template<class K, class V>
class map
{
public:
//…
private:
RBTree<K, pair<K, V>> _t;
};



> 
> 既然 pair 里面也有 Key,那为什么不能用 map 的封装方式代替 set 呢?
> 
> 
> 


这是比较核心的问题,主观上这样是没有问题的,但是要注意的是红黑树的第一个参数他一定不能省略,对于 set 倒是无关痛痒,两个 key 少一个还能活,但是 map 不行,map 里面的某些接口是只能通过调用 pair ,而某些接口只能通过调用 key,比如 find 、erase


### 数据的存储🤔


底层红黑树包含 KT 两个参数,因为红黑树上层容器的不同,KT 所代表的具体意义也是不同的


对于 set 都是 key,所以底层红黑树中的 KT 都是一样的,但是对于 map 红黑树就只能存储 T 了,又因为底层红黑树并不知道上层容器到底是什么,无法确定的情况下红黑树节点中直接存储 T 就行了。


==set:(RBTree<K, K> \_t;) + map :(RBTree<K, pair<K, V>> \_t;) ==



template<class K,class T>
class RBTree
{
typedef RBTreeNode(T) Node;
public:
//……
private:
Node* _root;
};


如果是 set 的话 T 可以是 Key ,如果是 map 的话 T 就是 pair<Key,Value>,所以我们进行糅合直接使用 T 就行了,最后得到既能实现 map 也能实现 set 的红黑树节点定义:



template
struct RBTreeNode
{
//三叉链
RBTreeNode* _left;
RBTreeNode* _right;
RBTreeNode* _parent;

//存储的数据
T _data;

//结点的颜色
int _col; //红/黑

//构造函数
RBTreeNode(const T& data)
	:\_left(nullptr)
	, \_right(nullptr)
	, \_parent(nullptr)
	, \_data(data)
	, \_col(RED)
{}

};


### 仿函数的支持🤔


T 在是 key 时操作都相对简单,但是如果此时 T 是键值对 pair ,如果需要进行大小比较等操作又该怎么办呢?如何获取键值对的值优势一门学问。


很明显此时 pair<Key,Value> 里面不就有一个 Key 吗,我们直接就地取材把他取出来即可,这时候就需要上层容器 map 提供一个仿函数来获取这个 Key ,我们就相当于直接使用仿函数在比较大小


首先仿函数是个啥?



> 
> 仿函数,就是使一个类的使用看上去像一个函数。其实就是在类中实现一个 operator(),这个类就有了类似函数的行为,就是一个仿函数类了。
> 
> 
> 



template<class K, class V>
class map
{
//内部类定义仿函数
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv) //返回键值对当中的键值 Key
{
return kv.first;
}
};
public:
//…
private:
RBTree<K, pair<K, V>, MapKeyOfT> _t; //加上仿函数
};


同时 map 有了 set 也需要搞一个,你可能会说 set 不都是 Key 吗,现成的为什么还需要仿函数呢?其实这个仿函数还是必需的,如果说容器是一瓶好酒,那么仿函数就是开瓶器。



KeyOfT kot; //比较函数
//……
if(kot(cur->_data) < kot(data));//map与set的值进行比较


同理,这里我们比较肯定是用上面函数比较,但是好酒和好酒比较,map 有杯子装了,那 set 总不能手捧着喝吧,所以我们为了统一比较所以 set 也使用仿函数机制:



template
class set
{
//内部类定义仿函数
struct SetKeyOfT
{
const K& operator()(const K& key) //返回键值Key
{
return key;
}
};
public:
//…
private:
RBTree<K, K, SetKeyOfT> _t;
};


![在这里插入图片描述](https://img-blog.csdnimg.cn/2dfc57605e9646c4949be6073bc2b2b9.png#pic_center)  
 这样一来,当底层红黑树需要进行两个结点之间键值的比较时,都会通过传入的仿函数来获取相应结点的键值,然后再进行比较,下面以红黑树的查找函数为例:



iterator Find(const K& key)
{
KeyOfT kot;
Node* cur = _root;
while (cur)
{
if (key < kot(cur->_data))
{
cur = cur->_left; //key值小于该结点在左子树中查找
}
else if (key > kot(cur->_data))
{
cur = cur->_right; //key值大于该结点在右子树中查找
}
else
{
return iterator(cur); //查找成功
}
}
return end();
}


### 迭代器实现🤔


#### 正向迭代器🎉


红黑树的正向迭代器实际上就是对结点指针进行了封装,因此在正向迭代器当中实际上就只有一个成员变量,那就是结点的指针!



template<class T, class Ref, class Ptr>//自义定,引用,指针类型
struct __TreeIterator
{
typedef RBTreeNode Node; //结点的类型
typedef __TreeIterator<T, Ref, Ptr> Self; //正向迭代器的类型

Node\* _node; //正向迭代器所封装结点的指针

};


这样我们就可以构造出一个正向迭代器。



class __TreeIterator(Node* _node)
:_node(node); //根据所给结点指针构造一个正向迭代器
{}


既然构造出了迭代器,那么必要的重载就必须写出来,比如解引用操作符:



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; //判断是否是同一个节点
}


而真正令人头大的,其实是 ++ 和 – 运算符的重载,注意使中序遍历的 ++,–。


我们就以前置 ++ 为例,++ 逻辑就两种:


1. 如果当前结点的右子树不为空,则++操作后应该找到其右子树当中的最左结点。
2. 如果当前结点的右子树为空,则++操作后应该在该结点的祖先结点中,找到孩子在祖先左的祖先



Self operator++()
{
if (_node->_right) //右子树不为空
{
//找右子树中最左结点
Node* left = _node->_right;
while (left->_left)
{
left = left->_left;
}
_node = left;
else //结点的右子树为空
{
//寻找孩子不在父亲右的祖先
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_right)
{
cur = parent;
parent = parent->_parent;//向上查找
}
_node = parent;
}
return *this;
}


同理, – 的逻辑是一样的:


1. 如果当前结点的左子树不为空,则–操作后应该找到其左子树当中的最右结点
2. 如果当前结点的左子树为空,则–操作后应该在该结点的祖先结点中,找到孩子在祖先右的祖先



Self operator–()
{
if (_node->_left) //结点的左子树不为空
{
//找左子树中的最右结点
Node* right = _node->_left;
while (right->_right)
{
right = right->_right;
}
_node = right;
}
else //结点的左子树为空
{
//寻找孩子不在父亲左的祖先
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_left)
{
cur = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}


我们实现迭代器会将迭代器类型进行 typedef 方便调用,当然记得要在 public 域里进行调用 typedef。完事了不要忘了迭代器还有两个成员函数 begin 和 end ;begin 返回中序序列当中第一个结点的正向迭代器,即起始结点,end 返回中序序列当中最后一个结点下一个位置的正向迭代器,这里直接用空指针构造一个正向迭代器(暴力粗糙处理):



template<class K, class T, class KeyOfT>
class RBTree
{
typedef RBTreeNode Node; //结点的类型
public:
typedef __TreeIterator<T, T&, T*> iterator; //正向迭代器

iterator begin()
{
	//寻找最左结点
	Node\* left = _root;
	while (left&&left->_left)
	{
		left = left->_left;
	}
	//返回最左结点的正向迭代器
	return iterator(left);
}
iterator end()
{
	//返回由nullptr构造得到的正向迭代器
	return iterator(nullptr);
}

private:
Node* _root; //红黑树的根结点
};


之所以我说直接用空指针构造一个正向迭代器是暴力粗糙的处理,实际上这样实现的迭代器是有残次品,因为我们对 end() 位置的正向迭代器进行 – 操作后,理应得到最后一个结点的正向迭代器,但我们 end() 是直接返回由 nullptr 构造的正向迭代器,因此上述代码无法完成此操作。


所以我们不妨看看 C++ STL 库的实现逻辑:


![在这里插入图片描述](https://img-blog.csdnimg.cn/f1acbbc2fd3c4d9887e48f5e82415a2b.png#pic_center)


库里面是采用了类似双向链表的处理,给整个红黑树造了一个哨兵位节点,该节点左边指向最小的最左节点,右边指向最大的右节点,同时还有一个非常 bug 的设计就是这里哨兵位节点 header 的红黑树头结点之间的 parent 相互指向,也就是 header->parent = BSTree\_node ,BSTree\_node->parent = header


此时 begin() 的实现就直接用头结点的左孩子构造一个正向迭代器即可,同样的,如果此时需要 ==rbegin()==就用头结点的右孩子构造一个反向迭代器,严谨的过程是先构造正向迭代器,再用正向迭代器构造反向迭代器,end() 和 rend() 此时就不需要什么 nullptr 了,直接有头结点(哨兵位)进行迭代器构造即可,这样就能完成一个逻辑完整的迭代器了。


#### 反向迭代器🎉


上面也说了,反向迭代器的严谨构造过程是用正向迭代器进行封装,我们可以将反向迭代器视为一个迭代器适配器。


反向迭代器中理所当然的就只有一个成员变量就是正向迭代器:



template//迭代器适配器
struct ReverseIterator
{
typedef ReverseIterator Self; //反向迭代器
typedef typename Iterator::reference Ref; //指针的引用
typedef typename Iterator::pointer Ptr; //结点指针

Iterator _it; //反向迭代器封装的正向迭代器

//构造函数
ReverseIterator(Iterator it)
	:\_it(it) //根据所给正向迭代器构造一个反向迭代器
{}

Ref operator\*()
{
	return \*_it; //调用正向迭代器的operator\*返回引用
}

Ptr operator->()
{
	return _it.operator->(); //调用正向迭代器的operator->返回指针
}

Self& operator++() //前置++
{
	--_it; //调用正向迭代器的前置--
	return \*this;
}
//前置--
Self& operator--()
{
	++_it; //调用正向迭代器的前置++
	return \*this;
}

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

};


为了让反向迭代器能通过正向迭代器获取到结点的引用类型和结点的指针类型,我们需要在正向迭代器中添加 typedef ,因为此时反向迭代器模板参数只有正向迭代器,他压根儿不知道什么引用类型和指针类型,所以还得我们自己动动手:



template<class T, class Ref, class Ptr>
struct __TreeIterator
{
typedef Ref reference; //引用
typedef Ptr pointer; //指针
};


完成后也需要再次在红黑树中对 rbegin 和 rend 成员进行 typedef 并实现:rbegin()返回最右节点的反向迭代器,rend 返回头结点的前一个节点的反向迭代器,这里也是用 nullptr 直接构造(不严谨),根据上面正向迭代器的实现,我们还是加入哨兵位节点来优化,不赘述。


### set 的实现🤔


基本操作都倒腾出来了,set 就是 easy case,接口实现直接照搬红黑树的现成,还要将 find 和 insert 返回的指针改成迭代器即可:



template
class set
{
//仿函数
struct SetKeyOfT
{
const K& operator()(const K& key) //返回键值Key
{
return key;
}
};
public:
typedef typename RBTree<K, K, SetKeyOfT>::iterator iterator; //正向迭代器
typedef typename RBTree<K, K, SetKeyOfT>::reverse_iterator reverse_iterator; //反向迭代器

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

reverse_iterator rbegin()
{
	return _t.rbegin();
}
reverse_iterator rend()
{
	return _t.rend();
}

//插入
pair<iterator, bool> insert(const K& key)
{
	return _t.Insert(key);
}
//删除
void erase(const K& key)
{
	_t.Erase(key);
}
//查找
iterator find(const K& key)
{
	return _t.Find(key);
}

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


### map 的实现🤔


map 也和 set 同理,复用红黑树的底层接口实现,此外还需要实现 [] 运算符的重载:



template<class K, class V>
class map
{
//仿函数
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv) //返回键值对当中的键值Key
{
return kv.first;
}
};
public:
typedef typename RBTree<K, pair<K, V>, MapKeyOfT>::iterator iterator; //正向迭代器
typedef typename RBTree<K, pair<K, V>, MapKeyOfT>::reverse_iterator reverse_iterator; //反向迭代器

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

reverse_iterator rbegin()
{
	return _t.rbegin();
}
reverse_iterator rend()
{
	return _t.rend();
}

//插入
pair<iterator, bool> insert(const pair<const K, V>& kv)
{
	return _t.Insert(kv);
}
//[]运算符重载
V& operator[](const K& key)
{
	pair<iterator, bool> ret = insert(make\_pair(key, V()));
	iterator it = ret.first;
	return it->second;
}
//删除
void erase(const K& key)
{
	_t.Erase(key);
}
//查找
iterator find(const K& key)
{
	return _t.Find(key);
}

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


### 完整代码🤔


红黑树



enum Colour
{
RED,
BLACK
};

//红黑树结点的定义
template
struct RBTreeNode
{
//三叉链
RBTreeNode* _left;
RBTreeNode* _right;
RBTreeNode* _parent;

//存储的数据
T _data;

//结点的颜色
int _col; //红/黑

//构造函数
RBTreeNode(const T& data)
	:\_left(nullptr)
	, \_right(nullptr)
	, \_parent(nullptr)
	, \_data(data)
	, \_col(RED)
{}

};
//红黑树的实现
template<class K, class T, class KeyOfT>
class RBTree
{
typedef RBTreeNode Node; //结点的类型
public:
typedef __TreeIterator<T, T&, T*> iterator; //正向迭代器
typedef ReverseIterator reverse_iterator; //反向迭代器

reverse_iterator rbegin()
{
	//寻找最右结点
	Node\* right = _root;
	while (right&&right->_right)
	{
		right = right->_right;
	}
	//返回最右结点的反向迭代器
	return reverse\_iterator(iterator(right));
}
reverse_iterator rend()
{
	//返回由nullptr构造得到的反向迭代器(不严谨)
	return reverse\_iterator(iterator(nullptr));
}

iterator begin()
{
	//寻找最左结点
	Node\* left = _root;
	while (left&&left->_left)
	{
		left = left->_left;
	}
	//返回最左结点的正向迭代器
	return iterator(left);
}
iterator end()

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注大数据)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

Iterator reverse_iterator; //反向迭代器

reverse_iterator rbegin()
{
	//寻找最右结点
	Node\* right = _root;
	while (right&&right->_right)
	{
		right = right->_right;
	}
	//返回最右结点的反向迭代器
	return reverse\_iterator(iterator(right));
}
reverse_iterator rend()
{
	//返回由nullptr构造得到的反向迭代器(不严谨)
	return reverse\_iterator(iterator(nullptr));
}

iterator begin()
{
	//寻找最左结点
	Node\* left = _root;
	while (left&&left->_left)
	{
		left = left->_left;
	}
	//返回最左结点的正向迭代器
	return iterator(left);
}
iterator end()

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注大数据)
[外链图片转存中…(img-EMJ5rIuL-1713423070560)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值