C++【一棵红黑树封装 set 和 map】

news2025/7/11 11:21:26

✨个人主页: 北 海
🎉所属专栏: C++修行之路
🎃操作环境: Visual Studio 2019 版本 16.11.17

成就一亿技术人


文章目录

  • 🌇前言
  • 🏙️正文
    • 1、红黑树的完善
      • 1.1、修改默认成员函数
        • 1.1.1、默认构造
        • 1.1.2、析构 ---> 遍历释放
        • 1.1.3、拷贝构造 ---> 深拷贝
        • 1.1.4、赋值重载
      • 1.2、新增迭代器
        • 1.2.1、整体设计
        • 1.2.2、移动操作
        • 1.2.3、数据访问
        • 1.2.4、逻辑判断
        • 1.2.5、迭代器测试
      • 1.3、反向迭代器的设计
    • 2、封装实现
      • 2.1、解决 k 与 k/v 的参数冲突
      • 2.2、解决不同类型的 key 获取问题
      • 2.3、解决 set 迭代器的非法操作
      • 2.4、调整函数返回值
      • 2.5、map 新增 operator[]
    • 3、性能测试
    • 4、完整源码
  • 🌆总结


🌇前言

红黑树的基本情况我们已经在上一篇文章中学习过了,本文主要研究的是红黑树的实际应用:封装实现 setmap,看看如何通过一棵红黑树满足两个不同的数据结构;在正式封装之前,先要对之前的红黑树进行完善,增加必要功能

图示


🏙️正文

1、红黑树的完善

1.1、修改默认成员函数

红黑树 中的每个节点都可能开辟独立的内存空间,因此在涉及拷贝、赋值等操作时,默认生成的成员函数已经无法满足需求了 --> 会导致两个指针指向同一块空间,然后造成重复析构问题

所以我们需要对其中的 默认成员函数 进行改造,手动添加符合要求的 默认成员函数

回顾

1.1.1、默认构造

写出默认构造函数是为了后面的 拷贝构造 做准备,因为祖师爷规定:只要我们写了构造函数(比如拷贝构造),就需要提供一个不需要传递参数的默认构造函数,否则编译会报错

假设只写了 拷贝构造 函数,编译时会报错:

报错

所以需要提供一个 默认构造函数

//因为写了拷贝构造,所以需要有默认构造
RBTree()
	:_root(nullptr)
{}

注意: 默认构造函数的要求是:不需要传递参数的构造函数,所以全缺省的拷贝构造函数也行,但最好还是额外提供一个无参版本

1.1.2、析构 —> 遍历释放

红黑树 中的节点可能涉及 动态内存申请,而编译器生成的 析构函数 无法满足 红黑树 的需求:释放其中的每个节点,所以我们需要编写 析构函数,释放其中的每个节点,确保不会出现 内存泄漏 问题

释放思路: 借助 后序遍历 -> 左右根 的思想,遍历到每一个不为空的节点,然后释放即可

因为需要 递归释放,所以推荐将释放流程封装为单独的函数,方便进行递归,析构函数 直接调用即可

	//析构
	~RBTree()
	{
		_destroy(_root);
	}
	
protected:
	void _destroy(Node*& root)
	{
		if (root == nullptr)
			return;

		//后序遍历
		_destroy(root->_left);
		_destroy(root->_right);

		//销毁节点
		free(root);
		root = nullptr;
	}

细节: _destroy 中参数使用引用,可以在不同栈帧中置空同一个指针变量

1.1.3、拷贝构造 —> 深拷贝

