c++ map,set封装

map 是一个 kv 结构, set 是 k结构。
我们前面模拟实现了 红黑树,但是我们实现的红黑树把 kv 结构写死了,怎么样才能用泛型编程的思想来实现map和set呢
我们先简单看一下原码中是怎么实现的

1.原码实现逻辑

我们打开这里的 stl_set.h
在这里插入图片描述
通过我们前面学习红黑树,我们看到这里的 rb_tree 的名字 就是一棵纯正的红黑树
我们打开 tree.h 的头文件去找这个 rb_tree
在这里插入图片描述
在这里插入图片描述

这里用 true 和 false 来表示 red 和 black,和我们的枚举有区别,但是没影响,只要能判断红黑即可。
这里map 和 set 都使用了红黑树的代码
接下来开始我们的重头戏
在这里插入图片描述
我们实现节点是一个 struct 就实现了,但是这里还用到了继承,并且我们的红黑树传入了两个模版参数 K, V ,但是这里的节点却只有一个 Value 的模版参数,我们接着往下看
在这里插入图片描述
我们看到,源代码实现的红黑树传入参数有点多,而且下这个整体的封装过程挺麻烦的。
在这里插入图片描述

首先,他的 __rb_tree_node 只传入了一个 Value 的模版参数。但是 map 是 kv 模型,只传入了一个 Value ,怎么能让 map 满足两个类型呢?
我们打开 stl_set.h ,虽然这里重命名了,但是 key_type 和 value_type 的原本类型都是 Key 类型
在这里插入图片描述
我们把这里联系起来
在这里插入图片描述
那这样我们猜测,map 中应该也有一个 key_type 和 value_type ,且这两个类型不同
在这里插入图片描述
这里我们能看到,key_type 和 value_type 的原本类型不同,且 value_type 还是用 pair<const Key, T> 来实现的
在这里插入图片描述
我们先简单理解一下,这里通过一个模版参数 value_type 来确定是哪种类型,如果传入的只是一个 value_type,就是 k 模型,如果传入的参数是键值对 pair<const Key, T>,那这里就是 kv 模型
这里就通过传入参数,使 map 和 set 可以使用同一段代码,实现了泛型编程的思想

2. 模拟实现

set

namespace xsz1
{template<class K>class set{private:RBTree<K, K> _t;}
}

map

namespace xsz2
{template<class K, class T>class map{private:RBTree<K, pair<K, T>> _t;}
}

这里我们先把 K 设置为可修改的。
在这里插入图片描述
我们这里的写法比源代码简化了很多,因为只是模拟实现,不是完全实现,所以只是实现基本功能。
上面的 map 和 set 写完后,我们还需要修改 红黑树的基本代码

template<class T>
struct RBTreeNode
{RBTreeNode<T>* _left;RBTreeNode<T>* _right;RBTreeNode<T>* _parent;//pair<K, V> _kv;Colour _col;T _data;RBTreeNode(const T& data):_left(nullptr),_right(nullptr),_parent(nullptr),_data(data),_col(RED){}
};
template<class K, class T>
class RBTree
{
public:typedef RBTreeNode<T> Node;//...
};

2.1. insert

但是还有一个问题需要处理,第二参数怎么比较大小?
如果是 set 比较大小很简单,都是 key,直接比较,那么 map 的键值对呢
在这里插入图片描述
我们看到,键值对是可以比较的,但是这个比较大小和我们想要的不一样,这里的比较大小,会先去比较 first,在 first 相同的情况下,会去比较 second,但是我们只需要比较 first,所以这里我们还需要单独处理一下
但是要怎么处理才能让 set 和 map 都适合这种用法呢

while(cur)
{if(cur->_data <data){parent = cur;cur = cur->-right;}else if(cur->_data >data){parent = cur;cur = cur->_left;}else{return false}
}

我们能不能用类型对比的方式?

if(cur->_data.type == K)
{//...
}

