stl实现练习

记录一下仿写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_frontpop_back也要处理释放缓存区的情况

push_back:下图代码直接迭代器++,迭代器++,可能直接重置到下一个node,但是并没有创建新的缓冲区,下次push_back的时候仍然是cur!=last;
在这里插入图片描述
push_backpush_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

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点是黑色。
  3. 每个叶子节点(通常称为NIL或空节点)是黑色。在大多数实现中,叶子节点不实际存储,而是用NIL或空指针表示。
  4. 如果一个节点是红色的,则它的两个子节点都是黑色的(即,不能有两个相邻的红色节点)。
  5. 对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点。

根节点为黑高为 h 的红黑树,内部节点数最少有 2h-1 个。因此红黑树总高度为 h ,则根节点的黑高大于等于 h/2 ,内部节点数 n >= 2h/2-1。即n 个节点的红黑树的高度 h <= 2log(n+1)

红黑树每个叶子节点(通常称为NIL或空节点)是黑色。多数实现中,叶子节点不实际存储,而是用NIL或空指针表示

红黑树新创建的节点默认为红,因为插入新值时,通常作为树叶插入,如果为黑,一定违反每个节点,到其所有后代叶子节点的简单路径上,黑色节点数目相等的性质。插入时若父节点为黑,则直接插入,若为红,违反红节点子节点为黑的性质,需改变节点颜色旋转树

旋转操作于avl_tree相同
插入4种旋转
在这里插入图片描述
在这里插入图片描述
左右对称
删除6种旋转
左子节点左高度大于右
在这里插入图片描述
左子节点左高度小于右
在这里插入图片描述
左子节点左高度等于右
在这里插入图片描述
左右对称

修复红黑树性质

  1. 新节点的父节点是黑色。无需修复操作
  2. 父节点和叔叔节点均为红色。需要将父节点和叔叔节点都重新着色为黑色,并将祖父节点(父节点的父节点)着色为红色。把祖父节点(红)作为新节点向上检查,直到根节点或遇到黑色父节点为止
  3. 父节点为红色,叔叔节点不存在或为黑色。如果祖父节点,父节点,新节点是LL,右旋,RR左旋。旋转后,将原父节点改为黑色,祖父节点改为红色。如果祖父节点,父节点,新节点是LR,先左再右旋,将原祖父节点的颜色更改为红色,并将新节点的颜色更改为黑色

所有修复操作完成后,再确保根节点是黑色

删除

  1. 被删节点无子节点,且被删结点为红色。无任何操作
  2. 被删结点有一个子结点,且被删结点为红色。不存在这种情况,不符合所有链路黑节点数量相同
  3. 被删结点有一个子结点,且被删结点为黑色。则子节点必为红,直接子节点置黑代替被删节点
  4. 被删结点有两个子结点,且被删结点为黑色或红色。找到被删节点的(前驱/后继节点),用其值覆盖被删节点,转换情况为删除(前驱/后继节点),一定是只有一个子节点或者无子节点
  5. 被删结点无子结点,且被删结点为黑色。(除非是根节点,否则一定有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:左闭有开

项目地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值