编译器生成的 拷贝构造浅拷贝,当 红黑树 中的节点涉及动态内存申请时,程序运行必然会崩溃(多个指针指向同一块空间,导致重复析构

比如下图中的场景,就是使用了 编译器生成的拷贝构造函数(浅拷贝)

void RBTreeTest1()
{
	RBTree<int, string> rb1;
	rb1.Insert(make_pair(1, "a"));

	RBTree<int, string> rb2(rb1);	//rb2 拷贝构造 rb1
}

重复释放问题

此时就需要手动实现 拷贝构造(深拷贝)

深拷贝思路:照着被拷贝红黑树,逐个节点申请空间,并进行链接即可

类似于 根据中序、后序重构二叉树的思想

	//拷贝构造
	RBTree(const RBTree<K, V>& tree)
		:_root(nullptr)
	{
		//深拷贝 ---> 遍历构造每个节点
		_root = _copy(tree._root);
	}

protected:
	Node* _copy(const Node* root)
	{
		if (root == nullptr)
			return nullptr;

		//构造新节点
		Node* new_node = new Node(root);

		//递归链接左右子树
		new_node->_left = _copy(root->_left);
		new_node->_right = _copy(root->_right);

		//注意父亲链的链接
		if (new_node->_left != nullptr)
			new_node->_left->_parent = new_node;
		if (new_node->_right != nullptr)
			new_node->_right->_parent = new_node;

		return new_node;
	}

借助 后序遍历 的思想重构好每个节点后,返给父亲进行链接,当整棵树都重构完成后,返回 根节点

注意:

  • 拷贝构造函数中的参数需要使用 引用,避免 无穷递归问题
  • 因为是三叉链结构,需要注意父指针的链接,判断不为空后直接链接即可

1.1.4、赋值重载

编译器生成的 赋值重载 函数也是 浅拷贝,实现 赋值重载 就比较简单了,有以下两种办法:

  1. 像 拷贝构造 一样,递归创建每一个节点
  2. 现代写法:直接与临时变量交换根节点

现代写法很简单,也更安全(只要 拷贝构造 没问题,那么现代写法也没问题),下面就是现代写法:

//赋值重载
RBTree<K, V>& operator=(RBTree<K, V> tmp)
{
	//直接交换根节点即可
	std::swap(_root, tmp._root);
	return *this;
}

tmp临时变量,传递参数时,会自动进行一次 拷贝构造 函数的调用,生成临时对象,并且此时是 深拷贝

临时变量 的资源不利用就浪费了,所以可以直接把它的 根节点 偷过来,间接完成了 红黑树 的赋值,原 红黑树 中的节点在函数运行后、临时变量 销毁时进行逐一释放(自动调用 析构函数

注意: 现代写法中的参数不能使用引用,否则会导致被赋值的红黑树节点丢失

1.2、新增迭代器

红黑树 中也有 迭代器,因为是 链式 结构,所以在进行 迭代器 设计时,需要单独设计一个 迭代器类,就像 list 一样

图示

1.2.1、整体设计

红黑树 的节点再一次封装,构建一个单独的 迭代器

因为此时节点的模板参数有 KV,所以 迭代器类 中也需要这两个参数

至于 迭代器类 设计时的精髓:不同类型的迭代器传递不同的参数 这里就不再展开叙述,简单来说,额外增加 RefPtr 的目的是为了让 普通迭代器const 迭代器 能使用同一个 迭代器类

迭代器类中的多参数默认设计思想详见 《C++ STL学习之【list的模拟实现】》

迭代器类 的大体框架如下:

//迭代器类
template<class K, class V, class Ref, class Ptr>
class __RBTreeIterator
{
	typedef RBTreeNode<K, V> Node;	//节点
	typedef __RBTreeIterator<K, V, Ref, Ptr> Self;	//迭代器
public:
	__RBTreeIterator()
		:_node(nullptr)
	{}

	//将节点构造为迭代器对象
	__RBTreeIterator(Node* root)
		:_node(root)
	{}
private:
	Node* _node;
};

其中的 RefPtr 具体是什么类型,取决于调用方传递了什么

1.2.2、移动操作

迭代器 最重要的操作莫过于 移动红黑树 的迭代器是一个 双向迭代器,只支持 ++-- 操作

树形 结构的容器在进行遍历时,默认按 中序遍历 的顺序进行迭代器移动,因为这样遍历 二叉搜索树 后,结果为 有序

遍历图示

清楚遍历路径后,就可以设计具体操作了

正向移动 operator++()operator++(int)

正向移动思路:

  1. 判断当前节点的右子树是否存在,如果存在,则移动至右子树中的最左节点
  2. 如果不存在,则移动至当前路径中 孩子节点为左孩子的父亲节点
  3. 如果父亲为空,则下一个节点就是空
//前置++
Self operator++()
{
	//左根右
	//思路:如果右子树存在,访问右子树的最左节点;如果右子树不存在,访问父亲
	//注意:避免 _node 为空
	if (_node != nullptr && _node->_right != nullptr)
	{
		//访问右子树的最左节点
		Node* cur = _node->_right;
		while (cur->_left != nullptr)
			cur = cur->_left;

		_node = cur;
	}
	else if(_node != nullptr)
	{
		//访问父亲节点(cur 须位于父亲的左边)
		Node*  cur = _node;
		Node* parent = _node->_parent;
		while (parent && parent->_left != cur)
		{
			cur = parent;
			parent = cur->_parent;
		}

		_node = parent;
	}

	return *this;
}


//后置++
Self operator++(int)
{
	Self tmp = *this;
	++(*this);
	return tmp;
}

为什么右子树不为空时,要访问 右子树的最左节点

  • 因为此时是正向移动,路径为 左根右,如果右边路径存在,就要从它的最左节点开始访问

图示

为什么右子树为空时,要访问当前路径中 孩子节点为左孩子 的父亲节点

  • 因为 孩子节点为右孩子 的父亲节点已经被访问过了

图示

在这两种情况的组合之下,就可以完成 迭代器的正向移动

反向移动 operator--()operator--(int)

反向移动很简单,就是与正向相反即可

反向移动思路:

  1. 判断当前节点的左子树是否存在,如果存在,则移动至左子树中的最右节点
  2. 如果不存在,则移动至当前路径中 孩子节点为右孩子的父亲节点
  3. 如果父亲为空,则下一个节点就是空
//前置--
Self operator--()
{
	//右根左
	//思路:如果左子树存在,访问左子树的最右节点;如果左子树不存在,访问父亲
	//注意:避免 _node 为空
	if (_node != nullptr && _node->_left != nullptr)
	{
		//访问左子树的最右节点
		Node* cur = _node->_left;
		while (cur->_right != nullptr)
			cur = cur->_right;

		_node = cur;
	}
	else if(_node != nullptr)
	{
		//访问父亲节点(cur 必须置于父亲的右边)
		Node* cur = _node;
		Node* parent = _node->_parent;
		while (parent && parent->_right != cur)
		{
			cur = parent;
			parent = cur->_parent;
		}

		_node = parent;
	}

	return *this;
}

//后置--
Self operator--(int)
{
	Self tmp = *this;
	--(*this);
	return tmp;
}

至于为何要这两种不同的情况进行移动,上面的 正向移动 已经解释过了

以上就是 红黑树 中迭代器移动操作的相关实现

注意: 在访问父亲节点前,需要先判断父亲是否为 nullptr,避免野指针

1.2.3、数据访问

数据访问 有两种方式:

  1. 直接解引用获取节点中的 _kv
  2. 获取节点中的 _kv 地址

具体实现如下:

//解引用
Ref operator*()
{
	return _node->_kv;
}

//成员访问
Ptr operator->()
{
	return &(operator*());	//复用
}

普通迭代器 创建对象时,传递的参数如下:

__RBTreeIterator<K, V, std::pair<K, V>&, std::pair<K, V>*>

此时的 RefPtr 就是普通的类型,允许发生 修改 行为

const 迭代器 创建对象时,传递的参数如下:

__RBTreeIterator<K, V, const std::pair<K, V>&, const std::pair<K, V>*>

RefPtrconst 对象,即不允许发生 修改 行为

图示

这样一来,就能只通过一个 迭代器类,满足两个性质不同的 迭代器,这就是 泛型编程 思想的魅力

1.2.4、逻辑判断

在进行 迭代器逻辑判断 时,可以直接两个 红黑树 节点是否为同一个

//判断相等
bool operator==(const Self& it) const
{
	return _node == it._node;
}

bool operator!=(const Self& it) const
{
	return !((*this) == it);	//复用
}

注意: 是迭代器和迭代器比较,所以参数是 Self 即迭代器对象

1.2.5、迭代器测试

有了这些模块后,我们的 红黑树 类中就可以引入 迭代器 的相关操作了

//新增迭代器
typedef __RBTreeIterator<K, V, std::pair<K, V>&, std::pair<K, V>*> iterator;
typedef __RBTreeIterator<K, V, const std::pair<K, V>&, const std::pair<K, V>*> const_iterator;

iterator begin()
{
	//起始位置是最左节点
	Node* cur = _root;
	while (cur && cur->_left != nullptr)
		cur = cur->_left;

	return iterator(cur);
}

iterator end()
{
	return nullptr;
}

const_iterator begin() const
{
	//起始位置是最左节点
	Node* cur = _root;
	while (cur && cur->_left != nullptr)
		cur = cur->_left;

	return const_iterator(cur);
}

const_iterator end() const
{
	return nullptr;
}

先来简单玩玩这个 迭代器

void RBTreeTest2()
{
	vector<pair<int, string>> vp{ make_pair(1,"a"),make_pair(2,"b"),make_pair(3,"c"),make_pair(4,"d"),make_pair(5,"e") };
	RBTree<int, string> rb;
	for (auto& e : vp)
		rb.Insert(e);

	const RBTree<int, string> crb(rb);

	cout << "普通对象: " << endl;
	RBTree<int, string>::iterator it = rb.begin();
	while (it != rb.end())
	{
		//两种访问方式都行,但推荐第二种,更方便
		cout << (*it).first << " | " << it->second << endl;
		++it;
	}

	cout << "const 对象: " << endl;
	RBTree<int, string>::const_iterator cit = crb.begin();
	while (cit != crb.end())
	{
		cout << cit->first << " | " << (*cit).second << endl;
		cit++;	//后置++ 也可以用
	}
}

结果

此时基于迭代器的范围 for 也可以正常使用

图示

注意: const 迭代器是为 const 对象提供的,所以可以选择重载 begin()end(),也可以选择重新编写 cbegin()cend(),二者除了函数名外,其他都是一样的

1.3、反向迭代器的设计

红黑树 的反向迭代器比较难搞,因为 反向迭代器类 中为了追求极致对称,rbegin() 是最后一个节点的下一个节点,即 红黑树中最右节点的下一个节点

由于 三叉链 结构的特殊性,我们实现的 红黑树 中没有指向 最右节点 的节点,既然没有,那就创造一个,这正是 SGISTL红黑树 的实现方法,它的根节点 _root 的父亲不是空,而是搞了一个 header 节点,让根节点指向它,它的左右指针分别指向 最左节点最右节点

图示

新增了这个节点后,之前的逻辑都要发生改变,比如 end() 不再是空,而是 header;涉及最左/最右节点的插入后,都要更新 header 的指向,这种方法在进行迭代器操作时比较友好,其他场景下就比较麻烦了,需要额外维护一个节点

如果按库中的 红黑树定义rbegin() 就是 header 这个节点,因为它指向 最右节点

为了避免破坏前面的操作,我们可以额外新增一个成员:header 指向最右节点,在调用 rbegin() 时对其进行更新并返回即可,属于一个比较 中庸 的解决方案,默认构造、拷贝构造、赋值、析构记得对这个节点进行处理

图示

额外新增一个节点,不会影响其他节点,也不会影响前面的逻辑

private:
	Node* _root = nullptr;
	Node* _header = nullptr;	//指向最右节点

涉及构造、初始化等操作时,需要带上 _header

反向迭代器的设计如下:

#include "reverse_iterator.hpp"

typedef __reverse_iterator<iterator, std::pair<K, V>&, std::pair<K, V>*> reverse_iterator;	//反向迭代器
typedef __reverse_iterator<const_iterator, const std::pair<K, V>&, const std::pair<K, V>*> const_reverse_iterator;

//反向迭代器
reverse_iterator rbegin()
{
	//返回指向最右节点的节点
	Node* cur = _root;
	while (cur && cur->_right != nullptr)
		cur = cur->_right;
	_header->_left = cur;
	return reverse_iterator(_header);
}

reverse_iterator rend()
{
	//返回最后一个节点的上一个节点,即最左节点
	Node* cur = _root;
	while (cur && cur->_left != nullptr)
		cur = cur->_left;
	return reverse_iterator(cur);
}

const_reverse_iterator rbegin() const
{
	//返回指向最右节点的节点
	Node* cur = _root;
	while (cur && cur->_right != nullptr)
		cur = cur->_right;
	_header->_left = cur;
	return const_reverse_iterator(_header);
}

const_reverse_iterator rend() const
{
	//返回最后一个节点的上一个节点,即最左节点
	Node* cur = _root;
	while (cur && cur->_left != nullptr)
		cur = cur->_left;
	return const_reverse_iterator(cur);
}

为什么一定要搞一个 辅助节点指向最右节点?

  • 因为反向迭代器类比较奇怪 rbegin() 表示的是最后一个节点的下一个节点,所以为了与之适配,只能新增一个辅助节点

关于反向迭代器类的实现详见 《C++ STL学习之【反向迭代器】》

其实库中解决方案是最优的,但这种方案会影响到前面的很多代码逻辑,于是我们选择了较为折中的方案

可以简单测试一下反向迭代器:

结果

至此 红黑树 算是完善了,比较麻烦的是 迭代器 的实现,需要对 ++-- 进行分析,借助辅助节点 _header,最后也是成功利用 反向迭代器适配器 适配出了 红黑树的反向迭代器

注意: _header_left 链接 最右节点,因为反向迭代器中的 ++ 相当于 --,下一个节点是左子树的最右节点,就是整个红黑树中的最右节点


2、封装实现

下面可以正式步入本文的主题:用一棵红黑树封装实现 setmap

红黑树的封装实现会涉及部分代码改动
为了进行区分,红黑树的完善代码名为 RBTree - 副本.hpp 存放在 Gitee 仓库中

2.1、解决 k 与 k/v 的参数冲突

在同时封装 setmap 时,面临第一个问题:两者参数不匹配

  • set 只需要 key
  • map 则需要 keyvalue

这就意味着一棵 红黑树 无法满足不同需求,难道真无法满足吗?

答案当然是 可以的

参考库中的解决方案:管你是 k 还是 k/v,我都看作 value_type,获取 key 值时再另想其他方法解决

库中的解决方案
注:re_tree 的参数3是获取 key 的方式(后续介绍),参数4是比较方式,参数5是空间配置器

能否省略 参数1 key_type

  • 对于 set 来说,可以,因为冗余了
  • 但对于 map 来说,不行,因为 map 中的函数参数类型为 key_type,省略后就无法确定参数类型了,比如 FindErase 中都需要 key_type 这个类型

这一波是 setmap 做出了牺牲,迁就了 map

红黑树 改造第一步:接口调整

注:库中的 value_type 太长了,这里改为 T,既能表示 k,也能表示 k/v;原红黑树节点中的 _kv 改成了 _data

红黑树 从之前的 K V 变成了现在的 K T,这样一来,凡是之前涉及 K V 的地方都要改,比如:节点类 和 迭代器

//红黑树的节点类
template<class T>
struct RBTreeNode
{
	RBTreeNode(T data = T())
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _data(data)
		, _col(RED)	//默认新节点为红色,有几率被调整
	{}

	//拷贝构造
	RBTreeNode(const T*& node)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _data(node->_data)
		, _col(node->_col)	//默认新节点为红色,有几率被调整
	{
		//拷贝节点中的信息
	}

	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;
	T _data;

	Color _col;
};

//迭代器类
template<class T, class Ref, class Ptr>
class __RBTreeIterator
{
	typedef RBTreeNode<T> Node;	//节点
	typedef __RBTreeIterator<T, Ref, Ptr> Self;	//迭代器

	//……
};

//红黑树类
template<class K, class T>
class RBTree
{
	typedef RBTreeNode<T> Node;
public:
	//……
	
	//新增迭代器
	typedef __RBTreeIterator<T, T&, T*> iterator;
	typedef __RBTreeIterator<T, const T&, const T*> const_iterator;
	typedef __reverse_iterator<iterator, T&, T*> reverse_iterator;	//反向迭代器
	typedef __reverse_iterator<const_iterator, const T&, const T*> const_reverse_iterator;
	
	//……
};

除此之外仍有许多需要修改的地方,这里就不一一展示

红黑树 的接口经过这样一改之后,setmap 就可以传递各自的参数了

Set.hpp

#pragma once
#include <iostream>
#include "RBTree.hpp"

using Yohifo::RBTree;

namespace Yohifo
{
	template<class K>
	class set
	{
		typedef RBTree<K, K> Tree;
	private:
		Tree _t;	//这是一棵红黑树树
	};
}

Map.hpp

#pragma once
#include <iostream>
#include "RBTree.hpp"

using Yohifo::RBTree;

namespace Yohifo
{
	template<class K, class V>
	class map
	{
		typedef RBTree<K, std::pair<K, V>> Tree;
	private:
		Tree _t;	//这也是一棵红黑树
	};
}

接下来就很简单了,直接使用 红黑树 中的接口即可(此处给 红黑树 新增了一个 Find 函数,代码如下)

bool Find(const K& key) const
{
	if (_root == nullptr)
		return false;

	Node* cur = _root;
	while (cur)
	{
		if (cur->_data.first < key)
			cur = cur->_right;
		else if (cur->_data.second > key)
			cur = cur->_left;
		else
			return true;
	}

	return false;
}

可以看到,Find() 的参数类型为 K

此时面临着一个尴尬的问题:Tkey 时,_data 不是 pair,自然没有 firstsecond,程序也就无法跑起来

Insert() 也是如此,凡是涉及获取 key 的地方都有这个问题,因为此时的 _data 是不确定的,对于这种不确定的类型,一般使用 仿函数 解决

2.2、解决不同类型的 key 获取问题

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

图示

set 中的 key 值就是 key,而 map 中的 key 值是 pair<K, V> 中的 first

所以 红黑树 的接口继续改进,新增 KeyOfT 这个模板参数
注:此时只需要在 红黑树类 中新增

//红黑树类
template<class K, class T, class KeyOfT>
class RBTree
{
	//……	
};

分别针对这两种不同的情况设计仿函数:

Set.hpp

template<class K>
class set
{
	//仿函数:获取 key 值
	struct SetKeyOfT
	{
		const K& operator()(const K& key) const
		{
			return key;
		}
	};

	typedef RBTree<K, K, SetKeyOfT> Tree;
private:
	Tree _t;	//这是一棵红黑树树
};

Map.hpp

template<class K, class V>
class map
{
	//仿函数:获取 key 值
	struct MapKeyOfT
	{
		const K& operator()(const std::pair<K, V>& kv) const
		{
			return kv.first;
		}
	};

	typedef RBTree<K, std::pair<K, V>, MapKeyOfT> Tree;
private:
	Tree _t;	//这也是一棵红黑树
};

这一波依然是 set 为了 map 做出了牺牲~

至于 rb_tree 中参数3,也是一个仿函数,主要是用来规定 pair 中的比较方式的

当我们得到不同的 key 值获取方式后,就可以更改 红黑树 中相应的代码了

比如:查找、插入

bool Find(const K& key) const
{
	KeyOfT kot;	//创建一个对象,用来获取 key 值

	if (_root == nullptr)
		return false;

	Node* cur = _root;
	while (cur)
	{
		//operator()(data) 运算符重载,根据不同的对象,使用不同的获取方式
		if (kot(cur->_data) < key)
			cur = cur->_right;
		else if (kot(cur->_data) > key)
			cur = cur->_left;
		else
			return true;
	}

	return false;
}


bool Insert(const T& data)
{
	KeyOfT kot;

	if (_root == nullptr)
	{
		_root = new Node(data);
		_root->_col = BLACK;	//根节点一定是黑色
		return true;
	}

	//寻找合适位置
	Node* parent = nullptr;
	Node* cur = _root;
	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 false;
		}
	}

	//插入节点
	cur = new Node(data);
	if (kot(parent->_data) < kot(data))
		parent->_right = cur;
	else
		parent->_left = cur;
	cur->_parent = parent;

	//判断是否需要 染色、旋转
	//……
}

