《STL源码剖析》序列式容器 学习笔记

序列式容器

1. vector

1. 初始化

vector<int> a(2, 9)为例,调用fill_initialize(size_type n, const T& value)来分配空间、放入元素、更新迭代器。

// 填充并初始化
void fill_initialize(size_type n, const T& value){
	start = allocate_and_fill(n, value);
	finish = start + n;
	end_of_storage = finish;
}
// 配置而后填充
iterator allocate_and_fill(size_type n, const T& x){
	iterator result = data_allocator::allocate(n);
	uninitialized_fill_n(result, n, x);
	return result;
}

fill_initialize()中,首先调用适配器(无论是一级还是二级)获取n个空间,然后取得指向这块内存首地址的指针,最后放入指定值并返回首地址的指针。

2. push_back()

​ 如果有剩余的空间,则在finish后构造一个新的内存并填入x,然后finish向后移一位。否则,调用insert_aux在最后配置新的空间,并赋值。insert_aux(iterator position, const T& x)细节如下:

  1. 后面还有空间:

    1. 将最后一个元素后移一位,然后finish++
    2. [position, finish-2)的元素向后移一位,这里的finish-2的元素即为最开始的最后一个元素,刚才已经后移了。
    3. 将 x 赋予position这个地址。
  2. 后面没有空间了:

    1. size()翻倍,如果原本为 0 ,则置为 1,设置为len
    2. 新建一个len这么大的空间,
    3. pos及之前的拷贝到新的空间,然后构造空间插入x,再将pos之后的拷贝进去。
    4. 释放原空间
    5. 将迭代器更新为指向新的空间。

3. pop_back()

finish向前移一位,然后销毁finish所在的那个空间。(finish指向的是最后一个元素的后一个)

4. erase()

iterator erase(iterator first, iterator last)
  1. [last,finish)的元素移到以first开始的地方,然后返回最后一个迭代器 i
  2. [i,finish)的空间销毁。
  3. 更新finish并返回first

5. insert()

void <vector T, Alloc>::insert(iterator position, size_type n, const T& x)
  1. 首先保证插入元素个数不为0。
  2. 如果备用空间足够:
    1. 记录当前finish这个迭代器,计算插入点之后有多少个元素elems_after
    2. 如果插入点之后的元素个数大于新增元素个数,那么当前position位置的元素会被移动到当前finish之前。
      1. [finish-n,finish)的元素移动到以finish开始的位置(移动了n个),更新finish
      2. [position,old_finish-n) 的元素移动(copy_backard)到以old_finish开始的位置。
      3. 填入 n 个 x。
    3. 如果插入点之后的元素个数小于等于新增元素个数,那么从当前position位置到当前finish之间都会被填入 x,finish之后也会被填入n-elems_after个 x。
      1. finish这填入n-elems_after个 x,并更新finish
      2. [position,old_finish)之间的元素移动到new_finish之后。
      3. [position,old_finish)之间填入n 个 x。
  3. 如果备用空间不足:
    1. 需要一个更大的新的内存空间,其长度为:old_size + max(old_size, n)
    2. 构造这块空间,并更新startfinish
    3. 做如下尝试:将[old_start, position)的元素拷贝过来,填入 n 个 x,再将[position, old_finish)的元素拷贝过来。这里使用的是uninitialized_copy()uninitialized_fill_n()
    4. 如果有异常,就需要实现commit or rollback
      1. 析构从[start, finish)的所有对象
      2. 释放从start开始的len个空间。
      3. 抛出异常。
    5. 销毁原有对象,释放空间。
    6. 更新startfinishend_of_storge

2. list

1. push_back()

调用insert(end(), x)来进行链表的插入,insert()就是一个双向链表的插入。

  1. 构造一个新的节点tmp
  2. tmp->prev=pos.node->prev, tmp->next=pos.node
  3. pos.node->prev->next=tmp, pos.node->prev=tmp
  4. 返回tmp

2. push_front()

调用insert(begin(), x)

3. erase()

  1. 标记pos的前一个节点和后一个节点。
  2. 连接这两个节点
  3. 销毁原pos的节点并回收内存
  4. 返回后一个节点

4. clear()

遍历每一个节点,销毁他。然后只留下最原始的node,让node的前后指针都指向自己。

5. remove()

遍历 list 每一个节点,如果节点值为指定值,就调用erase删除它。

