C++ map_set封装
文章目录
- 一、map_set基本架构
- 二、迭代器
- 三、Key不支持修改问题
- 四、map支持[]
- 五、完整代码
一、map_set基本架构
1.map_set底层都是红黑树,并且map和set是通过一个红黑树的类模板进行实例化复用
1.1 map是key/value场景,set是key场景,两个容器所存储的数据的个数都不一致,怎么
实现复用同一个类模板?
解决思路:使用pair将map的key和value两个值存储起来,
红黑树底层只需要存储一个pair类型对象就可以,不需要单独存储key和value
此时红黑树中只需要存储一个pair类型的对象就可以表示map的key/value场景
存储一个key类型的对象就可以表示set的key场景
所以底层的红黑树中只需要存储一个数据data,该data的类型可以是key表示set,
也可以是pair类型表示map
1.2无论是set还是map都是通过key去查找的,查找和删除的类型都是key类型
但是此时底层的红黑树中只存储的一个数据,既可以表示set也可以表示map,
当是set时,查找和删除的类型key都可以得到,但是当是map是,底层存储的是一个pair
查找和删除的类型key就得不到
解决思路:在使用set和map对红黑树进行封装时,单独传一个key类型过去,底层红黑树
查找删除时,就使用该key类型,就可以做到无论红黑树底层是一个key还是一个pair
都可以得到查找删除时所需要的key类型
1.3当进行插入删除查找时,无论是set还是map都需要通过key进行比较,来确定是小于
相等还是大于,以此来决定是向左走还是返回还是向右走
但是此时红黑树中只存储了一个数据,既可以是key也可以是pair,那么在插入需要使用
key去比较时,就无法准确的拿出key去比较
解决思路:set和map封装红黑树时,给红黑树传递一个仿函数,该仿函数可以得到此时
红黑树中存储的数据中的key,仿函数也是一个类型,set和map的KeyOfT只会被set和
map去使用,所以就可以将set和map的KeyOfT作为set和map的私有内部类,再通过
set和map对红黑树的封装,将set和map的私有内部类KeyOfT传递过去
复习一下内部类:
1.3.1内部类和外部类是两个独立的类,只是内部类受到外部类的限制
并且内部类可以通过外部类的对象访问外部类的所有成员
1.3.2内部类其实就是在类中再创建一个类,内部类受到外部类访问限定符的修饰
(1)当内部类被外部类的public修饰,外部类类域中可以直接访问内部类
但是在外部类的类外,必须先指定外部类的类域才可以使用外部类的内部类
外部类::内部类
(2)当内部类被外部类的protected修饰,只能在外部类以及派生类的类域中访问
(3)私有内部类只能在外部类的类域中访问
(4)上述情况下,set和map的KeyOfT是属于set和map的私有内部类,只可以
在set和map的类域中被访问,但是在set/map中将KeyOfT内部类作为封装的红黑树
的模板参数,那么红黑树就会获得该set和map的内部类,并且可以直接通过该类型
实例化对象,通过对象去调用其成员函数
1.3.3内部类可以访问外部类的所有成员包括私有成员,但是外部类要访问内部类
必须遵守内部类的访问限定符,如果需要访问内部类的私有,那么就需要声明为内部类
的友元
类只是一个类型一个声明,声明该类型由哪些成员组成,不会创建实体开辟内存,
只有通过类型实例化出对象之后,才会有实体
(1)内部类访问外部类的非静态成员需要在内部类中先创建一个外部类的对象
再通过外部类的对象去访问外部类的成员
(2)内部类访问外部类的静态成员,不需要创建外部类对象,因为外部类的静态成员
属于整个类,并不单一的属于某个外部类的对象
(3)外部类要访问内部类,需要创建一个内部类的对象,然后遵守内部内的访问限定符
的修饰
1.4代码逻辑架构
RBTree的_root给一个缺省值nullptr
2.insert和find
2.1map和set的insert和find直接封装红黑树的insert和find就可以 2.2红黑树Insert和Find实现
2.3为什么要提供KeyOfT将红黑树存储的数据中的key取出来去比较
而不是直接提供一个比较的仿函数,直接对红黑树存储的数据进行比较
类似于以下代码:
原因:因为这种代码只适合在insert中去进行比较,find、erase中无法适用
insert参数是红黑树中存储的数据进行比较的左右两个操作数都是红黑树中存储的数据
find、erase中参数是红黑树中存储的数据中的key,find和erase需要先将红黑树中存储的
数据中的key取出来,然后与参数key进行比较,所以采取直接比较的仿函数的方式不可行
二、迭代器
1.map和set迭代器实现思路和insert、find一样,都是通过封装红黑树的迭代器实现
2.红黑树迭代器实现思路:
红黑树中是一个一个结点链接起来,链式结构,实现迭代器的思路和链表相似
用一个类型封装红黑树的结点的指针,再通过运算符重载函数实现迭代器
像指针的行为,++是下一个数据的位置,--是上一个数据的位置,*是当前位置中的数据
->是当前位置中结构体中的成员
3.红黑树迭代器所需要的运算符重载函数
3.1红黑树中可能存储key,那么就需要运算符重载*,使得*迭代器 == 当前结点中的key
3.2红黑树中可能存储pair,pair中存储key/value,那么就需要运算符重载->,使得
迭代器->就可以得到当前结点中pair中的key/value
3.2.1运算符重载->需要注意:只需要将当前结点中存储的数据的地址进行返回就可以
在调用时iterator->_first == iterator.operator->()->_first
3.3在使用迭代器进行遍历时,迭代器需要判断是否相等
所以也需要运算符重载== && !=
3.4迭代器需要进行++,++从当前位置的迭代器到下一个位置的迭代器
3.4.1红黑树的迭代器走的是中序遍历,所以红黑树的begin()就是最左结点的迭代器
3.4.2迭代器++实现思路:
(1)因为红黑树的迭代器走的是中序遍历,左子树根结点右子树
那么就表明,一个结点的迭代器需要进行++时,表明该结点左子树以及该结点已经走完了
那么接下来迭代器++就应该是往该结点的右子树走而该结点的右子树又分为存在和不存在
所以接下来迭代器++就需要分为右子树存在和右子树不存在两种情况讨论
(2)当右子树存在时
因为迭代器的++方式是左子树根结点右子树,而右子树又分为左子树根结点右子树
所以右子树存在时,++迭代器,迭代器就应该走到右子树的最左结点
(3)当右子树不存在时
因为迭代器++方式是左子树根结点右子树,所以当右子树不存在时,就代表该子树已经
走完了,那么就需要去找祖先结点中子为父左的父结点
为什么?
此时右子树为空,那么就代表该子树已经走完了,那么该子树又是其父结点的左子树或者
右子树,那么当该子树是其父结点的右子树时,那么就代表该父结点以及左右子树已经
走完了,也就是当前子树走完了,那么就又需要看当前子树是其父结点的左子树还是
右子树,如果又是父结点的右子树,那么又代表当前子树已经走完了,如果是父结点的
左子树,左根右,那么就表明此时父结点的左子树走完了,下一个迭代器就是该父结点的
迭代器,这就是为什么当右子树为空时,需要去找祖先结点中子为父左的父结点
由上图可知,end()最后一个有效数据的下一个位置就是nullptr
(4)迭代器++代码实现
3.5迭代器需要--,--从当前位置的迭代器到前一个位置的迭代器
3.5.1红黑树的迭代器走的是中序遍历,所以红黑树的end()迭代器--就应该到达
红黑树最右结点位置的迭代器,但是end()迭代器中_node是nullptr,无法通过
_node找到红黑树的最右结点,所以可以将迭代器的成员变量新增一个_root
此时就可以获取到_root根结点找到红黑树中最右的结点,特殊处理
3.5.2迭代器--与迭代器++的实现思路正好反过来
(1)迭代器++的实现思路是左子树根结点右子树,当前结点的迭代器++时,表明当前
结点的左子树以及当前结点已经走完了,接下来就应该走当前结点的右子树,而右子树
又分为存在或者不存在
(2)迭代器--的实现思路是右子树根结点左子树,当前结点的迭代器--时,表明当前
结点的右子树以及当前结点已经走完了,接下来就应该走当前结点的左子树,而左子树
又分为存在或者不存在
3.5.3当左子树存在时:
因为迭代器--时的思路是右子树根结点左子树,表明当前结点以及右子树以及走完了
此时需要走左子树,左子树又分为右子树根结点左子树
所以当左子树存在时,当前结点的迭代器--得到的迭代器是左子树的最右结点的迭代器
3.5.4当左子树不存在时:
因为迭代器--时的思路是右子树根结点左子树,表明当前结点以及右子树以及走完了
而且当前结点的左子树还为空,那么表明当前子树以及遍历完了
需要去找祖先结点中子为父右的父结点
为什么?
因为当前结点的左子树为空,那么表明当前子树以及走完了,如果当前子树是其父结点的
左子树那么就表明当前父结点的整棵树已经走完了,那么就需要去看当前子树是其父结点的
左子树还是右子树,如果还是左子树,那么就表明当前父结点的整颗树已经走完了,如果是
右子树,那么就表明当前父结点的右子树走完了,下一个就应该是当前的父结点
这就是为什么当左子树为空时,需要去找祖先结点中子为父右的那个父结点
3.5.5迭代器--实现
4.const迭代器
4.1const迭代器和普通迭代器只有在*和->时返回值的类型不同,一个是T&和T*
一个是const T&,const T*,所以只需要将这些类型写成泛型即可
4.2当迭代器的返回值类型写成泛型之后,就可以根据传过来的类型实例化不同类型的
迭代器
5.从类模板中取内嵌类型,由于没有实例化,编译器只能从中取静态成员不能取内嵌类型,
所以需要添加一个typename,告诉编译器兄弟,你放心吧,这里是一个类型,
不是静态成员变量
三、Key不支持修改问题
1.无论是set中的key还是map中的key/value,set和map的key都是不支持修改的
因为key一旦支持修改,那么底层的二叉搜索树就会被破坏掉,只有map的value
是支持修改的
2.解决方案:
在set中传key时自动添加一个const,在map中的pair的first自动添加一个const
四、map支持[]
1.map中支持[]运算符重载
1.1map中[]的作用,参数传递一个key,如果map中拥有该key,那么就将对应的value
进行返回,如果map中没有该key,那么就将key和value()构成的pair进行插入,此时
value()表示调用value的默认构造构造一个value,然后再返回新插入的pair中的value
2.[]实现思路:
2.1此时需要更改以下insert的返回值,insert需要返回一个pair<iterator,bool>
iterator表示该key的迭代器,如果插入成功,那么就返回新插入的key的迭代器
bool为true,如果map中已经有了key,那么插入失败,返回已有的key的迭代器
bool为false,本质上吧底层的红黑树的insert的返回值进行修改就可以
顺带吧find的返回值改了,此时我们已经实现了迭代器,那么就让find找到返回当前key
的迭代器,如果没有找到就返回end()
2.2将insert返回值修改之后,那么[]的实现就可以直接调用insert,如果key已经存在
返回的pair<iterator,bool>中就拥有当前key的迭代器,通过该迭代器返回value
如果key不存在,那么就将key和value()进行插入,然后返回的pair<iterator,bool>
中就拥有新插入的key的迭代器,通过该迭代器返回value即可
五、完整代码
1.RBTree
2.set
3.map