【《STL源码剖析》提炼总结】 第3节:容器_1 序列式容器 vector list deque stack queue heap priority_heap

文章目录

  • 引言
  • 序列式容器概览
  • `vector` 可变长数组
    • 特点
    • 成员
    • 扩容机制
    • 迭代器
      • 迭代器失效
      • 迭代器属性
    • vector的常用方法
  • `deque` 双向队列
    • 特点
    • 中控器
    • 迭代器
      • 迭代器失效
      • 迭代器属性
    • 成员
    • 扩容机制
    • 常用方法
  • `list` 双向环形链表
    • 特点
      • 链表节点
    • 成员
    • 迭代器
      • 迭代器失效
      • 迭代器属性
    • 排序
    • 常用方法
  • `forward_list` 单向链表
    • 特点
    • 迭代器
  • `stack`
    • 特点
    • 成员
    • 迭代器
    • 常用方法
  • `queue`
    • 特点
    • 成员
    • 迭代器
    • 常用方法
  • `heap` :被定义于算法形式
    • 特点
    • 常用函数
  • `priority_queue` 优先队列
    • 特点
    • 常用方法

引言

​ 容器由序列式容器和关联式容器构成,前者按是线性的,后者则是以键:值形式的数据结构(set和unordered_set的键值是一样的)。

序列式容器概览

​ 序列式容器:其中的元素都可序,但未必有序。

vector 可变长数组

特点

​ vector的特点在于物理上其位于一个线性的空间,但是其长度是可以扩充的——这体现在两个方面,一个是分配内存的时候并不是以当前元素个数来分配,而是会多一些,这使其获得了变长的能力,其二是当预分配的内存满了之后,它会找一个2倍于当前长度的空间,并整体移动过去。

成员

​ vector的成员有三个指针:

  • start:指向当前空间的起点
  • finish:指向已存储部分的下一个位置
  • end_of_storage:指向整个分配的空间的下一个位置

扩容机制

​ 如上文所言,当vector预先分配的空间被使用完毕后,vector会进行空间扩容,一个2倍于当前长度的空间,并整体移动过去,对应的开销是很大的,因此若是进行若干次插入且其值很大的话,可以提早用reserve使其预分配一个足够大的空间,这样就省去其空间多次增长和移动的开销了。

迭代器

​ 因为是一整块的连续空间,因此迭代器遍历非常方便,早期的vector迭代器甚至是原生指针。

迭代器失效

​ 插入元素会导致其后的迭代器失效,若进行扩容则迭代器全部失效。

迭代器属性

​ 迭代器可以任意访问其中元素,因此是random_access类型

vector的常用方法

  • push_back : 从尾部插入
  • pop_back:弹出尾部元素
  • front:返回尾部部元素,值得一提的是返回的是引用类型
  • insert:在指针所指位置插入一个元素,所有其后的元素后移一个位置
  • erase:删除某个位置或者某个范围的元素,其后的元素前移

deque 双向队列

特点

​ 双向队列,顾名思义,可以向着两个方向扩充,与vector不同。它的特点是分段连续,通过多个相同长度的小区域拼接而成,因此扩充相对容易。不过也因为其物理上实际上是不连续的,因此迭代器的设计更为复杂。

中控器

​ 为了管理一个个小区块,deque内部有一个数组(其名为map),其指向每个小区块,区块中的每个元素类型为T,因此指向小区块的指针为T*,因此指向map的指针为T**类型。

【《STL源码剖析》提炼总结】 第3节:容器_1 序列式容器 vector list deque stack queue heap priority_heap_第1张图片

迭代器

​ 迭代器有4个部分:

  • first:指向当前小区块的起始位置
  • last:指向小区块的结束位置的后一个位置
  • cur:指向[first,last)中的某个位置
  • node:因为迭代器需要在小区块之间跳转,因此需要有一个指回map的指针,node指向map中的对应位置(map[n]位置,n为小区块序号)

​ 因为物理上是分段的,因此迭代器在前进后退的时候需要有跳转功能,有时要跳转多个区块,这可以通过 len/size of buffer 计算出来,然后在map中直接跳转,同时两个迭代器进行相减(获得它们相差的距离)也不能直接相减,而是需要计算它们直接差的小区块。

迭代器失效

​ 因为deque是顺序的,对其操作也会导致其迭代器失效,比如插入、删除、map扩容等等。

迭代器属性

​ 虽然物理上不连续,但是deque还是可以通过O(1)的复杂度来访问其成员,因此是random_access类型

成员

  • 指向中控器map的指针(T**类型)
  • 存储map长度的map_size
  • 两个迭代器start和finish,前者的cur指向整个deque的起始点,后者指向deque的结束点的下一个位置

扩容机制

  • 因为deque前后都有若干空间(start的cur和first之间,finish的cur和last之间),因此deuqe可以从头尾插入
  • 在当前小区块满了之后,deque会申请一个新的小区块,然后更新start/finish以及map
  • 若map也满了,则会实行对应的扩容操作:
    • 首先会对map当前已用空间进行判断,比如start指向的node已经是map第0个元素,而finish的node指向的元素距离map的尾端还有相当大的距离——已用长度小于总长度的一半,则只会将map中的元素重新排布,将其放在中间
    • 若map中的空间确实不够用了,则会进行类似vector的扩容,将map的长度扩充为当前的两倍,同时将旧的数据移动到新的map的中部,方便向两端扩充
  • 在插入的时候,程序会计算向头端和尾端哪个距离近(这意味着向外移动的元素个数不一样),从将往元素少的一端对应的元素往外移动,降低插入的开销。(这是vector做不到的,其插入头部就意味着所有元素需要往后移动一位)