如果 data 的类型和传入的第一模版参数类型相同,我们就直接比,如果传入的类型是 pair 类型,我们再对比 data.first
但是这样写肯定不行,其实 c 语言中有一种写法能支持这种思路
typeid.name()

if(typeif(cur->_data).name == K)
{//...
}

typeif.name 可以把传入的内容转化成字符串,可以按照这个思路写下去,但是这样写不太符合我们的泛型编程的思想。
还有一个方法

template<class K, class T>
class map
{
public:struct MapKeyofT{const K& operator()(const K& key){return key.first;}};
private:RBTree<K, pair<K, T>, MapKeyofT> _t;
};
template<class K>
class set
{
public:struct SetKeyofT{const K& operator()(const K& key){return key;}};
private:RBTree<K, K, SetKeyofT> _t;
template<class K, class T, class KeyofT>
class RBTree
{//....KeyofT kot;kot(cur->_data <kot < kot(data));
}

我们在 set 和 map 中实现两个仿函数,这两个仿函数在调用时会返回该对象的 value 。
如果是 set 调用,会返回 key,如果是 map 调用,会返回 键值对中的 first。
我们把这两个类作为模版参数传入 RBTree 中,在 RBTree 中,我们只需要去调用这个 kot 即可,kot 会把set , map 中需要比较的部分返回供我们比较,也就不需要我们自己去考虑怎么处理了。

2.2. 迭代器

2.2.1. 红黑树迭代器

红黑树,树形结构,和之前学的链表的迭代器很像。

template<class T, class KeyofT>
struct __TreeIterator
{typedef RBTreeNode<T> Node;typedef __TreeIterator<T, KeyofT> Self;Node* _node;__TreeIterator(Node* node):_node(node){}T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}
}

我们先把最基础的部分写出来。
上面这些都没什么问题,迭代器支持 ++,那么我们的迭代器怎么写才能支持++呢?
拿这棵树研究研究
在这里插入图片描述

一般情况下,会从 begin() 到 end()
begin() 的位置很好确认,就是 _root 的最左节点
在这里插入图片描述
观察后,我们发现,如果当前节点不存在孩子节点时,会去找节点是父亲左节点的父亲节点节点。
在这里插入图片描述
比如当 it== 5 或者 it == 10 时,应该会去找 节点是父亲节点左孩子节点的那个父亲节点
在这里插入图片描述

比如这里如果 it 是 9 ,先去找 10 ,然后走到10 的右孩子节点,此时要向上走,10这棵树已经走完了,那么就去找 10 的父节点。it 在 10 的右孩子节点时,不满足 节点是父亲左节点的条件,接着往上找,10 是 11 的父亲节点的左节点,所以 _node = 11,开始下一步。
如果节点的右子树不为空,那就去右子树中找最左节点。
这句比较好理解,和 begin() 位置一样,直接找最左节点。
我们在实现 ++ 前,先把 begin() 和 end() 实现了

iterator begin()
{Node* cur = _root;while(cur && cur->_left){cur = cur->_left;}return iterator(cur);
}
iterator end()
{return iterator(nullptr);
}

这里的 begin 和 end 函数都比较好理解
begin 最开始要指向最小的节点,所以最开始就是 cur 找最左节点即可
但是 end 要注意,并不是指向最右的节点,按照我们之前对vector 那些迭代器的理解,end 返回的是最后一个节点后面位置的位置,但是当遍历到最大节点时,树中是没有下一个节点的,所以我们默认直接返回 nullptr(按照上面++ 的逻辑,最后会指向 _root 的父节点,但是 _root 本身就是空节点,所以直接用 nullptr 返回即可)。
下面开始实现 ++ 的操作

Self& operator++()
{if(_node->_right){Node* cur = _node->_right;while(cur && cur->_left){cur = cur->_left;}_node = cur;}else{}return *this;
}

右不为空的情况下,去找右子树的最左节点
右为空的情况下,去找节点是父节左的那个父亲节点