template <class T, class Alloc>
void list<T, Alloc>::remove(const T& value){
	iterator first = begin();
	iterator last = end();
    while(first != last){
        iterator next = first;
        ++next;
        if(*first == value)
            erase(first);
        first = next;	
        // first被删除后,更新为后一个迭代器,以进行接下来的迭代。
    }
}

6. unique()

unique()用于移除 连续且相同 的的元素,只保留一个。从头到尾遍历每一个节点:

  1. 之后的值只要与其相同,就删掉。
  2. 不一样,遍历下一个节点。
template <class T, class Alloc>
void list<T, Alloc>::unique(){
	iterator first = begin();
	iterator last = end();
    if(first == last)
        return;
    iterator next = first;
    while(++next != last){	// 可保证每一轮 next都是 first下一个
        if(*first == *next)
            erase(next);
        else
            first = next;
        next = first;	// next 如果被删掉,这里重新设置,将两者对其,在while中对next更新
    }
}

7. transfer()

transfer()可以对 List 中连续范围的元素进行迁移。就是将这一范围的链表扣下来,并将两端与指定位置与其前一个节点连起来,然后将被挖掉那一段节点的两端连起来就可以了。transfer()是实现splice()merge()sort()reverse()的基础。

8. splice()

主要就是通过transfer()实现的。将一段 list 插入另一段 list 的某个位置

9. merge()

merge()可以将两个链表合并,但前提是两个链表必须是有序的。

  1. 挨个比较两个链表的第一个节点,直到有一个链表被遍历结束,如果*first2 < *first1,则将first2迁移到first1前,否则,first1后移。
  2. 如果first2 != last2,说明第二个链表剩的都比第一个大,直接把第二个链表剩余的部分放到第一个链表后面。

10. reverse()

  1. 如果链表为空或只有一个元素,就啥也不做。
  2. 从第二个节点开始遍历,依次将其放到(transfer)第一个节点之前,直到最后一个节点。(因为begin()指向的永远是尾端空白节点 node 的下一个节点,所以在这个过程中,它会不断地更新)。

11. sort()

STL的sort()只接受RandomAccessIterator,所以List必须使用自己的sort()

  1. 如果链表为空或只有一个元素,就啥也不做。
  2. 新建两个链表carrycounter[64]来存放一些中介数据。
  3. 只要当前链表还有节点
    1. 链表的第一个节点迁移到carry的最开始。
    2. 遍历counter的前fill层,只要有东西,就依次合并排序。
    3. 把最后的结果放到counter[i]中,如果i==fill存储到了最后一层,fill++
  4. 再把counter由低到高逐层合并,最终结果保存在counter[fill-1]中。
  5. 把这个结果交换给List。(merge只对顺序list操作,合并完依然有序)

3. deque

vector 是单向开口的连续线性空间,deque 则是一种双向开口的连续线性空间。dequevector 的最大差异,一在于deque允许于常数时间内对起头端进行元素的插入或移除操作,二在于deque没有所谓容量 (capacity)观念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。对deque进行的排序操作,为了最高效率,可将deque先完整复制到一个vector身上,将vector排序后(利用 STL sort 算法),再复制回deque

deque系由一段一段的定量连续空间构成。一旦有必要在 deque的前端或尾端增加新空间,便配置一段定量连续空间,串接在整个deque 的头端或尾端。deque 采用一块所谓的map(注意,不是STL的 map容器)作为主控。这里所谓map是一小块连续空间,其中每个元素(此处称为一个节点,node)都是指针,指向另一段(较大的)连续线性空间,称为缓冲区。map其实是一个T**,指向每一个缓冲区。

​ 中控器从中间开始设置startfinish这两个迭代器,随着元素的加入,逐渐向两侧拓展,每个迭代器含有四个元素:cur, first, last, node,分别指向所指的缓冲区的头、尾、当前填充元素的位置、这个缓冲区所归属的中控器。注意!start.cur指向这个deque的第一个元素,finish.cur指向这个deque的最后一个元素的后一个位置。
在这里插入图片描述

​ 下面是 deque迭代器的几个关键行为。由于迭代器内对各种指针运算都进行了重载操作,所以各种指针运算如加、减、前进、后退都不能直观视之。其中最关键的就是:一旦行进时遇到缓冲区边缘,要特别当心,视前进或后退而定,可能需要调用set_node()跳一个缓冲区。

void set_node(map_pointer new_node){
    node = new_node;
    first = *new_node;
    last = first + difference_type(buffer_size());
}

deque的重载运算子的主要重载就是加入了对缓冲区边界的考虑,以下举例其中几个。