现在代码可以跑起来了,先简单填充一下 setmap 中的基本操作

Set.hpp

public:
	bool Find(const K& key)
	{
		return _t.Find(key);
	}

	bool Insert(const K& key)
	{
		return _t.Insert(key);
	}

Map.hpp

public:
	bool Find(const K& key)
	{
		return _t.Find(key);
	}

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

测试自己封装的 setmap

void SetAndMapTest1()
{
	set<int> s;
	map<int, int> m;

	s.Insert(1);
	s.Insert(2);
	s.Insert(3);
	s.Insert(4);
	s.Insert(5);

	m.Insert(make_pair(1, 1));
	m.Insert(make_pair(2, 2));
	m.Insert(make_pair(3, 3));
	m.Insert(make_pair(4, 4));
	m.Insert(make_pair(5, 5));

	for (int i = 3; i < 8; i++)
	{
		cout << "set Find " << i << " -> " << s.Find(i) << endl;
		cout << "map Find " << i << " -> " << m.Find(i) << endl;
		cout << "=====================================" << endl;
	}
}

结果

查找插入 没有问题(其实只要底层结构足够稳定,像这种表面封装是不太容易出问题的)

继续完善

  • set 新增迭代器、判空、求大小、计数
  • map 也是一样,新增 set 中新增的功能

