红黑树封装map和set(c++版)

前言

在前面,我们介绍了c++中map和set库的使用,也实现了一颗简单的红黑树。那么现在我们就利用这两部分的知识,实现一个简单的myMap和mySet。

源码阅读

在我们实现之前,我们可以阅读一些标准库的实现,学习标准库的实现思路。这里我们选取的是SGI-STL30版本源代码,map和set的源代码在map/set/stl_map.h/stl_set.h/stl_tree.h等⼏个头⽂件

下面我们就分别把这几个头文件中核心代码拎出来阅读一下

// set
#ifndef __SGI_STL_INTERNAL_TREE_H //条件编译
#include <stl_tree.h> //红黑树
#endif
#include <stl_set.h> //不允许键值冗余
#include <stl_multiset.h> //允许键值冗余
// map
#ifndef __SGI_STL_INTERNAL_TREE_H //条件编译
#include <stl_tree.h> //红黑树
#endif
#include <stl_map.h> //不允许键值冗余
#include <stl_multimap.h> //允许键值冗余

我们发现map和set都是包含其他头文件,调用其他头文件的实现,这里也是再封装一层的体现,方便调用者的使用

// stl_set.h
template <class Key, class Compare = less<Key>, class Alloc = alloc>
class set {
public:// typedefs:typedef Key key_type;typedef Key value_type;
private:typedef rb_tree<key_type, value_type,identity<value_type>, key_compare, Alloc> rep_type;rep_type t; // 表示set的红黑树
};
// stl_map.h
template <class Key, class T, class Compare = less<Key>, class Alloc = alloc>
class map {
public:// typedefs:typedef Key key_type;typedef T mapped_type;typedef pair<const Key, T> value_type;
private:typedef rb_tree<key_type, value_type,select1st<value_type>, key_compare, Alloc> rep_type;rep_type t; //表示map的红黑树
};

Key是键;T是map中值的值;Compare是一个比较的仿函数,通过传入不同的仿函数,实现降序或升序;Alloc是空间配置器,我们不关注

不难发现,map和set共用了同一颗红黑树,这颗红黑树存在的是键值对。对于set来说键和值都是Key,对于map来说键是Key值是pair<const Key, T>的一个容器,作用是保证Key值不被修改

identity<value_type>和select1st<value_type>是两个仿函数,作用是从value_type类型中取出Key值的大小,本质上就是兼容上的处理,这个我们在后面还会展开。同理set传入两个Key类型给tree也是兼容上的处理。

下面我们就可以来看一下tree的实现

我们可以先来看看treeNode节点的实现

struct __rb_tree_node_base //节点的基类,定义了节点颜色,左右子节点和父节点这样的基础信息
{typedef __rb_tree_color_type color_type;typedef __rb_tree_node_base* base_ptr;color_type color;base_ptr parent;base_ptr left;base_ptr right;
};template <class Value>
struct __rb_tree_node : public __rb_tree_node_base//继承基类,添加节点存储的值
{typedef __rb_tree_node<Value>* link_type;Value value_field;
};

下面我们看一下tree的实现

// stl_tree.h
template <class Key, class Value, class KeyOfValue, class Compare, class Alloc
= alloc>
class rb_tree {
protected:typedef void* void_pointer;typedef __rb_tree_node_base* base_ptr;typedef __rb_tree_node<Value> rb_tree_node;typedef rb_tree_node* link_type;typedef Key key_type;typedef Value value_type;
public:// insert⽤的是第⼆个模板参数左形参pair<iterator,bool> insert_unique(const value_type& x); //插入,不允许键值冗余// erase和find⽤第⼀个模板参数做形参size_type erase(const key_type& x); //删除iterator find(const key_type& x); //查找
protected:size_type node_count; //记录树节点的数量link_type header; //根节点
};

我们发现这里的命令还是比较混乱的,我们建议后续的实现中,将键值定义为Key和Value,将红黑树存储的值定义为T,统一命名风格。我们这里也修改为这种命名风格

红黑树的实现

这里红黑树的实现我们复用我们之前实现的代码,这里实现的具体细节我们不在展开,需要进一步了解的可以查阅我们之前的博客

定义颜色

enum Color //颜色枚举
{RED, //红BLACK //黑
};

定义节点