difference_type operator-(const self& x) const{
    return difference_type(buffer_size()) * (node - x.node -1) + (cur - first) + (x.last + x.cur);
}
self& operator++(){
    ++cur;
    if(cur == last){
        set_node(node+1);	// 这个满了,那么移到下一个缓冲区的第一个。
        cur = first;
    }
    return *this;
}
self& operator++(int){
    self tmp = *this;
    ++*this;
    return tmp;
}

1. fill_initialize()

  1. 利用create_map_and_nodes()构建结构
    1. 计算需要的节点数
    2. 设置一个map要管理多少个节点,最少8个,最多为所需节点数+2.
    3. 分配空间
    4. 设置nstart,nfinish在中控器的中间,使前后的扩充空间一样大。
    5. 为中控器的每个节点设置缓冲区
    6. 设置start,finish,x.cur的具体内容。
  2. 利用uninitialized_fill()填充所有节点。

2. push_back/front()

  1. 如果这个缓冲区还有两个及以上的空间,就在finish.cur上构造元素,更新finish.cur
  2. 如果只有一个,就只够放一个元素,finish.cur需要放到下一个缓冲区了(finish.cur指向最后一个元素的下一个位置),调用push_back_aux()
    1. 复制插入值
    2. 如果空间不足,则需要换一个更大的中控器(reserve_map_at_back()
    3. 新建一个缓冲区(finish.node的下一个)
    4. finish设置到下一个缓冲区,更新finish.cur为下一个缓冲区的第一个。

push_front()原理与push_back()一致,但有个区别使只有在第一缓冲区没有空间的时候才在前面新建一个缓冲区,这是因为为了符合前闭后开的原则,start.cur指向的是第一个节点,而finish.cur指向最后一个节点的下一个节点。

3. reserve_map_at_back/front()

让我们回头看看一个悬而未解的问题:什么时候map需要重新整治?这个问题的判断由reserve_map_at_back()reserve_map_at_front()进行,实际操作则由reallocate_map()执行:

查看中控器前/后端的节点空间是否足够,不足则说明map已经有一端已经到边界了。调用reallocate_map()更新map的组织结构。

  1. 如果新的节点数还不到map容量的一半,这说明不需要换map,只需要把start,finish向中间挪一下。计算新的start应该在的位置。

    new_start = map + (map_size - new_num_nodes) / 2  
    			+ (add_at_front ? nodes_to_add : 0);
    
    1. 如果new_start < start,说明map应该向左移,那么顺序将所有节点复制到从new_start开始的地方
    2. 否则,说明map应该向右移,那么倒序将所有节点复制到从new_start+old_num_nodes结尾的地方
  2. 如果超过一半了,则应该更换一个更大的map

    1. 申请一个新的更大空间的map,空间大小为:map_size + max(map_size, node_to_add) + 2
    2. 再用同样的方法选择new_start的位置
    3. 把原来的map拷贝过来,并释放原map
    4. 更新map的初始地址和大小
  3. 重新设置startfinish

4. pop_back / front()

  1. 如果最后一个缓冲区不是只有finish.cur。调整finish.cur的位置,并将最后一个元素析构。
  2. 如果只有finish.cur,调用pop_back_aux()
    1. 释放最后一个缓冲区deallocate_node(x.first)
    2. 调整finish至前一个缓冲区。
    3. finish.cur指向前一个缓冲区的最后一个元素,并将该元素析构destroy(x.cur)

这里只说pop_back()pop_front()原理相同。只是判断条件为第一个元素是否在第一个缓冲区的last-1的位置。

5. clear()

  1. 对除了头尾缓冲区之外的所有的缓冲区进行元素的析构和缓冲区的释放。
  2. 如果还剩两个缓冲区,将这两个缓冲区的元素析构,并释放尾缓冲区
  3. 如果还剩一个缓冲区,将这个缓冲区的元素析构,不用释放

6. erase()

看清除点前后哪边元素少

  • 前面少,就倒序拷贝前面的元素(向后移一位),然后pop_front()
  • 后面少,就正序拷贝后面的元素(向前移一位),然后pop_back()

如果删除的节点较多,还要释放冗余的缓冲区,并更新start,finish,最后返回被删除的指定部分的第一个的迭代器。

7. insert()

  1. 插入在最前面,调用push_front(),返回start
  2. 插入在最后面,调用push_back(),然后返回--finish这个迭代器。
  3. 在中间,就调用insert_aux()
    1. 如果插入点前面元素少,在最前端插入一个与第一个元素同值的元素,把第二个到pos-1的元素整体向前用copy移一位。
    2. 如果插入点后面元素少,在最后端插入一个与最后一个元素同值的元素,把pos到倒数二个的元素整体向后用copy_backward移一位。
    3. 在指定位置插入指定值,返回pos

4. stack

​ 以某种既有容器作为底部结构,将其接口改变,使之符合“先进后出”的特性,形成一个stack,是很容易做到的deque 是双向开口的数据结构,若以deque为底部结构并封闭其头端开口,便轻而易举地形成了一个stack。因此,SGI STL便以deque 作为缺省情况下的stack底部结构。由于stack系以底部容器完成其所有工作,而具有这种“修改某物接口,形成另一种风貌”之性质者,称为adapter(配接器),因此,STL stack往往不被归类为container(容器),而被归类为container adapter

stack所有元素的进出都必须符合“先进后出”的条件,只有stack 顶端的元素,才有机会被外界取用。stack不提供走访功能,也不提供迭代器。

5. queue

queue是一种先进先出的数据结构,除了最底部可以加入,最顶端可以取出之外,没有别的办法可以存取其中的元素,所以queue也不允许遍历行为。queue也是一种配置器,没有迭代器。也用的deque.

6. heap

​ 堆是一个存储在vector中的完全二叉树,如果以大、小根堆的方式存储,则可以作为优先队列的底层机制,在大根堆中,如果将 0 号位置的元素保留(设为±∞),则留(或设为无限大值或无限小值),那当complete binary tree 中的某个节点位于vectori处时,其左子节点必位于vector2i处,其右子节点必位于vector2i+1处,其父节点必位于“i/2”处。通过这么简单的位置规则,vector 可以轻易实现出complete binary tree。这种以vector表述tree的方式,我们称为隐式表述法(implicit representation) 。

​ 以下算法的实现都是基于大根堆。

1. push_heap

  1. 先将指定元素通过push_back()放于vector的最尾端。
  2. 找到当前节点(最后一个元素)的父节点
  3. 上溯
    1. 只要当前节点没有被移动到堆的最顶端且其父节点的值比它小:
      1. 当前节点的值设为父结点的值
      2. 向上一层更新当前节点,重置父节点
    2. 将第三步结束的哪个节点的值设为最开始插入的那个节点的值(当前节点本来的值已经在之前被移动到下面了)

2. pop_heap()

  1. 将第一个元素与最后一个交换,然后--last。这时候最大值就被放到vector的最尾端,vector[1]处则是一个很小的值。标记为vector[holeIndex]
  2. 下溯:
    1. 依次比较vector[holeIndex]与其左右子节点的值,并与其较大值交换。这里要注意vector[holeIndex]的子节点的个数可能为1和2,就要区别一下,每次交换都要更新holeIndex
    2. vector[holeIndex]没有子节点的时候,说明其到了合适的那层,将vector[holeIndex]设为初值(当时移动到vector[1]的那个值)。

3. sort_heap()

​ 既然每次pop_heap可获得heap中键值最大的元素,如果持续对整个heappop_heap操作,每次将操作范围从后向前缩减一个元素(因为pop_heap 会把键值最大的元素放在底部容器的最尾端),当整个程序执行完毕时,我们便有了一个递增序列。

4. make_heap()

  1. 长度为0 或 1则不用重新排列。
  2. 计算第一个需要下溯的父节点(也就是最后一个父节点)的位置Distance parent = (last - first - 2) / 2
  3. 从后向前对每一个父节点进行一次下溯。

heap 的所有元素都必须遵循特别的(complete binary tree)排列规则,所以heap不提供遍历功能,也不提供迭代器。

7. priority_queue()

​ 顾名思义,priority_queue是一个拥有权值观念的queue,它允许加入新元素、移除旧元素、审视元素值等功能。由于这是一个queue,所以只允许在底端加人元素,并从顶端取出元素,除此之外别无其它存取元素的途径。priority-queue带有权值观念,其内的元素并非依照被推入的次序排列,而是自动依照元素的权值排列(通常权值以实值表示)。权值最高者,排在最前面。

​ 之前有提到堆可以作为优先队列的底层机制,所以具体实现是依赖heap实现的

1. push()

  1. 指定值放入容器的最后
  2. 利用push_heap()调整顺序

2. pop()

  1. 利用pop_heap()调整顺序
  2. 利用pop_back()得到极值。

priority_queue的所有元素,进出都有一定的规则,只有queue顶端的元素(权值最高者),才有机会被外界取用。priority_queue不提供遍历功能,也不提供迭代器。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值