常用方法

  • push_back
  • push_front

list 双向环形链表

特点

​ list为双向环形链表,其头指针指向一个空白节点,即尾节点(end的位置),尾节点的next指向的节点即开始位置(begin)。双向说明其指针域有两个指针,指向前一个元素和后一个元素。因此若是list为空,则空白节点的next和pre都指向自己。

链表节点

​ 链表本身一般只有指向特定位置的指针,链表指向的节点还需另外定义。

​ 链表节点被继承为两个部分,父类为指针域,子类为数据域,我的猜测是这样可以通过父类指针直接指向子类而不用关注子类中数据的类型。(早起没有使用继承关系的时候指针域是void*类型)

成员

​ 一个指针,指向空白节点。因为是双向环形链表,因此一个指针就可以很方便地找到其头尾。

​ 还有一个显示其长度的值。(根据侯捷老师的课件,早期没有,获取长度需要遍历一遍)

迭代器

​ 迭代器其实就是一个指针,指向节点即可。

迭代器失效

​ 链表插入删除不会影响其他节点,因此指向其他节点的迭代器是不会失效的。

迭代器属性

​ 双向访问,且不能随机访问,因此是bidirectional类型

排序

​ 因为sort需要random_access_iterator类型的迭代器,因此list自己定义了对应的排序方法,其为一个很巧妙的非递归归并排序。

​ 其使用一个数组(用来挂载链表),长度为64,说明其上限为2^64

​ 主要思路:数组的第i个位置的上限为2^(i+1) -1,因此第0个位置可以挂载0/1个节点,挂载2个节点后会发生转移,将其转移到第1个位置上,在第1个位置上从0->2->4后,也会发生转移,将4个节点转移到第2个位置上,这样一直向上转移,在转移的时候使用merge操作对其进行合并,因此在最高位置的那一串链表以及其他零散的链表在进行merge就是排序结果。

链表合并的大致过程:
第i位置:     0 1 2 3 4 
            1
            0 2
            1 2
            0 0 4
            1 0 4
            0 2 4
            1 2 4
            0 0 0 8
上面的数字是对应位置有几个节点,就这样一次次往上传递

可供参考:list的sort图解

常用方法

  • push_front
  • front
  • pop_front
  • push_back
  • back
  • pop_back
  • sort

forward_list 单向链表

特点

​ C++11新增的容器,结构与list几乎完全,据说被用于对性能比较敏感的时候。

迭代器

​ 为forward类型,即只能往后前进(因此只有++运算符)

stack

特点

​ stack即采用先进后出,是数据结构中一个非常重要的部分。

​ stack实际上就是对内部封装的其他容器的操作,因此被称为容器适配器(container adapter)

​ 只要拥有stack对其内部使用的方法,这个容器便可以称为stack的底层容器,因此对应容器有: deque(默认类型),list,vector

成员

​ 内部封装的容器

迭代器

​ 因为操作都是内部容器实现,且要实现严格的先进后出,所以stack没有遍历功能也没有迭代器。

常用方法

  • push
  • pop
  • top

queue

特点

​ queue即采用先进先出,同样是数据结构中一个非常重要的部分。

​ 和stack一样,queue内部封装了一个容器,因此也是一个容器适配器。

​ 可以完全适配的有两个容器:deque(默认)和list。

成员

​ 只有一个,即内部封装的容器

迭代器

​ queue同样不提供遍历功能和迭代器。

常用方法

  • push
  • pop
  • front

heap :被定义于算法形式

特点

​ heap即堆,为一种特殊的二叉树,其根节点的值比子树的节点都要小/大,对应的为小/大根堆。

​ 因为堆为满二叉树结构,因此可以将其的物理结构转换为一个列表,从0开始的话,第i个节点的子节点的位置是2*i+12*i+2

​ heap实际上被实现为算法形式,通过其对序列式容器进行操作,从而达成对应的效果(即位置在,且为函数模板的形式。

常用函数

  • make_heap:将数据组合成为一个大/小根堆
  • push_heap:[first,last-1)范围内为一个堆,此时last-1位置为一个新插入元素,对其操作将最后一个元素加入堆,使得[first,last)范围内的元素符合堆的定义
  • pop_back:将头结点放在末尾元素,然后通过调整位置,将末尾元素放在适合的位置,此时的[first,last-1)范围内为一个堆
  • sort_heap:执行n次pop_heap,若为大根堆则结果为升序

priority_queue 优先队列

特点

​ 优先队列即内部封装了一个vector(默认),对其操作实际上都是heap相关操作,因此它也是一个容器适配器。

​ 优先队列提供类似队列的操作,只允许push和pop进行修改,插入的元素会按照堆的规则进行排列(push_heap)。

​ 在定义的时候可以指定其compare函数,即比较规则,因此可以自定义是大根堆还是小根堆。

常用方法

  • push
  • pop
  • top

你可能感兴趣的:(《STL源码剖析》提炼总结,c++,list,数据结构)