记录一下仿写stl练习过程中遇到的一些问题
construct
placement new不负责分配内存,仅调用构造函数初始化对象,需手动调用析构函数(而非delete),且需确保内存的正确释放
vector
思路及踩坑点记录:
构造函数:直接构造先分配再填充allocate_and_fill,拷贝构造和赋值构造先分配再复制allocate_and_copy。3个参数时通过std::is_integral区分传入的参数是迭代器(allocate_and_copy)拷贝还是整形直接构造(allocate_and_fill)。移动构造直接改变三个指针指向
析构函数:通过分配器销毁元素(调用析构)并回收内存(free)
resize:缩小时销毁元素,但是不改变内存,分配器只destroy不deallocate。放大且内存足够时,直接调用初始化填充。放大但内存不足时,申请最少2倍的新内存,先初始化拷贝旧元素,再初始化填充新元素,然后销毁并回收旧内存。
reverse:仅扩大有效。申请参数大小的新内存,初始化拷贝旧元素,再销毁回收旧内存
取迭代器:逆向迭代器,通过逆迭代器类创建逆迭代器对象返回,其余直接返回指针
insert:和构造同理,三个参数时,区分传入的是迭代器还是整形。整形的话可以直接将插入后的元素往后复制(根据内存容量判断是否申请新内存)。迭代器的话,std::copy和std::copy_backward会调用赋值构造函数,所以copy的目标区域本身必须存在元素.即insert_aux里,将将元素复制到同一个序列的右边时,必须先检查序列右边的元素是否有值可以赋值,没有的则会赋值失败,迭代器索引相关地址时仍为空.std::vector调用insert时,如果右移的序列没有元素,则调用移动拷贝,有元素则直接调用移动赋值。因此自己实现时,要先进行自定义初始化拷贝(自定义的会调用拷贝构造,而不是赋值构造),不能直接调用copy_backward全部往右拷贝
erase:把删除段右边的元素往左边拷贝,再销毁右边多余的元素
swap:直接交换两个vector的三个指针
clear:只销毁不回收
list
list_iterator 链表的元素是个双向指针节点。要创建个链表节点模板类。节点是不连续的,因此不能直接用T*做迭代操作(++,–等)。需要额外做一个节点迭代器模板类(内部包含一个链表节点实例)来特殊处理各种操作(移动索引,*取值等)
list迭代器设置三个模板参数,为了方便设置const T* 和const T&
使用模板参数二,三 定义别名pointer和reference。保证* 解引用const_iterator迭代器的返回值是const T* 和const T&。如果用T和T&做别名, 解引用const_iterator的返回值可修改
typedef slist_detail::slist_iterator<T, const T*, const T&> const_iterator;
第一个模板参数是T,不为const T,因为list_iterator里需要第一个模板参数定义 typedef slist_node* nodePtr;
list也是前闭后开,head,tail类似vector的start,finish
构造函数 需额外创造一个初始空节点,作为head、tail指向目标。后续head指向元素后,tail指向空节点.赋值构造时要释放原节点,并且只能赋值节点里的data,直接复制拷贝节点,会把next和prev也复制过去
析构函数 删除元素节点后,额外删除初始空节点
operator== 是列表相同位置的元素内容判断相等,因此不能用迭代器相等判断(判断的是list_node相等),而是取迭代器做相等判断(取list_node的data相等判断)
operator== head和tail判断相等是list_iterator判断相等。虽然list_iterator代替的是指针的操作。所以list_iterator相等判断的是包含的list_node指针是否相等(类似两者是否指向同一个list_node*)
insert 插入是插入position前一个。注意处理为空的情况,直接push_front(),否则position前一个为空报错
erase 同理,position为end直接返回end,为begin,直接pop_front(),返回begin.中间删除,返回删除元素的下一个迭代器位置
splice 改变被搬运链表的指针,再通过改变被搬运的元素指针插入目标链表。注意处理head元素的情况 (搬运范围迭代器也是前闭后开)
stl算法sort 需要随机访问迭代器,list是双向访问迭代器,需要自定义sort.核心是归并排序(分治算法)
list的sort
deque
deque_iterator 控制中心map是一个T数组,每个T指向一组T buffer。迭代器里需要一个指向控制
中心map的指针即T**。一个T指向当前T元素。一个first一个last的T指向当前buffer的首尾。
deque_iterator& operator+= (difference_type n)不能加const,返回引用,需要修改内部指针位置
deque_iterator operator+ (difference_type n) const加const,返回拷贝的迭代器,自身内部指针不修改
使用constexpr定义一个buffer长度的常量
构造
注意deque里的迭代器finish不是指向map的最后一个元素下一个,finish就是指向map最后一个元素,而finish迭代器里的last指针才是指向最后一个元素所指buffer的末尾元素下一个位置。而实现前闭后开.
以两个迭代器为参数构造时,创建一个初始元素0的队列(类似deque()),然后以此push_back迭代器里的元素.
拷贝构造和拷贝复制,对比vector,vector复制后只需改变start,finish指针位置,finish到end_of_finish的值可以无视。deque需要判断是否erase,要把多余的缓存区释放掉
insert :判断剩余缓存区容量。不够则调用向后扩充函数reserve_elements_at_back/reserve_map_at_back
reserve_elements_at_back/reserve_map_at_back:向两端申请所需元素内存,start/finish指向的缓冲区足够,则直接改变cur的位置,不够则生成新的缓冲区。
back如果扩充的元素刚好占满buffer_size的倍数,需要额外在开辟一个缓存区,让finish指向新开辟缓存区的first。因此
size_type add_nodes_num = (n - 1 - surplus_n) / buffer_size + 1;
front不用额外开辟一个缓存区,因此
size_type add_nodes_num = (n - 1 - surplus_n) / buffer_size + 1;
reserve_map_at_back/reserve_map_at_front :判断控制中心剩余容量,不够则调整控制中心reallocate_map。用于push_back/push_front和reserve_elements_at_back/reserve_map_at_back,只是判断是否需要reallocate_map,即将控制中心移到vector中间或者迁移到新的vector。不会分配新的缓存区
reallocate_map :如果控制中心本身内存足够,则copy_back移动控制中心的位置,否则重新分配内存,并copy控制中心的元素(类似vector扩充)。拷贝的元素是指针,可以直接std::copy/std::copy_back。不需要再目标位置先创建元素。调整控制中心只是纯粹调整内存。start和finish的相对位置不会有任何改变
deque除了需要T的分配器,还需要定义一个T的分配器,用来分配控制中心(元素存储的是T)的内存
clear : clear的时候要把空的缓存区内存一起释放掉,否则会内存泄漏。因为析构时时是从start到finish遍历释放缓存区。clear是start的位置已经改变。另外push_front和push_back时也可能重新生成缓存区
同理pop_front和pop_back也要处理释放缓存区的情况
push_back:下图代码直接迭代器++,迭代器++,可能直接重置到下一个node,但是并没有创建新的缓冲区,下次push_back的时候仍然是cur!=last;
push_back 和push_front:注意区分是迭代器的cur++/–还是迭代器本身++/–,并且迭代器自身++/–操作符里cur++/–的顺序。以start和finish永远是前闭后开的规则处理。
因此保证finish的cur永远不能和finish的last相等,当finish的cur与last相等时,创建下一个缓冲区,并将finish指向其的first
insert:deque通过迭代器实现了++,–,+(int),-(int),所以在赋值时可以直接调unintialized里的函数即copy哪些,当作连续内存处理。注意扩充内存的处理。判断插入位置在现有元素位置,前半段则往前扩展,后半段则往后扩展。如果调用了reallocate_map后,只有start和finish的node地址是改变了的,而参数里position的node地址未改变,所以要根据距离重新计算插入的position
insert单个和多个val实现了不同逻辑,应该是为了让insert单个的逻辑更简化
slist/forward_list
构造函数:slist的head是空节点,第一个元素从head的next节点开始算,即begin()。end()函数直接就新建一个值为0的迭代器类型返回。注意这个返回的迭代器不是真正的尾巴,因为单向链表要想获得尾巴,只能从头开始一步一步走到最后。但尾巴一定指向空节点
赋值构造时,将两个链表以此从头往后遍历,每个元素复制,知道其中一个链表到末尾,再判断是erase_after还是insert_after
erase_after :first和last是前开后开区间
insert_after:是前闭后开
splice_after :也是前开后开,都是因为迭代器是单向的。所以改变指针时,无法获取first的前一个,只能删除first后面的
merge :不能直接套用list的merge,否则会死循环。
slist插入的是tmp的下一个元素,it2++只后移了一次,不会指向被搬走元素的下一个位置,而是刚好指向被搬走的元素
注意slist的大部分操作都是迭代器的后一位。以及区分迭代器和node本身的使用
queue
queue是容器适配器,模板参数是<class T, class Container = deque>
内含一个Container ,可以是deque,或者list。接口都是调用Container 的接口
注意swap调换的也是other的Container
queue没有迭代器相关的操作。无法遍历,for循环需要容器有begin接口‘
heap
make_heap: 通常是vector,构建的参数是RandomAccessIterator
堆中父节点的索引为 i ,则左孩子节点的索引为 2i + 1,右孩子节点的索引为 2i + 2,i结点的父结点下标就为(i–1)/2
最后一个节点的父节点是最后一个非叶节点,下标为(len/2) -1
自下而上构建 :倒序遍历堆(即层序遍历的倒序),依次将每个非叶节点当作堆顶进行下沉操作,复杂度O(n)
自上而下构建 :创建一个空堆,遍历列表,依次对每个元素执行入堆操作。即放入堆尾部,再进行上浮操作。入堆操作是O(
log
\log
logn),n个元素建堆的复杂度是O(n
log
\log
logn)
push_heap : 把容器的最后一位元素进行入堆(vector要先push_back入堆元素),对最后一个位置元素进行上浮操作
pop_heap :弹出堆第一个元素并放到容器的末尾位置(然后vectorpop_back),交换第一个元素到末尾位置,再对第一个元素进行下沉操作
sort_heap :每次执行pop_heap,就把极值放入末尾,然后再对前半部分pop_heap。最终排序所有元素
默认为大根堆(父节点的值大于或等于子节点的值),所以默认排序从小到打
priority_queue
priority_queue是容器适配器,模板参数是template<class T,class Container = std::vector,class Compare = std::less
默认的container是vector,以及一个比较仿函数判断优先级。仿函数的模板参数就是vector的value_type
rb_tree
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色。
- 每个叶子节点(通常称为NIL或空节点)是黑色。在大多数实现中,叶子节点不实际存储,而是用NIL或空指针表示。
- 如果一个节点是红色的,则它的两个子节点都是黑色的(即,不能有两个相邻的红色节点)。
- 对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点。
根节点为黑高为 h 的红黑树,内部节点数最少有 2h-1 个。因此红黑树总高度为 h ,则根节点的黑高大于等于 h/2 ,内部节点数 n >= 2h/2-1。即n 个节点的红黑树的高度 h <= 2log(n+1)
红黑树每个叶子节点(通常称为NIL或空节点)是黑色。多数实现中,叶子节点不实际存储,而是用NIL或空指针表示
红黑树新创建的节点默认为红,因为插入新值时,通常作为树叶插入,如果为黑,一定违反每个节点,到其所有后代叶子节点的简单路径上,黑色节点数目相等的性质。插入时若父节点为黑,则直接插入,若为红,违反红节点子节点为黑的性质,需改变节点颜色旋转树
旋转操作于avl_tree相同
插入4种旋转
左右对称
删除6种旋转
左子节点左高度大于右
左子节点左高度小于右
左子节点左高度等于右
左右对称
修复红黑树性质
- 新节点的父节点是黑色。无需修复操作
- 父节点和叔叔节点均为红色。需要将父节点和叔叔节点都重新着色为黑色,并将祖父节点(父节点的父节点)着色为红色。把祖父节点(红)作为新节点向上检查,直到根节点或遇到黑色父节点为止
- 父节点为红色,叔叔节点不存在或为黑色。如果祖父节点,父节点,新节点是LL,右旋,RR左旋。旋转后,将原父节点改为黑色,祖父节点改为红色。如果祖父节点,父节点,新节点是LR,先左再右旋,将原祖父节点的颜色更改为红色,并将新节点的颜色更改为黑色
所有修复操作完成后,再确保根节点是黑色
删除
- 被删节点无子节点,且被删结点为红色。无任何操作
- 被删结点有一个子结点,且被删结点为红色。不存在这种情况,不符合所有链路黑节点数量相同
- 被删结点有一个子结点,且被删结点为黑色。则子节点必为红,直接子节点置黑代替被删节点
- 被删结点有两个子结点,且被删结点为黑色或红色。找到被删节点的(前驱/后继节点),用其值覆盖被删节点,转换情况为删除(前驱/后继节点),一定是只有一个子节点或者无子节点
- 被删结点无子结点,且被删结点为黑色。(除非是根节点,否则一定有brother)
brother为黑色,且brother有一个与其方向一致的红色子结点son,(包含有两个红色子节点的情况)
brother为黑色,且brother有一个与其方向不一致的红色子结点son
brother为黑色,且brother无红色子结点。(区分父节点为红(结束递归),父节点为黑(继续递归,以父节点为删除节点平衡))
brother为红色,则father必为黑色.brother必双黑子节点。父节点设红,brother设黑后旋转。旋转后再以被删除节点重新平衡
红黑树默认有一个header节点,空红黑树header的left和right都指向自己,parent指向nullptr。非空红黑树,header的parent指向root,root的parent指向header,left和right分别指向红黑树最左端和最右端的节点。
header为红,root为黑
begin()返回的迭代器指向header的left节点,而end()返回的迭代器指向header自己
注意和list的新节点一样,申请了新节点的内存地址后,要单独调用construct对节点里的value地址调用placement new,不能直接使用uninitialized_fill_n。
clone_node 复制节点的值和颜色,左右子节点为nullptr;
构造函数:赋值构造时,要先clear,再copy
copy、earse :私用对整棵树操作,公有对单个节点操作
insert_unique:左闭有开