注:关于默认成员函数,编译器生成的足够用了,因为此时的 _t 是一个自定义类型,涉及拷贝、赋值等问题时,会去调用 红黑树 中相应的函数,所以我们不需要实现

还是那句话:底层的数据结构足够强大,封装的时候就不需要操太多心

对于 setmap 都需要的函数,可以在 红黑树 中统一实现,两者分别调用即可

RBTree.hpp

bool Empty() const
{
	return _root == nullptr;
}

size_t Size() const
{
	//将底层容器遍历一遍即可
	size_t cnt = 0;
	for (auto& e : *this)
		cnt++;
	return cnt;
}

size_t Count(const K& key) const
{
	KeyOfT kot;
	//统计 key 的数量
	size_t cnt = 0;
	for (auto& e : *this)
	{
		//此时的 e 就是 key
		if (kot(e) == key)
			cnt++;
	}
	return cnt;
}

Set.hpp

注:typename 的作用是告诉编译器这是一个类型

//迭代器
typedef typename Tree::iterator iterator;
typedef typename Tree::const_iterator const_iterator;
typedef typename Tree::reverse_iterator reverse_iterator;
typedef typename Tree::const_reverse_iterator const_reverse_iterator;

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

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

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

const_reverse_iterator rbegin() const { return _t.rbegin(); }
const_reverse_iterator rend() const { return _t.rend(); }

