STL -- C++ 红黑树封装 Map 和 Set

基本实现思路如下:

一、红黑树基础

1.1 什么是红黑树

红黑树是一种自平衡的二叉搜索树 ,每个节点增加一个存储位表示颜色(红或黑)。通过对任何一条从根到叶子的路径上各个节点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是近似平衡的。

1.2 红黑树的五条性质

  1. 每个节点不是红色就是黑色

  2. 根节点是黑色

  3. 每个叶子节点(NIL)是黑色(通常用空指针表示)

  4. 如果一个节点是红色,则它的两个子节点都是黑色(不能有连续的红节点

  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点(黑色高度相等

1.3 与 AVL 树的对比

  • AVL 树:严格平衡(左右子树高度差不超过1),查询快,插入/删除旋转较多

  • 红黑树:近似平衡(最长路径不超过最短路径的2倍),插入/删除旋转较少,整体性能更优

  • 应用场景map,set, Linux 内核的 epoll 红黑树

二、红黑树节点与迭代器

2.1 节点结构 RBTreeNode<T>

cpp 复制代码
template<class T>
class RBTreeNode
{
    T _data;
    RBTreeNode<T>* _left;
    RBTreeNode<T>* _right;
    RBTreeNode<T>* _parent;
    Color _col;   // RED 、BLACK
};
  • 三叉链表(左右 + 父节点)便于迭代器的前后移动

  • _data 存储实际数据(mappair<const K,V>setconst K

2.2 迭代器 Iterator<T, Ref, Ptr>

cpp 复制代码
template <class T, class Ref, class Ptr>
class Iterator
{
    Node* _node;
    Node* _root;  // 用于 --end() 
};
  • operator++(前置)

    cpp 复制代码
    Self& operator++()
    {
    	assert(_node != nullptr);
    	if (_node->_right)//右不为空 下一个节点就是 此右子树的 最左节点
    	{
    		Node* leftMost = _node->_right;
    		while (leftMost->_left)
    		{
    			leftMost = leftMost->_left;//找到左子树最小节点
    		}
    		_node = leftMost;
    	}
    	else//右为空 检查上一个父亲节点是祖父节点的左还是右节点--左则_node = parent 右则往上一直走 直到是上一个节点的左节点或者是根节点
    	{
    		Node* cur = _node;
    		Node* parent = cur->_parent;
    		while (parent && parent->_right == cur)
    		{
    			cur = parent;
    			parent = cur->_parent;
    		}
    		_node = parent;
    	}
    
    	return *this;
    }
    • 有右孩子 → 右子树的最左节点

    • 无右孩子 → 向上回溯,直到当前节点是父节点的左孩子

  • operator--(前置)

    cpp 复制代码
    Self& operator--()// 左 中 右--》右 中 左
    {
    	if (_node == nullptr)
    	{
    		Node* cur = _root;
    
    		while (cur && cur->_right)//加判空 防止空树
    		{
    			cur = cur->_right;
    		}
    		_node = cur;
    		return *this;
    	}
    
    	if (_node->_left)//左不为空 下一个节点就是 此左子树的 最右节点
    	{
    		Node* rightMin = _node->_left;
    		while (rightMin->_right)
    		{
    			rightMin = rightMin->_right;//找到右子树最大节点
    		}
    		_node = rightMin;
    	}
    	else//左为空 检查上一个父亲节点是祖父节点的左还是右节点--右则_node = parent 右则往上一直走 直到是上一个节点的左节点或者是根节点
    	{
    		Node* cur = _node;
    		Node* parent = cur->_parent;
    		while (parent && parent->_left == cur)
    		{
    			cur = parent;
    			parent = cur->_parent;
    		}
    		_node = parent;
    	}
    
    	return *this;
    }
    • 有左孩子 → 左子树的最右节点

    • 无左孩子 → 向上回溯,直到当前节点是父节点的右孩子

    • 如果 _node == nullptr(即 end()),回退到整棵树的最大节点

面试常问 :迭代器递增/递减的时间复杂度是多少?

平均 O(1),整体中序遍历整棵树为 O(N)

三、红黑树封装

3.1 模板参数设计

cpp 复制代码
template<class K, class T, class KeyofT>
class RBTree
  • K:键类型

  • T:节点存储的数据类型(mappair<const K,V>setconst K

  • KeyofT:仿函数,用于从 T 中提取 K

KeyofT 的作用 :统一比较逻辑,使红黑树无需关心 T 的具体结构

3.2 插入操作 Insert

  • 返回类型:pair<iterator, bool>

    • .first:插入位置或已存在节点的迭代器

    • .secondtrue 表示插入成功,false 表示键已存在

  • 插入后需要调整颜色和旋转,维持红黑树性质

  • 成功插入后,_size++ 维护节点计数

面试常问 :为什么 Insert 要返回 pair<iterator, bool>

为了支持 operator[] 它需要知道插入是否成功,并能返回新插入或已存在元素的引用

cpp 复制代码
pair<iterator,bool> Insert(const T& key)
{
	if (_root == nullptr)
	{
		_root = new Node(key);
		_root->_col = BLACK;
		++_size;
		return {iterator(_root,_root),true};
	}
	//空树插入情况单独考虑
	Node* cur = _root;
	Node* parent = nullptr;

	while (cur)
	{
		if (kot(cur->_data) < kot(key))
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (kot(cur->_data) > kot(key))
		{
			parent = cur;
			cur = cur->_left;
		}
		else
			return { iterator(cur,_root),false };
	}
	//循环结束 说明已经找到需要插入的位置
	cur = new Node(key);
	cur->_col = RED;
	Node* newnode = cur;
	if (kot(parent->_data) < kot(cur->_data))
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
	cur->_parent = parent;//前面也只是 对于父亲的指针做了更改

	//确定cur的是父亲节点左还是右
	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)//从整体上来看 用特殊情况替代一般情况
				{
					RotateR(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)
			{
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;

				cur = grandfather;
				parent = cur->_parent;

			}
			else
			{
				if (cur == parent->_left)
				{
					RotateR(parent);
					RotateL(grandfather);

					cur->_col = RED;
					grandfather->_col = BLACK;
				}
				else
				{
					RotateL(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
				}
				break;
			}
		}
	}
	++_size;
	_root->_col = BLACK;//最后把_root 的颜色置为黑 肯定没问题
	return { iterator(newnode,_root),true };
}

3.3 查找操作 Find

  • 返回 iteratorconst_iterator

  • 找不到返回 end()(即 iterator(nullptr, _root)

面试常问 :为什么 find 找不到要返回 end() 而不是 nullptr

统一接口,所有 STL 容器都这么设计,用户统一用 if (it != container.end()) 判断

cpp 复制代码
	iterator _Find(Node* root,const K& key)
	{
		Node* cur = root;

		while (cur)
		{
			if (key> kot(cur->_data))
			{
				cur = cur->_right;
			}
			else if (key < kot(cur->_data))
			{
				cur = cur->_left;
			}
			else
			{
				return iterator(cur, _root);//构造一个迭代器 需要传的参数 这里要自己去参考迭代器的构造函数
			}
		}
		return iterator(nullptr,_root);

	}
	const_iterator _Find(Node* root,const K& key) const
	{
		Node* cur = root;

		while (cur)
		{
			if (key> kot(cur->_data))
			{
				cur = cur->_right;
			}
			else if (key < kot(cur->_data))
			{
				cur = cur->_left;
			}
			else
			{
				return const_iterator(cur, _root);//构造一个迭代器 需要传的参数 这里要自己去参考迭代器的构造函数
			}
		}
		return const_iterator(nullptr,_root);

	}

3.4 旋转函数 RotateL / RotateR

  • 左旋和右旋是红黑树调整的核心操作

  • 注意更新父节点指针和子节点指针的双向链接

  • 具体代码 参考AVL树的实现

3.5 资源管理(RAII)

cpp 复制代码
// 强制使用默认构造
RBTree() = default;
// 析构函数
~RBTree() { _Destroy(_root); }

// 拷贝构造 --深拷贝
RBTree(const RBTree& other) : _root(nullptr), _size(other._size)
{
    _root = _Copy(other._root);
}

// 赋值运算符
RBTree& operator=(const RBTree& other)
{
    if (this != &other)
    {
        _Destroy(_root);
        _root = _Copy(other._root);
        _size = other._size;
    }
    return *this;
}

// 节点数量
size_t Size() const { return _size; }
bool Empty() const { return _root == nullptr; }

辅助函数

  • _Destroy(Node* root):递归释放整棵树

  • _Copy(Node* root):递归深拷贝整棵树,注意拷贝时保留颜色和父指针

面试常问 :如果不实现拷贝构造和赋值,会发生什么?

默认浅拷贝会导致多个对象共享同一棵树,修改一个会影响另一个,引发内存泄漏或重复释放(双重释放)

四、容器封装 mapset

4.1 Map 封装

cpp 复制代码
namespace JMP2
{

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

	public:
		typedef typename RBTree<K, pair<const K, V>, MapKeyofT>::iterator iterator;//在类里面 取没有确定的类模板成员 要加 typename
		typedef typename RBTree<K, pair<const K, V>, MapKeyofT>::const_iterator const_iterator;
	public:

		pair<iterator,bool> insert(const pair<K, V>& kv)//具体插入什么根据具体容器决定
		{
			return  _rb.Insert(kv);
		}
		iterator begin() { return _rb.begin(); }
		const_iterator begin() const { return _rb.begin(); }
		iterator end() { return _rb.end(); }
		const_iterator end() const { return _rb.end(); }

		iterator find(const K& kv)
		{
			return _rb.Find(kv);
		}
		const_iterator find(const K& kv) const
		{
			return _rb.Find(kv);
		}

		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = insert(make_pair(key, V()));
			return ret.first->second;
		}
		size_t Size() const { return _rb.Size(); }
		bool Empty() const { return _rb.Empty(); }
	private:
		RBTree < K, pair<const K,V>, MapKeyofT> _rb;
	};
}

4.2 Set 封装

cpp 复制代码
namespace JMP1
{

	template <class K>
	class set
	{
		struct SetKeyofT
		{
			const K& operator()(const K& key) const
			{
				return key;
			}
		};

	public:															//要明白 typename的作用 编译器默认认为 依赖名::xxx 不是类型(而是变量或函数)
		typedef typename RBTree<K, const K, SetKeyofT>::iterator iterator;//编译器猜不出来 标准就规定 如果这确实是一个类型,必须显式地用 typename 关键字告诉编译器
		typedef typename RBTree<K, const K, SetKeyofT>::const_iterator const_iterator;
	public:
		pair<iterator, bool> Insert(const K& key)//具体插入什么根据具体容器决定
		{
			return  _rb.Insert(key);
		}

		iterator begin() { return _rb.begin(); }
		const_iterator begin() const { return _rb.begin(); }
		iterator end() { return _rb.end(); }
		const_iterator end() const { return _rb.end(); }

		iterator find(const K& key)
		{
			return _rb.Find(key);
		}
		const_iterator find(const K& key) const
		{
			return _rb.Find(key);
		}
		size_t Size() const { return _rb.Size(); }
		bool Empty() const { return _rb.Empty(); }

	private:
		RBTree < K, const K, SetKeyofT> _rb;//前面两个是重复的 之所以这么设计是因为 要兼顾map的实现
	};
}

六、踩坑记录

坑1:MapKeyofT 参数类型不匹配

错误operator()(const pair<K,V>& kv),但树节点存储的是 pair<const K,V>

解决 :改为 operator()(const pair<const K,V>& kv)

坑2:kot(key) 错误用法

错误 :在 Find 中写 kot(key) < kot(cur->_data)

解决 :直接 key < kot(cur->_data),因为 key 已经是 K 类型,不需要再提取

坑3:拷贝构造忘记复制 _size

错误 :拷贝构造只复制了树结构,没有复制 _size

解决 :初始化列表加 _size(other._size)

坑4:赋值运算符未检查自赋值

解决 :加 if (this != &other)

七、总结

通过完整的手写红黑树及其封装,深入理解:

  1. 红黑树的核心性质与调整逻辑

  2. 迭代器的设计与实现

  3. 模板与仿函数在容器封装中的应用

  4. RAII资源管理的必要性--内存关系

  5. STL 容器的标准接口设计规范

八、面试高频问题汇总

Q1:红黑树插入有几种情况?

  1. 新节点为根 → 直接染黑

  2. 父节点为黑 → 直接插入,无需调整

  3. 父节点为红(需要调整):

    • 叔叔为红 → 变色(父、叔变黑,祖父变红,继续往上检查)。

    • 叔叔为黑(或不存在) → 旋转 + 变色:

      • LL型 → 右旋祖父

      • RR型 → 左旋祖父

      • LR型 → 左旋父 + 右旋祖父

      • RL型 → 右旋父 + 左旋祖父

Q2:迭代器失效问题?

  • 插入操作:不导致任何已有迭代器失效(节点地址不变)

  • 删除操作:指向被删除节点的迭代器失效

  • 拷贝构造/赋值:与源对象无关,不影响已有迭代器

Q3:mapunordered_map 底层区别?

  • map:红黑树(有序,查询 O(log N))

  • unordered_map:哈希表(无序,查询 O(1) 平均,最坏 O(N))

Q4:为什么 map 的键是 const

防止用户通过迭代器修改键,破坏红黑树的有序性。

Q5:find 返回 end() 而不是 nullptr 的好处?

统一接口,所有 STL 容器都这么设计,用户代码可以写成统一风格 if (it != container.end())

Q6:深拷贝和浅拷贝的区别?

  • 浅拷贝:默认拷贝构造/赋值,只复制指针,多个对象共享同一块内存

  • 深拷贝:复制整个数据结构,每个对象独立管理自己的资源

  • 我们的实现_Copy 递归复制整棵树,保证每个 RBTree 对象独立

我的gitee仓库

https://gitee.com/jiangmingpeng0716/c-learning-process

相关推荐
小园子的小菜1 小时前
Java核心基础:反射与泛型底层原理及实战用法详解
java·开发语言
初学者,亦行者1 小时前
算法设计与分析:动态规划 - TSP问题和0-1背包问题
c++·算法·动态规划
淘源码A1 小时前
多院区集团化云PACS系统源码,原生兼容国产软硬件环境
java·云计算·源码·saas·pacs·医学影像系统
何中应1 小时前
主流 OpenJDK 发行版下载安装
java·oracle·jdk·openjdk
daad7771 小时前
记录vscode连接matlab启动环境搭建
java·vscode·matlab
十五年专注C++开发1 小时前
C++之std::numeric_limits介绍
开发语言·c++·limits
2501_925963381 小时前
Hi3516 + WS73 SDIO Wi-Fi 适配笔记
java·开发语言
CoderYanger1 小时前
视频切割脚本(Python版)
linux·开发语言·windows·后端·python·程序人生·职场和发展
辞旧 lekkk1 小时前
CMake工程指南(一)
linux·运维·服务器·开发语言·qt·学习·萌新