【C++进阶】map与set的封装实践

文章目录

  • map和set
  • map
    • map的框架
    • 迭代器
      • operator++()
      • operator--()
      • operator==()和operator!=()
      • operator*()
      • operator->()
    • insert
    • begin()
    • end()
    • operator[] ()
    • map的所有代码:
  • set的封装
  • 迭代器的封装
  • 总结

map和set

通过观察stl的底层我们可以看见,map和set是通过红黑树实现的。
在这里插入图片描述
通过观察这些typedef就可以看到,map和set的封装基本都是套用的红黑树的迭代器来封装实现的,所以我们的map和set也可以通过完成的红黑树来进行封装。

map

map的框架

在这里插入图片描述
通过看stl的框架,我们可以看到set实际传递了两个key,但是两个key的重命名是不一样的,map也传递了两个key,第一个key是key,第二个key是pair,所以这里我们在搭建框架的时候,我们可以根据stl的底层的框架进行搭建。

namespace lyrics
{template<class K,class V>class map{public:private:RBTree<K, pair<const K, V>> _t;};
}

根据stl的源代码,map的框架可以简化为上面这种。
为什么要传递第一个参数呢?

因为虽然只传递一个参数,对于set来说没什么影响,但是如果我们想用一个红黑树来封装两个容器的话,这其实是有影响的,因为在比较的时候,set确实不会有影响,但是map在比较的时候,比较逻辑是通过k来比较而并非通过pair来比较,还有就是查找的话,查找set也可以通过一个模版参数来办到,但是对于map来说,查找也是通过pair的first来查找的,所以这里我们应该传递两个参数,第一个参数用于查找和比较,第二个参数用于插入。

迭代器

template<class T, class Ref, class Ptr>
struct RBTreeIterator
{typedef RBTreeNode<T> Node;typedef RBTreeIterator<T,Ref,Ptr> Self;//迭代器Node* _node;Node* _root;RBTreeIterator(Node* node, Node* root) :_node(node), _root(root) {}
};

先创建一个迭代器类,由于这里我们取不到原本红黑树中的root所以我们这里初始化的时候多传递一个参数,在这个迭代器类中多了一个参数root就是用来取原本红黑树中的root的。

operator++()

Self& operator++()
{//要找到当前节点的右子树的最左节点,因为遍历到当前节点说明左子树已经走完了if (_node->_right){Node* leftMost = _node->_right;while (leftMost->_left)leftMost = leftMost->_left;_node = leftMost;}//右为空,代表这棵树已经访问完了,所以应该找到父亲,因为当前节点是父亲的左子树//所以左 中 右//下一个访问的节点应该是访问祖先else{Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_right){cur = parent;parent = cur->_parent;}_node = parent;}return *this;
}

在这里插入图片描述
我们可以通过上面的红黑树来观察,假设当前遍历到的节点就是13,由于红黑树的遍历是中序遍历,所以可是知道,当前左子树已经遍历完了,所以所以下一个节点只看能在右子树,++求的是下一个节点,肯定是求右子树最小的节点,所以就是当前节点的右子树的最左节点。
在这里插入图片描述
如果右子树为空,我们来看看这种情况假设我们当前节点是66,右子树是空的节点,再假设,当前节点是父亲的右子树,如果当前节点是父亲的右子树,那么说明当前节点连带父亲的左子树和右子树都已经访问完了,所以应该向上进行遍历,将当前节点移动到父亲的位置,继续向上遍历,当当前节点是父亲的左子树的节点的时候就不需要在遍历了,因为遍历顺序是左中右,所以左的下一个节点就是右就是当前的父亲节点
在这里插入图片描述

operator–()