bool Empty() const
{
	return _t.Empty();
}

size_t Size() const
{
	return _t.Size();
}

size_t Count(const K& key) const
{
	return _t.Count(key);
}

Map.hpp

//迭代器
typedef typename Tree::iterator iterator;
typedef typename Tree::const_iterator const_iterator;
typedef typename Tree::reverse_iterator reverse_iterator;
typedef typename Tree::const_reverse_iterator const_reverse_iterator;

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

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

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

const_reverse_iterator rbegin() const { return _t.rbegin(); }
const_reverse_iterator rend() const { return _t.rend(); }

bool Empty() const
{
	return _t.Empty();
}

size_t Size() const
{
	return _t.Size();
}

size_t Count(const K& key) const
{
	return _t.Count(key);
}

可以简单测试一波

图示

注意: 在 红黑树 中,凡是涉及 key 获取的地方,都要通过 KeyOfT 的方式进行获取,因为 _data 的类型不确定

2.3、解决 set 迭代器的非法操作

此时的代码仍然存在问题:set 中只有 keykey 是不能修改的,但此时 set 中的 key 可以被修改!

void SetAndMapTest3()
{
	vector<int> arr = { 8,6,3,2,1 };
	set<int> s;
	for (auto e : arr)
		s.Insert(e);

	cout << "修改前: ";
	for (auto& e : s)
	{
		cout << e << " ";
		e = 1;
	}
	cout << endl;

	cout << "修改后: ";
	for (auto& e : s)
		cout << e << " ";
	cout << endl;
}

