前言
我们在开始之前,首先要明白一点,前面我们实现模拟实现红黑树以及现在的封装map和set,我们的目的主要学习它库中的框架及模板设计和复用,不是要原模原样的去实现。
(一) 修改原红黑树
(1)逐步修改并调试运行
先看库中的代码,取了核心代码。我们可以看到kv搞出来value_type,就是相关pair,pair里面的key是const,这样就可以做到key不能修改,T可以修改,
再看里面的map里面的value是pair,再看set。在没看之前,我们可能会以为库中要用两颗红黑树,一颗存pair,一颗存key,pair封装map,key封装set,但是库中是通过泛型来设计的,共用了一颗红黑树。
看库中代码,set第二个模板参数传到了value,这个value又传到了节点里面,节点又传到节点结构体上面的模板参数value,再到结构里面的value,如图红线。也就是说set而言,红黑树里的节点的值是由第二个参数决定,第二个参数是K就是K,为什么要传第一个模板参数,它要和map复用,如蓝线。第一个模板参数拿到K的类型,因为查找,删除等接口的参数是K,第二个模板参数决定了树的节点存什么,是K还是KV。这里就是用模板参数来解决使用一颗红黑树的问题。其实底层还是两颗红黑树,只是模板帮我们完成,对模板而言,传不同的参数实例化不同的类,只是同一个类模板实例化的。
第一步:
我们一步一步加,这里就不叫value_type,自己写的时候这叫T。把节点结构体里面的pair类型改成T,构造函数里面用data初始化_data。以及后面涉及到pair<K,V>&kv的全都改成T&data。map就通过第二个模板参数控制node里面的data,同理set也一样。
之前是kv现在要改为_data,把kv.first改为_data,但是这里的第二个_data并不是我们想要的,因为这里的data到底是什么并不知道,上一层加了一层泛型。虽然可以编译通过,pair是可以比较大小的,如果first小就小,如果不小就看second,但是我们只期望按key去比较,value不参与,那就要用仿函数。
我看看库里的:
第四个在自己实现的时候就省掉意思就是手动控制这个key怎么比,第三个模板参数是把keyvalue里面的key取出来。红黑树那一层是不知道T是什么,但是调用它的那一层知道也就是Map.h和Set.h知道。所以只需要在Map.h和Set.h增加一个结构体里面写仿函数,在红黑树里面,传过来一个类KeyOf,在用到的地方定义一个对象kot,帮助我们把data的key取出来,对于set直接取key,对于map取出pair里面first,comapre是控制比较大小方式可以自己加,它就是可以控制比较方式如下图写的只是部分代码:
其实这就是配套使用,达到map和set公用了一套代码。可以看到stl里面的特点擅于用模板参数做复用。
(2)红黑树的迭代器
接下来最终还要的环节就是迭代器,在库里面set普通迭代器和const迭代器都是用的const迭代器,而map的普通迭代器是普通迭代器,因为map需要修改,需要修改它的value,不能修改K,所以它是通过pair里面const K达到的;而set不能修改,它只有K,K是不能修改的。
我们先写好大致的框架:如下图
我们用node构造这个迭代器,接下来就写迭代器的功能,诸如++,解引用等。然后在红黑树里面把begin和end写出来,begin返回的是中序第一个即红黑树的最左节点的迭代器,怎么返回迭代器,就是节点指针构造迭代器,end返回的不是最后一个数据,它是最后一个数据下一个位置,直接用空去构造。
接下来就是实现++
怎么加到下一个位置,如果第一个位置开始加:
如果右不为空,下一个就是右子树的的最左节点,然后再把节点的指针给给那个节点即_node;
如果右为空,要沿着到根的路径,找孩子是父亲左的那个祖先,这需要三叉链中的parent,直到parent的右不是cur停止,最后把parent给_node。
实现–
如果左不为空,就找左子树的最右节点。
如果左为空,就找孩子是父亲的右的那个祖先,直到parent的左不是cur停止,最后把parent给_node。
也就是说迭代器的封装屏蔽了底层的细节。
我们可以先跑一下:
这里打印map是it->返回时数据的指针,相当于时pair*,在来个箭头就能访问first和sceond,这里省略了,编译器会给我们加上。加typename原因是因为它不认识这到底是静态变量还是类型,只要在类模板取这个东西就加这个typename,告诉编译器这是个类型,等类模板实例化以后去找个东西。
我们还要在Map.h里面实现一个operator[]
如图:
[]主要针对map来实现的,它要去访问第二个V值也就是pair的second,如果它没有就插入,如果有就返回key所在节点的迭代器,因为里面调用的是insert,这样就不仅能查找对应的value值,并有修改功能并可以插入新的键值对。而且并把有关的insert返回值同样set也一样改为pair<iterator,bool>,里面是是一个迭代器和布尔值,如果返回成功就返回新插入节点的构造的迭代器加一个true即make_pair(itertaor(cur), true)。
(3)最后一步最重要的
先看图:
这颗树就不是搜索树,因为set不准别人修改,如果在typedef typename::RbTree <K, const K, SetKeyOf>::itertaor iterator;
是可以,但是会存在各种各样的问题,而且红黑树里面typedef _RbTreeItrerator<T, T&, T*> itertaor; typedef __RBTreeIterator<T, const T&, const T*> const_itertaor;
第二个难道是const const K吗,这样做显然不行。
在前面我们说过,并在库里看到set的普通迭代器也是const迭代器,它其实就是为了解决这个问题,如图:
我们也这样改会出现以下的错误:
VS13和19的报错都是因为我们没写对应的构造
19的错误在用类模板进行编程,如果没有定义(自己写)一个拷贝构造函数,那就会以默认的类型为标准去构造,这里因为当被实例化const_iterator时候,它默认拷贝构造的参数也是const_iterator,而传过来的参数iterator,所以19编译才会那样的错误,虽然iterator和const_iterator底层调用的是一个类模板,在实例化时候会认为是不同的类型,所以在类模板中,不能像常规一样想一个A类型隐式类型转化const A类型,在泛型编程中,讲究要确保与模板匹配。
13的错误找到对应的构造没有找到,只找到了用节点指针的构造,编译器会以为用普通迭代器转换成这个节点指针回其实去匹配这个构造去了,但是迭代器不能转换为这个指针,所以会报错,它就说在const迭代器实现里面,参数不能转换为节点指针。
看stl库中的解决:
因为_t是一个普通对象,这个普通对象调用的时普通的begin,普通的begin返回的是普通迭代器,而这个iterator就不是普通迭代器,const迭代器和普通迭代器是同一个类模板,但是它们是同一个类模板实例化的不同的模板参数,这里的问题本质上是类型转换。我们看一下库中的解决,
分析:
我们的begin和end和库中写的一样,说明它这里并没有做改变,但是红黑树里面的迭代器构造比我们多了一个拷贝构造函数,它这里的Self和Iterator定义是不同的,Self就是自己类型的typedef,Iterator是一个普通迭代器类型,而且第二个拷贝构造参数里面没有用Self,而是用了Iterator。
1、也就是说,当使用iterator作为传过来的参数,如果类模板被实例化为iterator:这个函数就是拷贝构造。,或着如果类模板被实例化为const_iterator:它就不再是拷贝,它是一个支持用普通iterator去构造初始化cont_iterator的构造函数,其实还是拷贝构造,只是说法而已。那支持了普通迭代器去构造const迭代器,_t.begin返回的是普通迭代器,而外面那一层的iterator是一个const迭代器,就支持了隐式类型转换,单参数的构造函数支持隐式类型转换,也就是支持普通到const迭代器的构造,其实就是权限的缩小。
2、使用const_iterator作为传过来的参数,,如果类模板被实例化为iterator**,虽然调默认拷贝构造,会出现类型不匹配的错误;或着如果类模板被实例化为const_iterator,就会调用默认拷贝构造函数,因为我们自己写的拷贝构造里面的参数和它不匹配,所以才调自己生成的。
总结:也就是说it是一个普通对象,返回的是一个普通迭代器iterator,外面的iterator是const迭代器,就会涉及到一个问题,类型不一样,就会认为要去转换const迭代器,那两个不同类型转换可以依赖一个单参构造函数,支持一个隐式类型转换,就会找const迭代器的构造。如果我们不要普通版本只要const迭代器也可以解决,但是我们要和map复用,入宫这样map不可以,set不修改,但map里的value要修改,所以不能。