• STL——序列式容器


    一、容器概述与分类 1. STL容器即是将运用最广的一些数据结构实现出来。常用的数据结构有array, list, tree, stack, queue, hash table, set, map……等等。根据“数据在容器中的排列”特性,这些数据结构分为序列式和关联式两种。本篇讨论序列式容器。

    这里所谓的衍生,并非派生关系,而是内含关系。例如heap内含一个vector,priority-queue内含一个heap,stack和queue都内含一个deque,set/map/multiset/multimap都内含一个RB-tree,hash_x都内含一个hashtable。

    2. 序列式容器 所谓的序列式容器,其中的元素都可序(ordered,有位置属性),但未必有序(sorted,值未必有序)。C++ 语言本身提供了一个序列式容器array,STL另外再提供vector,list, deque, stack, queue, priority-queue等等序列式容器。其中stack和queue由于只是将deque改头换面而成,技术上被归类为一种配接器(adapter)。

    二、Vector

    1. vector 概述

    vector 的数据安排以及操作方式,与array非常相似。两者唯一差别在于空间运用的灵活性。array是静态空间、一旦配置了就不能改变,要修改空间大小,只能通过“配置新空间/数据移动/释还旧空间”这种成本很高的方法来进行。vector是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新的元素。

    2. 要使用vector,必须先包括<vector>,但SGI STL 将vector实现于更底层的<stl_vector.h>。 参见相关源码。

    3. vector 迭代器 vector维护的是一个连续显性空间,所以不论其元素型别为何,普通指针都可以作为vector的迭代器而满足所有必要条件,因为vector迭代器所需要的操作行为,如 operator*, operator->, operator++, operator--, operator+, operator-,operator+=, operator-=,普通指针天生就具备。vector支持随机存取,而普通指针正有着这样的能力。所以,vector提供的是Random Access Iterators。

    4. vector数据结构 vector所采用的数据结构非常简单:线性连续空间。它以两个迭代器start和finish分别指向配置得来的连续空间中目前已被使用的范围(容器大小),并以迭代器end_of_storage指向整块连续空间(含备用空间,容器容量)的尾端。

    5. vector的构造与内存管理:constructor,push_back
    vector缺省使用alloc(第二章)作为空间配置器,并据此另外定义了一个data_allocator,为的是更方便以元素大小为配置单位:

    template <class T, class Alloc = alloc>
    class vector
    {
        protected:
            typedef simple_alloc<value_type, Alloc> data_allocator;
        ....
    };

    于是,data_allocator::allocate(n) 表示配置n个元素空间。
    vector提供许多constructors,其中一个允许我们指定空间大小及初值:

    // fill_initialize(填充并予以初始化)-> 调用allocate_and_fll(配置而后填充)-> 
    // 调用uninitialized_fill_n(), uninitialized_fill_n()会根据第一参数的型别特性(type traits),决定
    // 使用算法fill_n() 或反复调用construct() 来完成任务(2.2.3节,图2-1),参见相关源码
    vector(size_type n, const T& value) { fill_initialize(n, value); }

    当我们以push_back() 将新元素插入于vector尾端是,函数首先检查是否还有备用空间,如果有就直接在备用空间上构造元素,并调整迭代器finish,使vector变大。如果没有备用空间了,就扩充空间(重新配置、移动数据、释还原空间)。参见相关源码。

    注意:所谓动态增加大小,并不是在原空间之后连续新空间(因为无法保证原空间之后尚有可供配置的空间),而是以原大小的两倍另外配置一块较大空间,然后将原内容拷贝过来,然后才开始在原始内容之后构造新元素,并释还原空间。因此,对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了。这是程序员易犯的一个错误,务需小心。

    6. vector 的元素操作:pop_back,erase,clear, insert

    (1)pop_back:将尾端标记往前移一格,表示将放弃尾端元素;释还尾端元素。

    (2)erase(first, last):将last之后的n个元素赋值到以first为起点的区间内(copy(last, finish, first);),释还元素;

    (3)insert操作图如下:

    三、List

    1. list概述 相较于vector的连续线性空间,list就显得复杂许多,它的好处是每次插入或删除一个元素,就配置或释还一个元素空间。因此,list对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素插入或元素移除,list永远是常数时间。

    2. list的节点 参见相关源码

    3. list的迭代器 list 不再能够像vector一样一普通指针作为迭代器,因为其节点不保证在存储空间中连续存在。list 迭代器必须有能力指向list的节点,并有能力进行正确的递增、递减、取值、成员存取等操作。所谓“list迭代器正确的递增、递减、取值、成员取用”操作是指,递增时指向下一个节点,递减时指向上一个节点,取值时取的是节点的数据值,成员取用时取用的是节点的成员。由于STL list 是一个双向链表,迭代器必须具备前移、后移的能力,所以list提供的是Bidirectional iterators。

    list有一个重要性质:插入操作和接合操作都不会造成原有的list迭代器失效。这在vector是不成立的,因为vector 的插入操作可能造成容器重新配置,导致原有的迭代器全部失效。甚至list的元素删除操作,也只有“指向被删除元素”的那个迭代器失效,其他迭代器不受任何影响。

    list迭代器的设计,参见相关源码

    4. list 的数据结构

    SGI list 不仅是一个双向链表,而且还是一个环状双向链表。所以它只需一个指针,便可完整表现整个链表。如果让指针node指向刻意置于尾端的一个空白节点,node便能符合STL 对于“前闭后开”区间的要求,成为last迭代器。

     

    5. list的构造与内存管理:constructor、push_back、insert

    list 缺省使用alloc 作为空间配置器,并据此另外定义了一个list_node_allocator,为的是更方便地以节点大小为配置单位:

    template <class T, class Alloc = alloc>
    class list
    {
        protected:
            typedef __list_node<T> list_node;
            // 专属之空间配置器,每次配置一个节点大小
            typedef simple_alloc<list_node, Alloc> list_node_allocator;
        ....
    };

    push_back() 函数内部调用insert()。

    6. list 的元素操作:  push_front, push_back, erase, pop_front, pop_back,  clear, remove, unique, splice, merge, reverse, sort

    list 内部提供一个所谓的迁移操作(transfer):将某连续范围的元素迁移到某个特定位置之前。技术上很简单,节点间的指针移动而已。 参见相关源码

    四、deque

    1. deque 概述

    vector是单向开口的连续线性空间,deque则是一种双向开口的连续线性空间。所谓双向开口,意思是可以在头尾两端分别作元素的插入和删除操作。vector当然也可以在头尾两端进行操作,但其头部操作效率奇差,无法被接受。

    deque和vector最大差异,一在于deque允许于常数时间内对起头端进行元素的插入或移除操作,二在于deque没有所谓容量(capacity)概念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。

    虽然deque也提供Random Access Iterator,但它的迭代器并不是普通指针,其复杂度和vector不可以道里计。因此,除非必要,我们应尽可能选择使用vector而非deque。

    2. deque系由一段一段的定量连续空间构成。一旦有必要在deque的前段或尾端增加新空间,便配置一段定量连续空间,串接在整个deque的头端或尾端。deque的最大任务,便是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取接口。避开了“重新配置、复制、释放”的轮回,代价则是复杂的迭代器架构。

    deque采用一块所谓的map(注意,不是STL的map容器)作为主控,这里的map是一小块连续空间,其中每个元素都是指针,指向另一段(较大的)连续线性空间,称为缓冲区。缓冲区才是deque的存储空间主体。SGI STL 允许我们指定缓冲区大小,默认值0表示将使用512bytes缓冲区。map其实是一个T**,它是一个指针,所指之物又是一个指针。

    参见deque容器相关源码;

    3. deque 的迭代器

    参见deque迭代器相关源码

    4. deque的数据结构 deque除了维护一个先前说过的指向map的指针外,也维护start,finish两个迭代器,分别指向第一缓冲区的第一个元素和最后缓冲区的最后一个元素(的下一位置)。此外,它当然也必须记住目前的map大小。因为一旦map所提供的节点不足,就必须重新配置更大的一块map。 参见deque容器相关源码

    5. deque的构造与内存管理:ctor, push_back, push_front deque自行定义了两个专属的空间配置器:

    protected:
    // 专属空间配置器,每次配置一个元素大小
    typedef simple_alloc<value_type, Alloc> data_allocator;
    // 专属空间配置器,每次配置一个指针大小
    typedef simple_alloc<pointer, Alloc> map_allocator;
    
    // 并提供一个constructor如下:
    deque(int n, const value_type& value)
        :start(), finish(), map(0), map_size(0)
    {
        // 参见相关源码
        fill_initialize(n, value);
    }

    注意,在create_map_and_nodes()函数中:

    (1)需要节点数 = ( 元素个数 / 每个缓冲区可容纳的元素个数) + 1 ;

    (2)一个map要管理几个节点。最少8个,最多是“所需节点数加2”(前后各预留一个,扩充时可用);

    (3)nstart和nfinish 指向map所拥有之全部节点的最中央区段,保持在最中央(已初始化节点位于map节点列表最中央区段。),可使头尾两端的扩充能力一样大。每一节点可对应一个缓冲区。

    push_back/push_front 参见相关源码

    什么时候map需要重新整治?这个问题的判断由reserve_map_at_back() 和 reserve_map_at_front() 进行,实际操作则由reallocate_map() 执行。

    参见相关源码。

    6. deque 的元素操作:pop_back, pop_front, clear, erase, insert

    泛型算法find() 寻找deque某个元素

    注意,deque的最初状态(无任何元素时)保有一个缓冲区,因此,clear() 完成之后回复初始状态,也一样要保留一个缓冲区。

    参见相关源码

    五、stack

    1. stack概述

    stack是一种先进后出(First In Last Out, FILO)的数据结构。它只有一个出口。stack允许新增元素、移除元素、取得最顶端元素。但除了最顶端外,没有任何其它方法可以存取stack的其他元素。换言之,stack不允许有遍历行为。

    2. stack定义 以某种既有容器作为底部结构,将其接口改变,使之符合“先进后出”的特性,形成一个stack,是很容易做到的。deque是双向开口的数据结构,若以deque为底部结构并封闭其头端开口,便轻而易举地形成了一个stack。因此,SGI STL 便以deque作为缺省情况下的stack底部结构,stack的实现因而非常简单,源代码十分简短。 除了deque之外,list也可以作为stack的底部容器。

    由于stack系以底部容器完成其所有工作,而具有这种“修改某物接口,形成另一种风貌”之性质者,称为adapter(配接器),因此,STL stack 往往不被归类为container(容器),而被归类为container/adapter。

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

    六、queue

    1. queue概述

    queue 是一种先进先出(First In First Out, FIFO)的数据结构。queue允许新增元素、移除元素、从最底端加入元素、取得最顶端元素。但除了最底端可以加入、最顶端可以取出外,没有任何其他方法可以存取queue的其他元素。换言之,queue不允许有遍历行为。

    2. queue定义 queue同样也是一种配接器,也没有迭代器。参见相关源码。

    七、heap(隐式表述,implicit representation)

    参见STL——heap结构及算法

    八、priority_queue

    1. priority_queue 概述

    顾名思义,priority_queue是一个拥有权值的queue,其内的元素并非依照被推入的次序排列,而是自动依照元素的权值排列(通常权值以实值表示)。权值最高者,排在最前面。缺省情况下priority_queue系利用一个max-heap完成,后者是一个以vector表现的complete binary tree(参见STL——heap结构及算法)。max-heap可以满足priority_queue所需要的“依权值高低自动递减排序”的特性。

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

    九、slist

    1. slist 概述 STL list 是个双向链表。SGI STL 另提供了一个单向链表,名为slist。这个容器并不在标准规格之内。slist 和 list 的主要差别在于,前者的迭代器属于单向的Forward Iterator, 后者的迭代器属于双向的Bidirectional Iterator。由于slist 是一个单向链表,STL 默认的插入操作“将新元素插入于指定位置之前”对于slist来说并不适用,故而slist 特别提供了insert_after() 和 erase_after()供灵活运用。基于同样的(效率)考虑,slist 不提供push_back(), 只提供push_front()。(因为对于slist, push_back会导致遍历整个链表,才能找到最后一个节点的位置,执行插入;slist 默认提供链表头节点指针,push_front可以快速插入。)

    2. slist 的节点 slist 节点和其迭代器的设计,架构上比list复杂许多,运用了继承关系,运用了继承关系,因此在型别转换上有复杂的表现。这种设计方式在第5章RB-tree将再一次出现。如下图:

    slist node源码参见相关源码

    3. slist 的迭代器

    参见相关源码

    注意:slist 没有实现operator--,因为这是一个forward iterator。

    4. slist 的数据结构

    参见相关源码

  • 相关阅读:
    Qt Creator 5.0 发布
    Qt编写可视化大屏电子看板系统19-横向柱状图
    Qt开发经验小技巧166-170
    Qt编写安防视频监控系统62-子模块6预置位
    Qt编写可视化大屏电子看板系统18-柱状分组图
    Qt编写安防视频监控系统61-子模块5设备控制
    如果对golang的gin框架中的handler做单元测试
    samba
    内存溢出
    动态规划面试题
  • 原文地址:https://www.cnblogs.com/yyxt/p/4979678.html
Copyright © 2020-2023  润新知