Self& operator++()
{if(_node->_right){Node* cur = _node->_right;while(cur && cur->_left){cur = cur->_left;}_node = cur;}else{Node* cur = _node;Node* parent = cur->_parent;while(parent && cur == parent->_right){cur = parent;parent = cur->_parent;}_node = parent;}return *this;
}

这样,我们的 ++ 逻辑就写完了
接下来是 != 操作

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

这里注意是节点指针进行比较,不是对比节点的值,或者是传入的 s 对象。
减减操作和加加操作相反。
++:
右树存在找右树最左节点
右树不存在,找节点是父亲左的那个父亲节点
–:
可以把情况想的极端一点,++是从最左节点遍历到最右节点,–是从最右节点遍历到最左节点,也就是说这俩操作是个完全对称的。
左树存在找最右节点
左树不存在,找节点是父亲右的那个父亲节点

        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 = cur->_parent;}_node = parent;}return *this;}

在这里插入图片描述

2.2.2. set,map封装迭代器

红黑树的迭代器写完了,接下来要在 set 中也实现迭代器,因为我们要通过 set 的接口去使用红黑树。

template<class K>
class set
{
public://..typedef RBTree<K, K, SetKeyofT>::iterataor iterator;iterator begin(){return _t.begin();}iterator end(){return _t.end()}
private:RBTree<K, K, SetKeyofT> _t;
}

既然迭代器写好了,那么我们来试试吧

void test_set()
{xsz1::set<int> s;s.insert(4);s.insert(1);s.insert(2);s.insert(3);s.insert(2);xsz1::set<int>::iterator it = s.begin();while (it != s.end()){cout << *it << "  ";++it;}cout << endl;
}

当我们运行这段代码后,我们会惊奇的发现
在这里插入图片描述
他崩了
注意这里显示出来的问题,iterator 不是类型,我们明明 typedef 了 红黑树的iterator, 为什么这里说 iterator 不是类型呢?
这里要注意,域作用访问限定符

typedef RBTree<K, K, SetKeyofT>::iterataor iterator;

一般除了这里会用到 这个操作符,我们在声明 其类中的静态成员变量也会用到这个操作符,因为我们的红黑树代码是用模版来实现的 ,所以这段代码在预编译的时候并不存在红黑树的代码(实例化后才会出现,想要实例化也是在运行期间),而预编译时访问到了这段代码,编译器就不知道你是想给 iterator 这个静态成员变量重命名,还是给这个类型命重命名,所以会报错。
因此如果想通过这里,我们必须强制说明一下

typedef typename  RBTree<K, K, SetKeyofT>::iterataor iterator;

现在可以开测我们的代码了
在这里插入图片描述
我们看到,通过迭代器,随机插入的数据正向输出了。
迭代器实现了,范围 for 也就随便用。
然后是 map 的迭代器,与 set 同理

template<class K>
class set
{
public://..typedef RBTree<K, pair<K, T>, MapKeyofT>::iterataor iterator;iterator begin(){return _t.begin();}iterator end(){return _t.end()}
private:RBTree<K, pair<K, T>, MapKeyofT> _t;
}

我们运行之后
在这里插入图片描述
完全没什么问题。
虽然 set,map 大题功能已经实现,但不是还存在一个很严重的问题。
如果我们去修改 迭代器位置的值

(*it) = 100;

在这里插入图片描述
我们看到,修改后的数据并不会自动去调整,而且最小的值被修改成了 100,我们的红黑树结构也遭到了破坏。
所以接下来我们要想办法,如何修改才能使key 值不可修改。

2.2.3. key 值不可修改

想要解决这个问题,我们先去库里看看大佬的思路。
set.h
在这里插入图片描述
set 只含有一个 key,在这里 iterator 被处理为 const_iterator,我们去看看 map 是怎么处理的
在这里插入图片描述
map并的处理方式和 set 完全不一样。
set 只含有一个值,这个key值本身就不能被修改,所以这里的 const_iterator 就能保证其中的 key 值不会被修改。
但是 map 中含有 key 和 value,map 中的 key 值不能被修改,但是 value 值是可以被修改的,所以 map 就不能那么暴力的直接全部修改为 const_iterator。
这里 set 和 map 的实现逻辑就不太一样了。
首先,我们先把insert 的返回值修改一下,库里面的返回值是键值对,这里我们也返回键值对

