前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >在代码的红与黑间——红黑树实现 map 和 set 的美丽旅程

在代码的红与黑间——红黑树实现 map 和 set 的美丽旅程

作者头像
suye
发布2024-11-19 10:16:58
发布2024-11-19 10:16:58
13200
代码可运行
举报
文章被收录于专栏:17的博客分享17的博客分享
运行总次数:0
代码可运行

前言

红黑树,这种平衡二叉搜索树以其独特的颜色标记和平衡机制,成为实现 map 和 set 等容器的核心。它在保证有序性和高效性之间取得了微妙的平衡,为C++标准库带来了无与伦比的查找效率。本篇博客将带你走进红黑树的世界,从原理到实现,揭开其在 map 和 set 中的应用奥秘。

本篇文章将深入探讨如何使用红黑树实现自定义的 MapSet 数据结构,目的是帮助读者理解标准库中 mapset 容器的底层机制。通过实现这些容器,我们可以在掌握红黑树等底层数据结构的同时,设计和构建出符合特定需求的容器。本文将逐步介绍如何改造红黑树、设计红黑树迭代器,并基于此实现 SetMap 两个容器。

📒 一、改造红黑树

红黑树是一种平衡二叉树,主要用于实现关联数据结构(如 mapset)。它满足二叉搜索树的特性,并通过节点的颜色属性及一系列平衡操作,确保树的平衡性。标准的红黑树需要满足以下规则:

  1. 每个节点是红色或黑色
  2. 根节点是黑色
  3. 所有叶子节点(nullptr)是黑色
  4. 红色节点的子节点必须是黑色,即没有连续的红色节点。
  5. 从任一节点到其叶子节点的所有路径包含相同数目的黑色节点
🧮1.1 红黑树的结点设计

在红黑树的实现中,每个节点包含一个 Color 属性用于指示节点颜色,且需要在插入和删除节点时进行颜色检查和旋转操作以保持树的平衡。具体实现如下:

代码语言:javascript
代码运行次数:0
复制
enum Color {
    RED,
    BLACK
};

template <class T>
class RBTreeNode {
public:
    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) {}
};
🧮1.2 修改后红黑树的插入

红黑树的插入过程比普通二叉搜索树复杂,需要保持平衡。在插入节点后,根据节点颜色和结构调整,具体调整规则如下:

代码语言:javascript
代码运行次数:0
复制
pair<Iterator, bool> insert(const T& data) {
	if (!_root) {
		_root = new Node(data);
		_root->_col = BLACK;
		return make_pair(Iterator(_root), true);
	}

	// 定义当前节点cur
	Node* cur = _root;
	Node* parent = nullptr;

	// 仿函数用来索引key
	KeyOfT kot;
	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); // 不可能出现相等的情况插入
	}

	// 创建新的Node实例cur,并将它插入到适当的位置
	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;

	// 更新结点颜色,控制平衡
	while (parent && parent->_col == RED) {
		Node* grandfather = parent->_parent;
		// 父亲是祖父的左孩子
		if (grandfather && 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 if (grandfather && parent == grandfather->right) {
			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->right) {
					RotateL(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
				}
				else {
					RotateR(parent);
					RotateL(grandfather);
					cur->_col = BLACK;
					grandfather->_col = RED;
				}

				break;
			}
		}
	}
	_root->_col = BLACK;

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

📜 二、红黑树的迭代器

红黑树的迭代器设计与其他容器的迭代器类似,需要实现 operator++operator-- 操作符,以支持顺序访问节点。迭代器在容器的遍历、修改等操作中扮演重要角色。在红黑树的 MapSet 容器中,迭代器不仅用于遍历节点,还用于检查元素是否存在,因此设计合理的迭代器十分关键。

🌞 2.1 迭代器的基本设计

迭代器设计的核心是封装对节点的操作,使用户可以通过迭代器透明地访问节点数据。红黑树的节点以二叉树结构相连,因此迭代器可以通过节点之间的相对关系进行遍历。对于红黑树,我们需要设计如下迭代器类:

代码语言:javascript
代码运行次数:0
复制
template<class T, class Ref, class Ptr>
class __TreeIterator {
public:
    typedef RBTreeNode<T> Node;
    typedef __TreeIterator<T, Ref, Ptr> Self;
    typedef __TreeIterator<T, T&, T*> Iterator;
    Node* node;
	
    // 如果迭代器是const,则为构造函数,否则为拷贝构造函数
    __TreeIterator(const Iterator& it) : node(it._node) {}
    __TreeIterator(Node* node = nullptr) : node(node) {}

    Ref operator*() { return node->data; }
    Ptr operator->() { return &(node->data); }

    bool operator!=(const Self& other) const { return node != other.node; }

    Self& operator++();
    Self& operator--();
};

以上迭代器类提供了基本的访问接口,包括解引用(*)、成员访问(->)以及不等比较操作符(!=),方便用户直接使用迭代器访问数据。此外,++-- 操作符实现了顺序访问和逆序访问功能。

🌙2.2 begin()end() 的实现

为了在 SetMap 中使用迭代器遍历红黑树,我们需要实现 begin()end()begin() 返回红黑树的最左节点,即序列中的第一个节点;end() 则返回空迭代器,表示遍历结束。

以下迭代器需要在 RBTree 类中实现,方便map&set的使用。

代码语言:javascript
代码运行次数:0
复制
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);
}

const_Iterator begin() const {
	Node* cur = _root;
	while (cur && cur->left) {
		cur = cur->left;
	}
	return  const_Iterator(cur);
}

const_Iterator end() const {
	return const_Iterator(nullptr);
}

begin()end() 的实现确保用户可以通过常见的迭代器接口遍历红黑树中的所有节点。

⭐2.3 operator++()operator--() 的实现

operator++operator-- 是迭代器设计的关键,它们分别用于遍历红黑树的后继节点和前驱节点。operator++ 的实现逻辑如下:

  1. 如果当前节点有右子树,则后继节点是右子树的最左节点。
  2. 如果当前节点没有右子树,则向上遍历其父节点,直到找到一个父节点,它是其父节点的左子节点。
代码语言:javascript
代码运行次数:0
复制
Self& operator++() {
    if (node->right) {
        node = node->right;
        while (node->left) {
            node = node->left;
        }
    } else {
        Node* parent = node->parent;
        while (parent && node == parent->right) {
            node = parent;
            parent = parent->parent;
        }
        node = parent;
    }
    return *this;
}

operator-- 的实现与 operator++ 类似,只是方向相反:

  1. 如果当前节点有左子树,则前驱节点是左子树的最右节点。
  2. 如果当前节点没有左子树,则向上遍历其父节点,直到找到一个父节点,它是其父节点的右子节点。
代码语言:javascript
代码运行次数:0
复制
Self& operator--() {
	if (_node->left) {
		Node* cur = _node->left;
		while (cur->right) {
			cur = cur->right;
		}
		_node = cur;
	}
	else {
		Node* cur = _node;
		Node* parent = cur->_parent;
		while (parent && cur == parent->left) {
			cur = parent;
			parent = parent->_parent;
		}
		_node = parent;
	}

	return *this;
}

通过实现 operator++operator--,我们可以通过迭代器在红黑树中自由前后遍历。

📚三、Set 的模拟实现

MySet.h 文件中,Set 使用红黑树 RBTree 作为底层数据结构来实现集合。红黑树的自平衡特性保证了集合元素的有序性,且插入、删除、查找操作的时间复杂度为

O(log⁡n)

Set 的实现不允许重复元素,因此当插入新元素时,红黑树会判断该元素是否已存在,若存在则不插入。

🔖3.1 Set 的基本设计

首先定义了一个 SetKeyOfT 仿函数类,它的作用是从集合中的元素提取出键值。在 Set 中,每个元素即为集合的键,因此 SetKeyOfT 直接返回元素本身。

代码语言:javascript
代码运行次数:0
复制
class SetKeyOfT {
public:
    const K& operator()(const K& key){
        return key;
    }
};

然后,我们通过 RBTree 实现了 Set 容器的主要功能,包括 begin()end()insert() 方法:

代码语言:javascript
代码运行次数:0
复制
template<class K>
class set{
public:
    // typename告诉编译器这是类型,
	// 因为set不能够进行修改,所以我们用const迭代器来初始化普通迭代器,来达到不能修改的目的
    typedef typename RBTree<K, K, SetKeyOfT>::const_Iterator Iterator;
    typedef typename RBTree<K, K, SetKeyOfT>::const_Iterator const_Iterator;

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

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

    pair<Iterator, bool> insert(const K& key) {
        pair<typename RBTree<K, K, SetKeyOfT>::Iterator, bool> ret = _t.insert(key);
        return pair<Iterator, bool>(ret.first, ret.second);
    }

private:
    RBTree<K, K, SetKeyOfT> _t;
};
🔖3.2 Setinsert() 方法

Setinsert() 方法中,调用了 _t.insert(key) 来向红黑树 _t 插入一个元素。RBTreeinsert() 方法会自动处理元素是否重复的问题,因此若尝试插入已存在的元素,红黑树会拒绝插入。插入方法返回一个 pair,包含:

  • Iterator:指向插入位置的迭代器。
  • bool:表示是否成功插入,若插入成功为 true,否则为 false(即元素已存在)。

📝 四、Map 的模拟实现

📀4.1 Map 的基本设计

首先,定义了一个 MapKeyOfT 仿函数类,用于从 Map 的元素(即键值对 pair<K, V>)中提取键 KRBTree 使用该仿函数来确定键值对在树中的位置。

代码语言:javascript
代码运行次数:0
复制
class MapKeyOfT {
public:
    const K& operator()(const pair<K, V>& kv) {
        return kv.first;
    }
};

接下来,我们使用 RBTree 实现 Map 的主要功能,包括 begin()end()insert()operator[] 方法:

代码语言:javascript
代码运行次数:0
复制
template<class K,class V>
class map {
public:
    // map是一个key不能修改,value能够修改的结构
    typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::Iterator Iterator;
    typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::const_Iterator const_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();
    }

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

    // 重载map中的operatorp[]
	// operatorp[] 当原数据中没有时,插入并初始化,有则修改second
    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;
};
📀4.2 Map 的 insert() 方法

Mapinsert() 方法用于将键值对插入到 Map 中。其调用 _t.insert(kv) 来将 kv 插入到红黑树 _t 中。insert() 方法返回一个 pair,包含:

  • Iterator:指向插入位置的迭代器。
  • bool:表示是否成功插入,若插入成功为 true,否则为 false(即键已存在)。
📀4.3 Map 的 operator[] 重载

operator[] 提供了通过键访问值的方式。如果键不存在,operator[] 会将键值对插入 Map,并将值初始化为默认值 V();如果键已存在,则直接返回对应值的引用。

代码语言:javascript
代码运行次数:0
复制
V& operator[](const K& key) {
    pair<Iterator, bool> ret = insert(make_pair(key, V()));
    return ret.first->second;
}

该实现方式确保了 Map 的键值对在访问时自动插入,并支持直接通过键来访问和修改值。

结语

红黑树的美在于平衡,数据在红与黑的交替间流转,让查找、插入、删除都化繁为简。掌握了红黑树,你将不再畏惧复杂的数据结构,而是能够在数据的森林中自如穿行。愿这篇博客为你的算法之路增添一份优雅与从容。

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,17的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是17前进的动力!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-11-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
    • 📒 一、改造红黑树
      • 🧮1.1 红黑树的结点设计
      • 🧮1.2 修改后红黑树的插入
    • 📜 二、红黑树的迭代器
      • 🌞 2.1 迭代器的基本设计
      • 🌙2.2 begin() 与 end() 的实现
      • ⭐2.3 operator++() 与 operator--() 的实现
    • 📚三、Set 的模拟实现
      • 🔖3.1 Set 的基本设计
      • 🔖3.2 Set 的 insert() 方法
    • 📝 四、Map 的模拟实现
      • 📀4.1 Map 的基本设计
      • 📀4.2 Map 的 insert() 方法
      • 📀4.3 Map 的 operator[] 重载
  • 结语
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档