图示

此时居然能将 set 中的 key 进行修改!? 这是非常不合理的

库中给出的解决方案:对于 set 来说,无论是否为 const 迭代器,都使用 红黑树中的 const 迭代器进行适配

也就是说,锁死了 set 中迭代器的修改权限,此时自然无法修改 key

  • 此时迭代器类中的 RefPtr 都是 const 版本

Set.hpp

//迭代器
typedef typename Tree::const_iterator iterator;
typedef typename Tree::const_iterator const_iterator;
typedef typename Tree::const_reverse_iterator reverse_iterator;
typedef typename Tree::const_reverse_iterator const_reverse_iterator;

修改完成,VS 启动,代码,运行

结果

结果:出现了一个编译错误

注意: 先要把修改相关的代码屏蔽,否则会导致这个错误无法出现

出现错误的原因

  • set 中,普通对象调用 begin()end() 时,返回的是 普通迭代器,但此时的 iteratorconst 迭代器,这就涉及一个类型转换问题了,其中的 RefPtr 类型不匹配!

解决方案:在 红黑树迭代器类 中新增一个特殊的构造函数

  • 当类模板实例化为 普通迭代器 时,就是一个普通的 拷贝构造 函数
  • 当类模板实例化为 const 迭代器 时,则是一个特殊的 构造函数 -> 将普通的迭代器对象 -> 构造为 const 迭代器
typedef __RBTreeIterator<T, T&, T*> iterator;	//普通迭代器

//特殊的构造函数
__RBTreeIterator(const iterator& it)
	:_node(it._node)	//构造 或 拷贝构造
{}

如何做到的?

  • 当创建 set(普通对象) 中的普通迭代器时,因为此时是普通对象,所以 红黑树 底层会返回一个 普通迭代器,但对于 set 来说,无论是否为 const 对象,它要返回的都是 const 对象,于是它会把 红黑树返回的普通迭代器 -> 借助特殊的构造函数 -> 构造为 const 迭代器
  • 如果 setconst 对象,那么 红黑树 返回的就是 const 迭代器,都不用进行类型转换了

这种写法对于 map 是否有影响?

  • 没有影响,对于 map 来说,普通对象对应的就是普通迭代器,不存在 普通迭代器 转为 const 迭代器 这种情况

新增这个特殊的构造函数后,能正常编译,将 e = 1 这条赋值语句取消注释,再编译,可以发现出现了预料中的报错信息:不能给常量对象赋值

图示

注意: set 中的普通对象对应的也是 const 迭代器,但底层 红黑树 仍然是普通对象,返回的普通迭代器无法转换为 set 中的 const 迭代器,需要通过特殊构造函数解决;不能单纯的通过 const 修饰迭代器暴力解决问题,因为这样会出现 const const 的问题

2.4、调整函数返回值

setmap 中部分函数的返回值比较特殊,不是单纯的 bool

比如 Find() 返回的是 迭代器,查找成功返回所在位置的迭代器,失败返回最后一个位置的迭代器

Insert 插入时,成功返回 《新节点所在位置迭代器 与 true》 构成的 pair,失败则返回 《冗余节点所在位置的迭代器 与 false》 构成的 pair

红黑树 中的对应函数进行改造

RBTree.hpp

iterator Find(const K& key) const
{
	KeyOfT kot;	//创建一个对象,用来获取 key 值

	if (_root == nullptr)
		return iterator(nullptr);

	Node* cur = _root;
	while (cur)
	{
		//operator()(data) 运算符重载,根据不同的对象,使用不同的获取方式
		if (kot(cur->_data) < key)
			cur = cur->_right;
		else if (kot(cur->_data) > key)
			cur = cur->_left;
		else
			return iterator(cur);
	}

	return iterator(nullptr);
}

std::pair<iterator, bool> Insert(const T& data)
{
	KeyOfT kot;

	if (_root == nullptr)
	{
		_root = new Node(data);
		_root->_col = BLACK;	//根节点一定是黑色
		return std::make_pair(iterator(_root), true);
	}

	//寻找合适位置
	Node* parent = nullptr;
	Node* cur = _root;
	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 std::make_pair(iterator(cur), true);
		}
	}

	//插入节点
	//……
	
	//判断是否需要 染色、旋转
	//……

	return std::make_pair(iterator(new_node), true);
}

setmap 中做出相应调整即可

Set.hpp

iterator Find(const K& key) const
{
	return _t.Find(key);
}

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

Map.hpp