template<class T>
class RBTreeVal
{template<class K, class T,class KFromT>friend class RBTree; //将红黑树提前声明为友元类,方便红黑树使用节点template<class T, class Ref,class Ptr>friend class RBTreeIterator; //将迭代器提前声明为友元类,方便迭代器使用节点
public:RBTreeVal(const T& t) //构造函数:_t(t){}
private://左右子节点和父节点RBTreeVal<T>* _left = nullptr;RBTreeVal<T>* _right = nullptr;RBTreeVal<T>* _parent = nullptr;//存储数据T _t = T();//节点默认为红色Color _col = RED;
};

这里我们在实现迭代器的时候使用了模板,是普通迭代器和const迭代器复用同一个类,等到具体实现的时候我们会再介绍一次

KFromT是从T类型中得到K值的仿函数

定义红黑树

首先我们需要将红黑树的框架搭好

template<class K,class T,class KFromT>
class RBTree
{typedef RBTreeVal<T> Node;
public:typedef RBTreeIterator<T, T&, T*> Iterator;//普通迭代器 typedef RBTreeIterator<T, const T&, const T*> constIterator;//const迭代器
private:	Node* _root=nullptr; //根节点
};

这里我们现将迭代器定义好,在后面再展开实现

加入红黑树旋转的逻辑

    //左单选void rotateL(Node*par){Node* chi = par->_right;Node* chiL = chi->_left;par->_right = chiL;if (chiL != nullptr){chiL->_parent = par;}chi->_parent = par->_parent;chi->_left = par;par->_parent = chi;if (chi->_parent != nullptr && chi->_parent->_left == par){chi->_parent->_left = chi;}else if (chi->_parent != nullptr && chi->_parent->_right == par){chi->_parent->_right = chi;}if (_root == par){_root = chi;}}//右单旋void rotateR(Node* par){Node* chi = par->_left;Node* chiR = chi->_right;par->_left = chiR;if (chiR != nullptr){chiR->_parent = par;}chi->_parent = par->_parent;chi->_right = par;par->_parent = chi;if (chi->_parent != nullptr && chi->_parent->_left == par){chi->_parent->_left = chi;}else if (chi->_parent != nullptr && chi->_parent->_right == par){chi->_parent->_right = chi;}if (_root == par){_root = chi;}}//右左双旋void rotateRL(Node* par){Node* chi = par->_right;rotateR(chi);rotateL(par);}//左右双旋void rotateLR(Node* par){Node* chi = par->_left;rotateL(chi);rotateR(par);}

统一接口的风格,传入旋转的支点节点就可以实现旋转

我想我们可能需要先实现迭代器,因为插入,寻找可能都需要用到迭代器

我们先搭建一个迭代器的框架

template<class T,class Ref,class Ptr>
class RBTreeIterator
{typedef RBTreeVal<T> Node;typedef RBTreeIterator<T,Ref,Ptr> Self;template<class K, class T, class KFromT>friend class RBTree;
public:RBTreeIterator(Node* node=nullptr,Node* root = __nullptr):_node(node),_root(root){}
private:Node* _node = nullptr;//当前节点Node* _root = nullptr;//根节点,
};

这里我们把迭代器就是封装一层Node*,采用中序遍历的方式,遍历结果是有序的

下面我们就可以加入迭代器++的逻辑

这里++的逻辑可能比较复杂,要分两种情况,如果此迭代器还有右子节点,则找右子树中的最左节点;如果此节点没有右节点,则代表这个节点所在的这颗子树是某个祖先节点的左子树,而左子树已经遍历完,这个祖先节点就是++后的下一个节点,或者这是最后一个有效节点此时会返回一个nullptr标记的迭代器

我们按照这个逻辑完成迭代器++

Self operator++(){if (_node->_right != nullptr){Node* cut = _node->_right;while ( cut->_left){cut = cut->_left;}_node = cut;return *this;}Node* par = _node->_parent;Node* chi = _node;while (par != nullptr){if (par->_left == chi){_node = par;return *this;}chi = par;par = par->_parent;}_node = nullptr;return *this;}

--的逻辑和++的逻辑为镜像,注意的是,如果用到nullptr的迭代器则返回最后一个有效的节点,此时需要用到_root,如果是根节点--则返回nullptr节点

Self operator--(){if (_node == nullptr){Node* cut = _root;while (cut && cut->_right){cut = cut->_right;}_node = cut;return *this;}if (_node->_left != nullptr){Node* cut = _node->_left;while (cut->_right){cut = cut->_right;}_node = cut;return *this;}Node* par = _node->_parent;Node* chi = _node;while (par != nullptr){if (par->_right == chi){_node = par;return *this;}chi = par;par = par->_parent;}_node = nullptr;return *this;}