Self& operator--()
{//_node是空,代表是endif (_node == nullptr){//end--,则取的就是最右节点,也就是整棵树的最大节点Node* rightMost = _root;//找到左子树的最右节点while (rightMost && rightMost->_right)rightMost = rightMost->_right;_node = rightMost;}//--是左子树的最右节点else if (_node->_left){Node* rightMost = _node->_left;//找到左子树的最右节点while (rightMost->_right) rightMost = rightMost->_right;_node = rightMost;}//如果左为空else{Node* cur = _node;Node* parent = cur->_parent;//因为是反过来的,所以是右根左while (parent && parent->_left == cur){cur = parent;parent = cur->_parent;}_node = parent;}return *this;
}

在这里插入图片描述

–的操作其实是和++相似的,但是需要注意的是:

–的顺序是右中左,而不是左中右,如果是右中左的话,我们假设当前节点是13,那么–的话上一个节点就是11,–应该是求的是比当前节点小的最大的一个节点,所以比当前节点小,所以应该是当前节点的左子树,又是最大的,所以应该是当前节点的左子树的最右的节点。
在这里插入图片描述
但是有可能左子树是空树,所以我们我们拿上面的那个红黑树的图来举例,当节点是22的时候,可以看见左子树是空的,所以所以这时候应该去找父节点,如果当前节点是父节点的左节点,那么继续向上找,因为是左节点,由于是右中左,所以左节点访问完之后,连同当前节点的父节点和右子树的节点已经访问完了,所以应该将当前节点遍历到父节点继续向上找,如果当前节点是父节点的右节点的话,那么就可以停下来不用找了,因为遍历的顺序是右中左,当前节点是父节点的右节点,说明下一个节点是父节点,所以不用找了,直接返回父节点就可以了。
在这里插入图片描述

==注意:这里有一个特殊情况,当节点是end()的时候,需要特殊处理,因为对于第一段逻辑来说,我们需要去访问node的left,这里我们访问空指针的成员已经报错了,所以这里要对空指针进行特殊处理:
在这里插入图片描述
这里就体现出来我们在初始化迭代器的时候传递root的重要性了,这里空指针的情况,我们直接取最右节点,也就是整棵树最大的节点。

operator==()和operator!=()

//!=
bool operator!=(const Self& s) const
{return _node != s._node;//迭代器比较就是两个指针相不相等
}
//==
bool operator==(const Self& s) const
{return _node == s._node;
}

operator*()

//返回节点中的data
Ref operator*()
{return _node->_data;//data是T&
}

operator->()

//pair的迭代器可以访问first和second
Ptr operator->()
{return &(_node->_data);
}

insert

我们先看看之前的insert的代码:
在这里插入图片描述
首先可以看见的是我们之前的红黑树是写的比较死的,适用的范围很小,为了,让其更模版话,我们可以将实际需要插入的类型用一个模版类型T来表示。
在这里插入图片描述

修改之后可以看见,这种形式,在传参的时候只需要根据是map或者是set来看传递pair还是传递K。
在这里插入图片描述
但是我们接着来看红黑树的代码,可以发现,我们之前写的比较逻辑都是通过pair的比较逻辑去比较的,这里就涉及到一个问题,我们现在的data换成T了,所以之前比较的逻辑就不符合语法了。
在这里插入图片描述
可以看看之前的部分的比较逻辑,上面的比较逻辑是基于值是pair的时候成立的,但是现在我们需要的是能适用于map和set的两个容器的比较,所以这里我们必须进行修改,最好的办法就是我们需要传递一个类仿函数,我们将他以模版参数的形式传递,在map和set中写一个类仿函数,然后分别传递到红黑树中,这里就多出来了第三个模版参数。

namespace lyrics
{template<class K,class V>class map{public:private:RBTree<K, pair<const K, V>,MapKeyOfT> _t;};
}

这里第三个参数传递的是一个类仿函数,所以这里我们先写一个仿函数,这里我们写的目的就是要得到map中的pair的first。

struct MapKeyOfT
{const K& operator()(const pair < K, V>& kv){return kv.first;}
};

这样我们就可以取到map中pair的第一个了。
接下来比较就方便多了,我们只需要创建一个对象即可,然后调用operator()()即可,接下来我们尝试改写一个insert的逻辑
在这里插入图片描述