iterator Find(const K& key) const
{
	return _t.Find(key);
}

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

可以通过代码测试一下

结果

清楚了 Insert 的返回值之后,就可以轻而易举的理解下图了

图示

2.5、map 新增 operator[]

mapset 多一个 operator[] ,主要作用是用来 访问 value

并且 operator[] 还是一个多功能函数,兼顾 插入、修改、插入+修改、查找

具体实现如下:

Map.hpp

V& operator[](const K& key)
{
	//首先插入
	auto ret = Insert(std::make_pair(key, V()));

	//插入成功:获取新迭代器
	//插入失败:返回已存在节点迭代器
	auto it = ret.first;	//获取迭代器

	return it->second;	//返回 value
}

可以测试一下:

结果

至此,用一颗 红黑树 完成 setmap 的封装就算完成了


3、性能测试

将自己封装的 set 与库中的 set 进行一波性能对比(Release 模式下)

#include <set>

void SetAndMapTest6()
{
	Yohifo::set<int> mySet;
	std::set<int> stdSet;

	srand((size_t)time(NULL));

	int mySetTime = 0;
	int stdSetTime = 0;

	clock_t begin, end;

	int sum = 0;
	int n = 5000000;
	for (int i = 0; i < n; i++)
	{
		int val = rand() % n + i;

		begin = end = 0;
		begin = clock();
		auto ret1 = mySet.Insert(val);
		end = clock();
		mySetTime += (end - begin);

		begin = end = 0;
		begin = clock();
		auto ret2 = stdSet.insert(val);
		end = clock();
		stdSetTime += (end - begin);

		if (ret1.second && ret2.second)
			sum++;	//成功插入的数据量
	}

	cout << "成功插入 " << sum << " 个数据" << endl;
	cout << "mySet 耗时: " << mySetTime << " ms" << endl;
	cout << "stdSet 耗时: " << stdSetTime << " ms" << endl;
}

结果
成功插入 300w+ 数据,结果与库中的 set 性能差不多,证明我们这棵红黑树还是很强的


4、完整源码

关于本次完善的红黑树、封装实现 setmap 的相关代码在下面这个 Gitee 仓库中

《 封装set和map博客 》

图示


🌆总结

以上就是本次关于 C++【一棵红黑树封装 set 和 map】的全部内容了,在本文中,我们首先将 红黑树 进行了完善,解决了一些深拷贝问题,新增了迭代器,同时还用反向迭代器适配器适配出了 反向迭代器,当红黑树完善后,我们用同一棵红黑树同时封装实现了 setmap,其中涉及大量 泛型编程思想,值得仔细推敲


星辰大海

相关文章推荐

C++ 进阶知识

C++【红黑树】

C++【AVL树】

C++【set 和 map 学习及使用】

C++【二叉搜索树】

C++【多态】

C++【继承】

STL 之 泛型思想

C++【模板进阶】

C++【模板初阶】

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/679103.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

nodejs 跳蚤市场网站-计算机毕设 附源码83381

nodejs 跳蚤市场网站 目 录 摘要 1 绪论 1.1选题背景与意义 1.2国内外发展情况 1.3node.js主要功能 1.4node.js功能模块 1.5论文结构与章节安排 2跳蚤市场网站系统分析 2.1 可行性分析 2.2 系统流程分析 2.2.1数据增加流程 2.3.2数据修改流程 2.3.3数据删除流程 …

django公交线路查询系统-计算机毕设 附源码85021

django公交线路查询系统 摘 要 本论文主要论述了如何使用django框架开发一个公交线路查询系统&#xff0c;本系统将严格按照软件开发流程进行各个阶段的工作&#xff0c;面向对象编程思想进行项目开发。在引言中&#xff0c;作者将论述该系统的当前背景以及系统开发的目的&…

售后商品破损回复话术

想必客服们最关注且最头疼的问题便是售后问题了吧&#xff0c;特别是一些商品破损类的问题&#xff0c;是客服们最不想面对的问题之一了&#xff0c;为此小编整理了一份商品破损回复话术提供给大家&#xff0c;希望能对大家有所帮助哦~ 一、询问与核实 1.对不起&#xff0c;由…

Linux系统编程:进程的状态和进程优先级

目录 一. Linux操作系统进程的状态 二. 进程状态的标识 2.1 运行状态R 2.2 睡眠状态S 2.3 前台进程和后台进程 2.4 磁盘睡眠状态D 2.5 暂停状态和调试状态T 2.6 僵尸状态Z 2.7 终止状态X 2.8 孤儿进程 三. 进程优先级 3.1 什么是优先级 3.2 进程优先级的修改和进…

一文带你了解Java的I O机制

Java I/O 一&#xff1a;故事背景二&#xff1a;什么是Java的I\O机制2.1 流2.1.2 字节流和字符流的区别2.1.3 输入流和输出流的区别 2.2 文件 I/O2.3 缓冲 I/O2.4 标准 输入/输出2.5 对象序列化与反序列化2.6 N I/O 三&#xff1a;画图表示Java I/O机制3.1 输入输入系统流程示意…

vue中响应式的数组方法

vue响应式的数组方法 1、push 在数组中追加元素 <div id"app"><ul><li v-for"item in arr">{{item}}</li></ul><button click "change">按钮</button> </div> <script src"js/vue.js&…

普通单目相机标定——准备工作

前言 这里我们还是以普通相机为例(非鱼眼相机)来进行后续的相关标定操作,再回顾下相机的成像模型如下所示。 已知相机内参(fx,fy,u0,v0),畸变系数[k1,k2,k3,p1,p2],相机外参[R|T]。世界坐标系中点Pw(Xw,Yw,Zw),投影至像素坐标系点p(u,v)的计算过程如下。 1)由世…