实现迭代器 == 和 != 比较的逻辑,只需要比较地址是否相同即可

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

在加入迭代器取值的操作 * 和 ->

	Ref operator*(){return _node->_t;}Ptr operator->(){return &_node->_t;}

有了这些,我们就可以在红黑树中实现普通迭代器和const迭代器的begin和end了

	Iterator begin(){if (_root == nullptr){return Iterator(nullptr,_root);}Node* cut = _root;while (cut->_left != nullptr){cut = cut->_left;}return Iterator(cut, _root);}constIterator begin() const{if (_root == nullptr){return constIterator(nullptr, _root);}Node* cut = _root;while (cut->_left != nullptr){cut = cut->_left;}return constIterator(cut, _root);}Iterator end(){return Iterator(nullptr, _root);}constIterator end() const{return constIterator(nullptr, _root);}

我们接着实现插入元素的逻辑。

插入节点传入的参数是T类型,仿函数KFromT会从T类型中返回K的值,返回值遵从标准库返回一个pair<Iterator,bool>,迭代器指向插入元素的位置,bool值返回插入是否成功,如果存在则返回失败,插入的逻辑和红黑树插入的逻辑相同,这里就不展开了

//插入元素pair<Iterator,bool> insert(const T& kv){if (_root == nullptr){_root = new Node(kv);_root->_col = BLACK;return { Iterator(_root,_root),true };}Node* chi = _root;Node* par = nullptr;KFromT tToK;while (chi !=nullptr){if (tToK(kv) > tToK(chi->_t)){par = chi;chi = chi->_right;}else if (tToK(kv) < tToK(chi->_t)){par = chi;chi = chi->_left;}else if (tToK(kv) == tToK(chi->_t)){//已经存在return { Iterator(chi, _root), false };}else{assert(false);}}chi = new Node(kv);Node* rem = chi;chi->_parent = par;if (tToK( par->_t) > tToK(kv)){par->_left = chi;}else if (tToK(par->_t) < tToK(kv)){par->_right = chi;}else{assert(false);}while (par !=nullptr && par->_col != BLACK){Node* gra = par->_parent;if (gra == nullptr){break;}Node* unc = nullptr;if (gra->_left == par){unc = gra->_right;}else if (gra->_right == par){unc = gra->_left;}else{assert(false);}//叔叔存在且为红if (unc != nullptr && unc->_col == RED){unc->_col = BLACK;par->_col = BLACK;gra->_col = RED;chi = gra;par = chi->_parent;}else if (unc == nullptr || unc->_col == BLACK){//右单旋if (chi == par->_left && par == gra->_left){rotateR(gra);gra->_col = RED;par->_col = BLACK;}//右左双旋else if(chi == par->_left && par == gra->_right){rotateRL(gra);chi->_col = BLACK;gra->_col = RED;}//左右双旋else if (chi == par->_right && par == gra->_left){rotateLR(gra);chi->_col = BLACK;gra->_col = RED;}//左单旋else if (chi == par->_right && par == gra->_right){rotateL(gra);gra->_col = RED;par->_col = BLACK;}else{assert(false);}break;}else{assert(false);}}_root->_col = BLACK;return { Iterator(rem,_root),true };}

查找的逻辑比较简单,和插入找合适位置的逻辑相似,返回对应的迭代器,不存在则返回最后一个元素迭代器的下一个位置标识即可,这里也不展开实现了

删除的代码我们也不再展开了,我们在红黑树的介绍中介绍过删除的逻辑,还是比较复杂的,感兴趣的读者可以自行找其他资料查看

map封装

这里我们就可以分装map了

template<class K,class V>class Map{private:struct KOfT{const K& operator()(const pair<K, V>& kv){return kv.first;}};RBTree<K, pair<const K, V>, KOfT> map;public:typedef typename RBTree<K, pair<const K, V>, KOfT>::Iterator iterator;typedef typename RBTree<K, pair<const K, V>, KOfT>::constIterator c_iterator;pair<iterator, bool> insert(const pair<K, V>& kv){return map.insert(kv);}iterator begin(){return map.begin();}iterator end(){return map.end();}c_iterator begin() const{return map.begin();}c_iterator end() const{return map.end();}};

我们在实现一个重载[]的方法