pair<iterator, bool> insert(const T& data)
{//...
}

红黑树的 insert 修改后,要注意 map.h 和 set.h 中的 insert 的返回值也要修改。

pair<iterator, bool> insert(const T& data)
{return _t.insert(data);
}

因为 set 需要用到const_iterator 所以我们先在红黑树中把 const_iterator 实现了
这里和我们前面实现 list 的const_iterator 的做法一样,我们在迭代器的模版参数中传入 Ref(reference参考值) 和 Ptr(指针), 当我们调用 const_iterator 时,就给 迭代器传入 const T& 和 const T*,如果我们要用 iterator, 我们就传入 T& 和 T*

template<class T, class Ref, class Ptr, class KeyofT>
struct __TreeIterator
{//...
}
template<class K, class T, class KeyofT>
class RBTree
{
public:typedef __TreeIterator<T, T&, T*, KeyofT>  iterator;typedef __TreeIterator<T, const T&, const T*, KeyofT> const_iterator;//...
}

const_iteraor 写好后,我们就开始操作 set

typedef typename RBTree<K, K, SetKeyofT>::const_iterator iterator ;
typedef typename RBTree<K, K, SetKeyofT>::const_iterator iterator;
pair<iterator, bool> insert(const K& key)
{return _t.insert(key);
}
iterator begin()const
{return _t.begin();
}
iterator end()const
{return _t.end()
}

把这里写好之后,我们尝试修改 set 中的值
在这里插入图片描述
这个时候,这里就会报错,我们不拿这里处理之后,运行代码,我们发现
在这里插入图片描述
还是报错了。
这里的问题就比较复杂了,
先看我们 红黑树 和 set 中 insert 中的返回值

pair<iterator, bool> insert(const T& data)
{return make_pair(iterator(cur), true);
}
//set.h
pair<iterator , bool> insert(const K& key)
{return _t.insert(key);
}