将下面的代码的比较逻辑用这种方式进行修改之后的insert就适用于map和set了,但是在我们查看官方文档之后可以看到官方文档中的insert的返回值是pair<iterator,bool>,所以这里我们也应该修改成和官方文档相同的返回值,这样有利于我们后面封装operator[] (),所以改完之后整体就是下面的:

pair<Iterator,bool> Insert(const T& data)
{//根节点是空时,判断一下if (_root == nullptr){_root = new Node(data);_root->_col = BLACK;return make_pair(Iterator(_root,_root), true);}//遍历节点,找到插入位置Node* cur = _root;Node* parent = nullptr;KeyOfT kot;while (cur){//比当前节点大小就去左子树if (kot(cur->_data) > kot(data))//这里调用operator(){parent = cur;cur = cur->_left;}//比当前节点大就去右子树else if (kot(cur->_data) < kot(data)){parent = cur;cur = cur->_right;}//插入失败返回falseelse return make_pair(Iterator(cur, _root), false);}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* grandparent = parent->_parent;if (grandparent->_left == parent){Node* uncle = grandparent->_right;//叔叔存在为红if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandparent->_col = RED;//改变指向cur = grandparent;parent = cur->_parent;}//叔叔不存在或者叔叔存在且为黑else{//单旋加变色if (cur == parent->_left){RotateR(grandparent);parent->_col = BLACK;grandparent->_col = RED;}//双旋else{//双旋RotateL(parent);RotateR(grandparent);//双旋之后cur和parent是交换了,所以cur充当根cur->_col = BLACK;grandparent->_col = RED;}//第一个节点是黑色节点,所以可以直接结束break;}}else{Node* uncle = grandparent->_left;//叔叔存在为红if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandparent->_col = RED;//改变指向,向上更新cur = grandparent;parent = cur->_parent;}//叔叔不存在或者叔叔存在且为黑else{//单旋加变色if (cur == parent->_right){RotateL(grandparent);parent->_col = BLACK;grandparent->_col = RED;}//双旋else{//双旋RotateR(parent);RotateL(grandparent);//双旋之后cur和parent是交换了,所以cur充当根cur->_col = BLACK;grandparent->_col = RED;}//第一个节点是黑色节点,所以可以直接结束break;}}}//暴力处理根节点的颜色_root->_col = BLACK;//父亲的颜色是黑色直接结束return make_pair(Iterator(newnode, _root), true);
}

map的insert:

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

begin()

Iterator Begin()
{Node* leftSub = _root;//最左节点就是第一个while (leftSub && leftSub->_left != nullptr)//找到最左节点leftSub = leftSub->_left;return Iterator(leftSub, _root);//用最左节点构造一个begin
}
ConstIterator Begin() const
{Node* leftSub = _root;//最左节点就是第一个while (leftSub && leftSub->_left != nullptr)//找到最左节点leftSub = leftSub->_left;return ConstIterator(leftSub, _root);//用最左节点构造一个begin
}

begin需要封装两个一个是const的begin一个是非const的迭代器。
begin很简单,begin就是取到最小的,如果当前节点不是nullptr直接访问这棵树的最左节点返回最左节点即可。
对map封装:

iterator begin()
{return _t.Begin();
}
const_iterator begin()const
{return _t.Begin();
}

end()

//用空代表end
ConstIterator End()const 
{return ConstIterator(nullptr, _root);//引用nullptr构造一个迭代器
}
Iterator End()
{return Iterator(nullptr, _root);//引用nullptr构造一个迭代器
}

end()我们可以直接返回nullptr。

对map进行封装:

iterator end()
{return _t.End();
}
const_iterator end()const
{return _t.End();
}

operator[] ()

这个我们查看官方文档可以看见:
在这里插入图片描述
这个operator[] ()是用insert来封装的,所以我们接下来不管三七二十一先取出来当前key对应的值