V& operator[](const K& key){pair<iterator, bool> ret = map.insert({key,V()});return ret.first->second;}

set封装

template<class K>class Set{private:struct KOfT{const K& operator()(const K& kv){return kv;}};RBTree<K, const K, KOfT> set;public:typedef typename RBTree<K, const K, KOfT>::Iterator iterator;typedef typename RBTree<K, const K, KOfT>::constIterator c_iterator;pair<iterator,bool> insert(const K& kv){return set.insert(kv);}iterator begin(){return set.begin();}iterator end(){return set.end();}c_iterator begin() const{return set.begin();}c_iterator end() const{return set.end;}};

结语

这里实现的代码还是非常简陋的,只是搭好了一个框架,后续还可以进行升级和加入新的逻辑,本文只是抛砖引玉,更复杂的代码和逻辑我们就不再展开了

以上便是今天的全部内容。如果有帮助到你,请给我一个免费的赞。

因为这对我很重要。

编程世界的小比特,希望与大家一起无限进步。

感谢阅读!

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

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

相关文章

ui设计公司分享:浅色 UI 设计

在数字化产品琳琅满目的今天&#xff0c;用户对于界面的要求早已不止于功能的实现&#xff0c;更追求一种舒适、无压的交互体验。而浅色UI设计&#xff0c;凭借其独特的魅力&#xff0c;正逐渐成为众多设计师营造优质体验的首选。 一、浅色UI设计的视觉优势 &#xff08;一&a…

Nacos:使用PgSQL数据源

数据源插件开源仓库地址&#xff1a;nacos-datasource-extend-plugins 一、PostgreSQL数据库安装 1、本文使用Docker进行数据库的安装&#xff0c;使用docker命令拉取的PG14版本的数据库&#xff1a; docker pull postgres:14.6 2、创建PG容器并启动&#xff0c;映射了5432…

Linux——入门基本指令汇总

目录 1. ls指令2. pwd3. whoami指令4. cd指令5. clear指令6. touch指令7. mkdir指令8. rm指令9. man指令10. cp指令11. mv指令12. cat指令13. tac指令14. more指令15. less指令16. head指令17. tail指令18. date指令19. cal指令20. find指令21. which指令22. alias指令23. grep…

C语言之装甲车库车辆动态监控辅助记录系统

&#x1f31f; 嗨&#xff0c;我是LucianaiB&#xff01; &#x1f30d; 总有人间一两风&#xff0c;填我十万八千梦。 &#x1f680; 路漫漫其修远兮&#xff0c;吾将上下而求索。 C语言之装甲车库车辆动态监控辅助记录系统 目录 一、前言 1.1 &#xff08;一&#xff09;…

2024年Vue面试题汇总

流程图如下&#xff1a; vue核心知识——语法篇 1.请问 v-if 和 v-show 有什么区别&#xff1f; 相同点&#xff1a; 两者都是在判断DOM节点是否要显示。 不同点&#xff1a; a.实现方式&#xff1a; v-if是根据后面数据的真假值判断直接从Dom树上删除或重建元素节点。 v-…

centos搭建 Node.js 开发环境

Node.js &#xff0c;通常简称为Node&#xff0c;是一个事件驱动 I/O 服务端 JavaScript 环境&#xff0c;基于 Chrome V8引擎&#xff0c;具备速度快、性能强等特点&#xff0c;可用于搭建各类网络应用&#xff0c;及作为小程序后端服务环境。npm 和 npx 都是和 Node.js 相关的…

DuckDB:精通Insert语句处理数据冲突

本文介绍DuckDB insert语句用法&#xff0c;包括常规的批量插入&#xff0c;尤其是插入数据冲突的处理&#xff0c;最后还提及returning子句的用法&#xff0c;每个用法提供示例说明。 insert插入数据 INSERT INTO向表中插入新行。可以插入由值表达式指定的一行或多行&#xf…

【Linux系统】Ext系列磁盘文件系统二:引入文件系统(续篇)

inode 和 block 的映射 该博文中有详细解释&#xff1a;【Linux系统】inode 和 block 的映射原理 目录与文件名 这里有几个问题&#xff1a; 问题一&#xff1a; 我们访问文件&#xff0c;都是用的文件名&#xff0c;没用过 inode 号啊&#xff1f; 之前总是说可以通过一个…

SpringBoot实现定时任务,使用自带的定时任务以及调度框架quartz的配置使用