这里和 pair 的特性相关
在这里插入图片描述
我们知道,pair 的牛逼之处在于它的拷贝,他不需要我们一定要传入相同类型的值才能拷贝构造,而是只要 传入参数 pair 中的 第一第二参数能构造 被传入 的 pair 中的第一第二参数,就能完成 pair 的拷贝构造

 void func(pair<string, const int> p1){//..}int main(){pair<const char*, int> p;func(p);return 0;}

在这里插入图片描述这里是没有问题的,因为 const char* 能构造 string, int 能构造 const int,所以 p 就能去构造 p1。
现在回到上面的问题,为什么这里会报错。
我们先清楚一件事,set 的 insert 返回值pair 中的类型iterator,对红黑树来说是 const_iterator 类型。
红黑树返回的类型是 pair<iterator, bool> 类型,
这里的 iterator 构造出来的类型 是 __TreeIterator<T, Ref, Ptr, KeyofT> 的类型
但是我们set 接收到这个类型后·,再返回 pair<const_iterator, bool> 类,型这里的 返回类型 是
RBTree<K, K, SetKeyofT>::const_iterator 类型,也就是
__TreeIterator<K, T, const T&, const T*, KeyofT>。
红黑树的 insert 返回的 pair 中 的迭代器没有 const T 的类型,但是 从 set 的 insert 返回的 pair 的迭代器中含有 const T 类型,pair 能做到 模版参数只要参数类型能构造就行,但是 我们模拟实现的迭代器是不支持自我完善的,也就是说 __TreeIterator<T, Ref, Ptr, KeyofT> 不能去构造 __TreeIterator<K, T, const T&, const T*, KeyofT> 类型 。
解决方法
所以我们可以考虑,不要使用它的默认拷贝构造,我们自己去写一个拷贝构造函数
除了这个方法,还有个更简单的方法
既然 pair 不需要传入和目标相同的模版参数,只要类型能构造就行,那么我们管目标是什么迭代器,我们直接把节点给set,set拿到节点爱怎么构造不关我红黑树的事

pair<Node*, bool> insert(const T& datd)
{//...return make_pair(cur, true);//...
}

在这里插入图片描述
此时我们的代码就能正常运行了,set 拿到节点,就要去自己用节点去构造 const_iterator。
这里要注意 const_iterator 和 const iterator 不是一个东西,const iterator只能保证这个迭代器不能修改,也就不能进行++,–等操作, 但是是可以修改 iterator 内的值。而 const_iterator 是我们自己实现的可以进行 ++,–等操作的类,只是这个类中的成员都用 const 进行修饰,成员不能修改。

然后就是 map 了,map 因为迭代器不那样暴力处理,我们想既然 key 不能修改,但是 value 可以修改,那么把 所有返回和 K 相关的地方,都返回 const K 应该就行了吧。

        template<class K, class T>class map{public:struct MapKeyofT{const K& operator()(const pair<K, T>& key){return key.first;}};typedef        typename RBTree<K, pair<const K, T>, MapKeyofT>::iterator iterator;typedef        typename RBTree<const K, const pair<const K, T>, MapKeyofT>::iterator const_iterator;pair<iterator, bool> insert(const pair<const K, T>& key){return _t.insert(key);}iterator begin(){return _t.begin();}iterator end(){return _t.end();}T& operator[](const K& key){pair<iterator, bool> ret = insert(make_pair(key, T()));return (ret.first)->second;}private:RBTree<K, pair<const K, T>, MapKeyofT> _t;};

在这里插入图片描述
大概逻辑写完后,我们看到 map 中的 第一参数不能修改,第二参数可以修改。
在这里插入图片描述

2.2.4. operator[]

既然我们上面把迭代器整体都玩完了,那么再写一个 [] 操作助助兴。
在这里插入图片描述
这里的解析在 map_set 学习的那节,想要了解这个怎么操作的可以去那里了解,这里主要进行实现

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

注意这个操作要完成的事,如果 [] 位置不存在值则是插入,如果存在值则返回其中的 value

void test_map2()
{string arr[] = { "香蕉", "甜瓜", "苹果", "西瓜","香蕉", "甜瓜", "苹果", "西瓜", "香蕉", "甜瓜", "苹果", "西瓜", "香蕉", "甜瓜", "苹果", "西瓜", "香蕉", "甜瓜", "苹果", "西瓜" };xsz2::map<string, int> m;for (auto& e : arr){m[e]++;;}for (auto& e : m){cout << e.first << "  " << e.second << "    ";}cout << endl;
}

在这里插入图片描述
这样我们的 [] 操作节完成了。
最后一个问题,我们在 RBTree 中传入了 模版参数 K,但是我们这里全程没用到过这个类型,我们需要 key 时直接使用了 KeyofT,这个 K 模版参数真的有必要吗?
有,因为我们这里主要实现的是 insert 操作,set,map中含有其他操作,比如 find,如果要实现find 操作

iterator find(const k& key)
{//...
}

可以说我们在简单的模拟实现这里没用上,但是绝对不能说他没有用。

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

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

相关文章

淘宝闲鱼卖复印机,日入2000,2024年全新教程

1、项目概述 今天&#xff0c;我要向大家介绍一个在淘宝闲鱼上进行的复印机买卖项目。随着科技的快速发展&#xff0c;电子产品的更新换代速度加快&#xff0c;许多公司每年都需要更换新的复印机&#xff0c;而旧的复印机通常会被转售到二手市场&#xff0c;其中淘宝闲鱼是最大…

Java---类和对象第一节

目录 1.面向对象初步认识 1.1什么是面向对象 1.2面向对象和面向过程的区别 2.类的定义和使用 2.1简单认识类 2.2类的定义格式 2.3类的实例化 2.4类和对象的说明 3.this关键字 3.1访问本类成员变量 3.2调用构造方法初始化成员变量 3.3this引用的特性 4.对象的构造以…

面向侧扫声纳目标检测的YOLOX-ViT知识精馏

面向侧扫声纳目标检测的YOLOX-ViT知识精馏 摘要IntroductionRelated WorkYOLOv-ViTKnowledge DistillationExperimental Evaluation Knowledge Distillation in YOLOX-ViT for Side-Scan Sonar Object Detection 摘要 在本文中&#xff0c;作者提出了YOLOX-ViT这一新型目标检测…

如何远程操作服务器中的Python编译器并将运行结果返回到Pycharm

文章目录 一、前期准备1. 检查IDE版本是否支持2. 服务器需要开通SSH服务 二、Pycharm本地链接服务器测试1. 配置服务器python解释器 三、使用内网穿透实现异地链接服务器开发1. 服务器安装Cpolar2. 创建远程连接公网地址 四、使用固定TCP地址远程开发 本文主要介绍如何使用Pych…

Spring底层入门(十一)

1、条件装配 在上一篇中&#xff0c;我们介绍了Spring&#xff0c;Spring MVC常见类的自动装配&#xff0c;在源码中可见许多以Conditional...开头的注解&#xff1a; Conditional 注解是Spring 框架提供的一种条件化装配的机制&#xff0c;它可以根据特定的条件来控制 Bean 的…

【Docker】Ubunru下Docker的基本使用方法与常用命令总结

【Docker】docker的基本使用方法 镜像image与容器container的关系基本命令- 查看 Docker 版本- 拉取镜像- 查看系统中的镜像- 删除某个镜像- 列出当前 Docker 主机上的所有容器&#xff0c;包括正在运行的、暂停的、已停止的&#xff0c;以及未运行的容器- 列出当前 Docker 主机…

数据结构与算法===贪心算法

文章目录 定义适用场景柠檬水找零3.代码 小结 定义 还是先看下定义吧&#xff0c;如下&#xff1a; 贪心算法是一种在每一步选择中都采取在当前状态下最好或最优&#xff08;即最有利&#xff09;的选择&#xff0c;从而希望导致结果是全局最好或最优的算法。 适用场景 由于…

centos7.9系统安全加固

1、限制用户登陆 vim /etc/hosts.deny&#xff0c;若禁止192.168.0.158对服务器进行ssh的登陆&#xff0c;添加如下内容 sshd : 192.168.0.158 添加完毕后就生效了&#xff0c;直接用192.168.0.158访问主机&#xff0c;就无法连接了&#xff0c;显示 Connection closing...Soc…

数字生态系统的演进与企业API管理的关键之路

数字生态系统的演进与企业API管理的关键之路 在数字化时代&#xff0c;企业正经历着一场转型的浪潮&#xff0c;而API&#xff08;应用程序编程接口&#xff09;扮演着至关重要的角色。API如同一座桥梁&#xff0c;将组织内部的价值转化为可市场化的产品&#xff0c;从而增强企…

(二)Jetpack Compose 布局模型

前文回顾 &#xff08;一&#xff09;Jetpack Compose 从入门到会写-CSDN博客 首先让我们回顾一下上一篇文章中里提到过几个问题&#xff1a; ComposeView的层级关系&#xff0c;互相嵌套存在的问题&#xff1f; 为什么Compose可以实现只测量一次&#xff1f; ComposeView和…

day05-面向对象内存原理和数组

day05 面向对象内存原理和数组 我们在之前已经学习过创建对象了,那么在底层中他是如何运行的。 1.对象内存图 1.1 Java 内存分配 Java 程序在运行时&#xff0c;需要在内存中分配空间。为了提高运算效率&#xff0c;就对空间进行了不同区域的划分&#xff0c;因为每一片区域…

ctfshow web271--web273

web271 laravel5.7反序列化漏洞 define(LARAVEL_START, microtime(true));/* |-------------------------------------------------------------------------- | Register The Auto Loader |-------------------------------------------------------------------------- | |…