V& operator[](const K& key)
{pair<iterator, bool> ret = Insert(make_pair(key, V()));//这里第二个参数给缺省值,有可能插入成功,有可能插入失败return ret.first->second;
}

map的所有代码:

namespace lyrics
{template<class K,class V>class map{struct MapKeyOfT{const K& operator()(const pair < K, V>& kv){return kv.first;}};public:typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::Iterator iterator;typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::ConstIterator const_iterator;//这里底层取的是const key//这里取const,上层就不能修改,set也可以用两个const迭代器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);}iterator find(const K& key){return _t.Find();}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;};
}

set的封装

set的封装和map的如出一辙,甚至比map的简单

namespace lyrics
{template<class K>class set{struct SetKeyOfT{const K& operator()(const K& key){return key;}};public://因为类模版没有被实例化,所以没有被实例化//加上typename就表示等实例化之后去里面将其替换掉typedef typename RBTree<K, const K, SetKeyOfT>::Iterator iterator;//第二个K是给给底层的data用的,所以data不能修改,所以只用给第二个模版参数加上const就可以了typedef typename RBTree<K, const K, SetKeyOfT>::ConstIterator const_iterator;iterator begin(){return _t.Begin();//调用RBTree的begin,只是套一层壳子而已}const_iterator begin()const{return _t.Begin();//调用RBTree的begin,只是套一层壳子而已}iterator end(){return _t.End();}const_iterator end()const{return _t.End();}pair<iterator,bool> Insert(const K& key){return _t.Insert(key);}iterator find(const K& key){return _t.Find(key);}private:RBTree<K, const K, SetKeyOfT> _t;};
}

迭代器的封装

template<class T, class Ref, class Ptr>
struct RBTreeIterator
{typedef RBTreeNode<T> Node;typedef RBTreeIterator<T,Ref,Ptr> Self;//迭代器Node* _node;Node* _root;//返回节点中的dataRef operator*(){return _node->_data;//data是T&}//pair的迭代器可以访问first和secondPtr operator->(){return &(_node->_data);}//构造函数,通过节点的指针进行构造RBTreeIterator(Node* node, Node* root) :_node(node), _root(root) {}//!=bool operator!=(const Self& s) const{return _node != s._node;//迭代器比较就是两个指针相不相等}//==bool operator==(const Self& s) const{return _node == s._node;}//前置++Self& operator++(){//要找到当前节点的右子树的最左节点,因为遍历到当前节点说明左子树已经走完了if (_node->_right){Node* leftMost = _node->_right;while (leftMost->_left)leftMost = leftMost->_left;_node = leftMost;}//右为空,代表这棵树已经访问完了,所以应该找到父亲,因为当前节点是父亲的左子树//所以左 中 右//下一个访问的节点应该是访问祖先else{Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_right){cur = parent;parent = cur->_parent;}_node = parent;}return *this;}Self& operator--(){//_node是空,代表是endif (_node == nullptr){//end--,则取的就是最右节点,也就是整棵树的最大节点Node* rightMost = _root;//找到左子树的最右节点while (rightMost && rightMost->_right)rightMost = rightMost->_right;_node = rightMost;}//--是左子树的最右节点else if (_node->_left){Node* rightMost = _node->_left;//找到左子树的最右节点while (rightMost->_right) rightMost = rightMost->_right;_node = rightMost;}//如果左为空else{Node* cur = _node;Node* parent = cur->_parent;//因为是反过来的,所以是右根左while (parent && parent->_left == cur){cur = parent;parent = cur->_parent;}_node = parent;}return *this;}
};

总结

通过对 map 和 set 的封装,我们可以简化代码的使用方式,增强数据操作的安全性和可维护性,同时根据具体需求扩展功能。这不仅提高了代码的可读性和复用性,还在多线程环境下提供了更好的保障。尽管封装需要额外的设计和实现工作,但它带来的长远收益是显而易见的。未来,我们可以进一步探索封装的优化方向,如支持更多类型的容器或集成其他数据结构,以适应更复杂的应用场景。在实际项目中,合理地利用封装技术,将使我们的C++开发工作更加高效和灵活。

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

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

相关文章

密码学基础---椭圆曲线一文打尽

1.ECC简介及密钥生成 当前公认安全有效的三大类公钥密钥体制分别为基于大数因子分解难题(RSA)、离散对数难题(DSA)和椭圆曲线离散对数&#xff08;ECC&#xff09;难题的密码体制。 最初RSA由于其容易理解被广泛运用&#xff0c;但随着计算机性能的提升&#xff0c;要保证RS…

Golang | Leetcode Golang题解之第336题回文对

题目&#xff1a; 题解&#xff1a; // 哈希表实现 class Solution {public List<List<Integer>> palindromePairs(String[] words) {List<List<Integer>> res new ArrayList<>();int n words.length;Map<String, Integer> indices ne…

AIGC:clip-interrogator

文字生成图片是近年来多模态和大模型研究的热门方向&#xff0c;openai提出的CLIP提供了一个方法建立起了图片和文字的联系&#xff0c;但是只能做到给定一张图片选择给定文本语义最相近的那一个&#xff0c;实际项目开发中我们总是需要从一张图片获取描述&#xff0c;clip-int…

高效录制新选择:2024年Windows录屏软件

录屏能帮助我们捕捉屏幕上的精彩瞬间&#xff0c;作为老师可以用来录制课程&#xff0c;作为会议记录员可以用来录制远程会议。那么有什么软件是适合windows录屏的呢&#xff1f;这次我们一起来探讨一下吧。 1.福昕录屏大师 链接&#xff1a;www.foxitsoftware.cn/REC/ 这款软…

什么是链表/双向链表

看csp j选择的时候看到链表题&#xff0c;那就来写一写吧 什么是链表 首先我们知道数组&#xff1a; 链表和数组有点像&#xff0c;他是这样的&#xff1a; 1----->2------->3------->4 链表中每个数据都有一个指针&#xff0c;指着自己的下一项数据是哪一个 比如…

Android高版本抓包总结

方案1 CharlesVirtualXposedJustTrustMe 推荐使用三星手机此方案 VirtualXposed下载链接&#xff1a;https://github.com/android-hacker/VirtualXposed/releases JustTrustMe下载链接&#xff1a;https://github.com/Fuzion24/JustTrustMe/releases/ 下载完成后使用adb命令…

编辑器和工具扩展

https://www.youtube.com/watch?vovpiYkYFlPM ui提示 检查资源的合法性

使用 Ollama 集成 GraphRag.Net:一步步教你如何实现

在当今的技术世界&#xff0c;人工智能 (AI) 正在以惊人的速度发展。对于开发者来说&#xff0c;使用最新的工具和框架来提升工作效率至关重要。而在 AI 领域&#xff0c;GraphRag.Net 作为一个强大的图算法框架&#xff0c;允许我们以高效的方式进行数据处理。同样&#xff0c…

【Java 并发编程】(二) 从对象内存布局开始聊 synchronized

对象的内存布局 首先抛出一个经典面试题: 一个 Object 对象占多大? 这里我用工具打印了出来, 发现是 “16bytes”, 也就是 16B; 为什么? 请继续往下看; 普通对象(除了数组), 由markword, 类型指针, 实例数据(就是对象里的成员), 对齐填充(整个对象大小要能被8B整数, 方便6…

电销机器人引领电销变革

以前电销都是都是通过盲打&#xff0c;现在有了电话机器人的出现&#xff0c;为电销公司带来新的篇章。 我们都知道外呼中心的人员离职率始终居高不下&#xff0c;人员的培训频繁成本很高&#xff0c;外部电话水平参差不齐&#xff0c;服务态度不够稳定等问题&#xff0c;都是难…

基础 - 前端知识体系详解

一、前端三要素 HTML&#xff08;结构&#xff09;&#xff1a; 超文本标记语言&#xff08;Hyper Text Markup Language&#xff09;&#xff0c;决定网页的结构和内容。CSS&#xff08;表现&#xff09;&#xff1a; 层叠样式表&#xff08;Cascading Style Sheets&#xff0…

【Mac】Downie 打开提示试用的解决办法?

前情 我们在使用 Downie 的时候&#xff0c;可能遇到提示试用的问题&#xff0c;如下图所示。 原因 旧版本的 Downie 没有卸载干净导致的。 解决办法 先使用 AppCleaner 卸载掉电脑上的 Downie 旧版本软件&#xff0c;必须使用 AppCleaner 卸载。重新安装 Downie 即可。

如何保证数据不丢失?(死信队列)

死信队列 1、什么是死信 死信通常是消息在特定的场景下表现&#xff1a; 消息被拒绝访问消费者发生异常&#xff0c;超过重试次数消息的Expiration过期时长或者队列TTL过期时间消息队列到达最大容量 maxLength 2、什么是死信队列 只由死信构成的消息队列是死信队列 死信队…

PhpStorm完全配置指南:打造高效PHP开发环境!

Phpstorm环境配置与应用&#xff0c;具体包括安装PhpStorm、配置PHP运行环境、Apache集成、调试和部署等步骤。下面将详细展开每个步骤的具体操作和注意事项。 PhpStorm的下载与安装 下载地址&#xff1a;访问PhpStorm的官网下载地址&#xff0c;选择合适的版本进行下载。建议选…

springboot、spring@JsonAlias(velue)的作用

‌JsonAlias注解用于为字段或参数指定反序列化时的别名。‌‌ 在Java开发中&#xff0c;‌特别是在使用Jackson库进行JSON序列化和反序列化时&#xff0c;‌JsonAlias注解扮演着重要的角色。‌它允许开发者为字段或参数指定一个或多个别名&#xff0c;‌这样在反序列化过程中&…

希尔排序,详细解析(附图解)

1.希尔排序思路 希尔排序是一种基于插入排序的算法&#xff0c;通过将原始数据分成若干个子序列&#xff0c;然后对子序列进行插入排序&#xff0c;逐渐减小子序列的间隔&#xff0c;最后对整个序列进行一次插入排序。 1.分组直接插入排序&#xff0c;目标接近有序--------…

玩机进阶教程-----回读 备份 导出分区来制作线刷包 回读分区的写入与否 修改xml脚本

很多工作室需要将修改好的系统导出来制作线刷包。前面分享过很多制作线刷包类的教程。那么一个机型中有很多分区。那些分区回读后要写入。那些分区不需要写入。强写有可能会导致不开机 不进系统的故障。首先要明白。就算机型全分区导出后在写回去 都不一定可以开机进系统。那么…

萌啦数据插件使用情况分析,萌啦数据插件下载

在当今数字化时代&#xff0c;数据已成为企业决策与个人分析不可或缺的重要资源。随着数据分析工具的日益丰富&#xff0c;一款高效、易用的数据插件成为了众多用户的心头好。其中&#xff0c;“萌啦数据插件”凭借其独特的优势&#xff0c;在众多竞品中脱颖而出&#xff0c;成…

基于SpringBoot的房屋交易平台的设计与实现

TOC springboot235基于SpringBoot的房屋交易平台的设计与实现 第1章 绪论 1.1 研究背景 互联网概念的产生到如今的蓬勃发展&#xff0c;用了短短的几十年时间就风靡全球&#xff0c;使得全球各个行业都进行了互联网的改造升级&#xff0c;标志着互联网浪潮的来临。在这个新…

基于Web的农产品直卖平台的设计与实现

TOC springboot266基于Web的农产品直卖平台的设计与实现 绪论 1.1 选题背景 当人们发现随着生产规模的不断扩大&#xff0c;人为计算方面才是一个巨大的短板&#xff0c;所以发明了各种计算设备&#xff0c;从结绳记事&#xff0c;到算筹&#xff0c;以及算盘&#xff0c;到…