Linux基础_1

目录 一、用户登录 1、root用户 2、普通&#xff08;非特权&#xff09;用户 二、终端terminal 1、终端类型 2、查看当前的终端设备 三、交互式接口 1、概念&#xff1a;启动终端后&#xff0c;在终端设备附加一个交互式应用程序 2、类型 3、什么是Shell 4、各种She…

Python源码:用turtle画美国队长盾牌

解析&#xff1a; 美国队长盾牌结构分析&#xff1a; 有4个圆和一个五角星组成。 源码如下&#xff1a; import turtle as t #控制画笔的速度 t.speed(10) #画最外面的大圆 t.penup() t.goto(100,-235) t.pendown() t.color("red","red") t.begin_fill(…

iOS 实现多scheme 切换环境完整流程

文章目录 一、 添加configuration二、 添加scheme三、 设置scheme 和 configuration的对应关系四 、 本地配置文件五、 运行 前言 我们在开发中都要涉及到切换环境&#xff0c;很多小伙伴是通过宏定义的方式实现 切换环境的 &#xff0c; 这种方式不优雅&#xff0c;还可造成每…

MySQL基础篇(day02,复习自用)

MySQL第二天 基本的SELECT语句大小规范注释命名规则数据导入指令select语句练习 运算符代码练习 基本的SELECT语句 SQL 可以写在一行或者多行。为了提高可读性&#xff0c;各子句分行写&#xff0c;必要时使用缩进 每条命令以 ; 或 \g 或 \G 结束 关键字不能被缩写也不能分行 关…

中国开源,迈入“新生代”

提到中国开源&#xff0c;大家会想到什么&#xff1f; “追随者&#xff0c;而不是创造者”“国外一开源&#xff0c;国內就自主”“全球开源社区里的nobody”……如果你带着这些“刻板印象”&#xff0c;参加近两年国內的开源活动&#xff0c;极大可能会受到不小的震撼。 中国…

cglib动态代理 | 如何生成代理类、代理类内容解析

文章目录 简介一、cglib动态代理有什么特点CgLib动态代理&#xff1a;优点&#xff1a;缺点&#xff1a; 二、Cglib如何生成代理类生成代理类的具体代码&#xff1a;生成代理对象的具体代码&#xff1a; 三、代理类内容解析持久化代理类&#xff1a;代理类内容&#xff1a; Cgl…

数据指标体系建设

一、什么是数据指标体系&#xff1f; 指标体系是从不同维度梳理业务&#xff0c;把指标有系统的组织起来。简而言之&#xff0c;指标体系指标体系&#xff0c;所以一个指标不能叫指标体系&#xff0c;几个毫无关系的指标也不叫指标体系。指标体系就像是一个整体、一辆车&#…

MacOS Sonoma 14.0 (23A5276g) Beta2 带 OC 引导双分区黑苹果镜像

苹果今日向 Mac 电脑用户推送了 macOS 14 开发者预览版 Beta2 更新&#xff08;内部版本号&#xff1a;23A5276g&#xff09;&#xff0c;本次更新距离上次发布隔了 15 天。 镜像下载&#xff1a; 微信公众号&#xff1a; macOS Sonoma 14.0 (23A5276g) Beta2 带 OC 引导双分…

【人工智能】— 深度神经网络、卷积神经网络(CNN)、多卷积核、全连接、池化

【人工智能】— 深度神经网络、卷积神经网络&#xff08;CNN&#xff09;、多卷积核、全连接、池化 深度神经网络训练训练深度神经网络参数共享 卷积神经网络&#xff08;CNN&#xff09;卷积多卷积核卷积全连接最大池化卷积池化拉平向量激活函数优化小结 深度神经网络训练 Pr…

罗大佑、把午睡打造成领域驱动设计创新-UMLChina建模知识竞赛第4赛季第6轮

DDD领域驱动设计批评文集 欢迎加入“软件方法建模师”群 《软件方法》各章合集 参考潘加宇在《软件方法》和UMLChina公众号文章中发表的内容作答。在本文下留言回答。 只要最先答对前3题&#xff0c;即可获得本轮优胜。第4题为附加题&#xff0c;对错不影响优胜者的判定&am…

最优化系列 之 线性规划问题的灵敏度分析(1/6)

一、实验目的&#xff1a; 1. 掌握用MATLAB、LINGO 、EXCEL优化工具箱解线性规划的方法&#xff1b; 2. 练习建立实际问题的线性规划模型&#xff1b; 3. 掌握线性规划灵敏度分析原理&#xff1b; 4. 预习线性规划的灵敏度分析原理及方法。 二、实验内容 题目1&#xff1a;求解…

【spring cloud学习】2、Eureka服务注册与发现

前言 一套微服务架构的系统由很多单一职责的服务单元组成&#xff0c;而每个服务单元又有众多运行实例。由于各服务单元颗粒度较小、数量众多&#xff0c;相互之间呈现网状依赖关系&#xff0c;因此需要服务注册中心来统一管理微服务实例&#xff0c;维护各服务实例的健康状态…

终端基础知识

CLI、Terminal、Shell、Prompt 一、CLI CLI is the abbreviation of Command Line Interface. It’s a text-based way of interacting with a computer. 相比于图像界面提供的按钮&#xff0c;你可以使用一行命令来实现你想要的功能&#xff0c;比如说打开、关闭文件&…