SpringBoot实现定时任务&#xff0c;使用自带的定时任务以及调度框架quartz的配置使用 文章目录 SpringBoot实现定时任务&#xff0c;使用自带的定时任务以及调度框架quartz的配置使用一. 使用SpringBoot自带的定时任务&#xff08;适用于小型应用&#xff09;二. 使用调度框架…

flutter 使用google_mlkit_image_labeling做图片识别

在AI横行的如今&#xff0c;相信大家或多或少都做过跟AI接轨的需求了吧&#xff1f;今天我说的是关于图片识别的需求&#xff0c;flutter的专属图片识别插件google_mlkit_image_labeling。 google_mlkit_image_labeling它是Google旗下的Google Cloud Vision API中分支出来的一部…

国产编辑器EverEdit - 合并行

1 合并行 1.1 应用场景 在编写代码或其他场景下&#xff0c;有时需要把多行的内容缩减成一行&#xff0c;或者纯粹减少行数进行合并&#xff0c;比如&#xff1a;下面的字典的定义&#xff0c;每个元素占了一行&#xff0c;有点浪费&#xff0c;现在需要把它们缩减行数。 typ…

3 前端(中):JavaScript

文章目录 前言&#xff1a;JavaScript简介一、ECMAscript&#xff08;JavaScript基本语法&#xff09;1 JavaScript与html结合方式&#xff08;快速入门&#xff09;2 基本知识&#xff08;1&#xff09;JavaScript注释&#xff08;和Java注释一样&#xff09;&#xff08;2&am…

RIME-CNN-LSTM-Attention多变量多步时序预测Matlab实现

SCI一区级 | Matlab实现RIME-CNN-LSTM-Multihead-Attention多变量多步时序预测 目录 SCI一区级 | Matlab实现RIME-CNN-LSTM-Multihead-Attention多变量多步时序预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现RIME-CNN-LSTM-Multihead-Attention霜冰算法…

一文读懂服务器的HBA卡

什么是 HBA 卡 HBA 卡&#xff0c;全称主机总线适配器&#xff08;Host Bus Adapter&#xff09; &#xff0c;是服务器与存储装置间的关键纽带&#xff0c;承担着输入 / 输出&#xff08;I/O&#xff09;处理及物理连接的重任。作为一种电路板或集成电路适配器&#xff0c;HBA…

mfc操作json示例

首先下载cJSON,加入项目; 构建工程,如果出现, fatal error C1010: unexpected end of file while looking for precompiled head 在cJSON.c文件的头部加入#include "stdafx.h"; 看情况,可能是加到.h或者是.cpp文件的头部,它如果有包含头文件, #include &…

综述:大语言模型在机器人导航中的最新进展!

简介 机器人导航是指机器人能够在环境中自主移动和定位的能力。本文系统地回顾了基于大语言模型&#xff08;LLMs&#xff09;的机器人导航研究&#xff0c;将其分为感知、规划、控制、交互和协调等方面。具体来说&#xff0c;机器人导航通常被视为一个几何映射和规划问题&…

owasp SQL 注入-03 (原理)

1: 先看一下注入界面: 点submit 后&#xff0c;可以看到有语法报错&#xff0c;说明已经起作用了: 报如下的错误: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near at line 1 2:…

Vscode:问题解决办法 及 Tips 总结

Visual Studio Code&#xff08;简称VSCode&#xff09;是一个功能强大的开源代码编辑器&#xff0c;广泛用于各种编程语言和开发场景&#xff0c;本博客主要记录在使用 VSCode 进行verilog开发时遇到的问题及解决办法&#xff0c;使用过程中的技巧 文章目录 扩展安装失败调试配…

Linux(NFS服务)

赛题拓扑&#xff1a; 题目&#xff1a; NFS&#xff1a; 共享/webdata/目录。用于存储AppSrv主机的WEB数据。仅允许AppSrv主机访问该共享。 [rootstoragesrv ~]# yum install nfs-utils -y [rootstoragesrv ~]# mkdir /webdata [rootstoragesrv ~]# chmod -R ow /webdata …

c.p.api.config.MyAuthenticationProvider

文章目录 1、URL1、AdminController3、AuthenticationProvider 2025-01-15 14:21:31.017 WARN 1972 --- [nio-8087-exec-8] c.p.api.config.MyAuthenticationProvider : 管理员:13524972741 登录失败:密码错误解释: 时间戳: 2025-01-15 14:21:31.017 - 表示日志记录的时间…