十分钟带你熟悉STL之序列化容器

所谓序列化容器,就是其中的元素都可序,但不一定有序。C++语言本身提供了一个序列化容器array,STL另外再提供了vector、list、deque、stack、queue、priority-queue等等序列化容器。其中stack、queue都是deque改头换面而成的,技术上被归类为配接器。

array

内部维护连续线性空间

静态空间:如果内存空间不足,需要程序员自己控制申请一块更大的空间,将旧空间的数据和新加入的数据移至新空间,再将旧空间释放。 申请新空间——数据移动——释放旧空间

vector

vector的数据结构

  • 内部维护连续线性空间

  • 动态空间,自动扩容。

  • 扩容机制:申请新空间——数据移动——释放旧空间。一般申请的新空间是旧空间的两倍,如果新空间还是无法将数据全部装入,则申请更大的空间。

    • 因为扩容并不是在原有空间的后面进行扩容,因为原有空间后面不一定有足够的空间用来扩容,所以一旦扩容之后旧空间的迭代器就会失效,一定要注意
  • vector元素操作

    • 插入:在vector尾端插入非常方便,但是如果在非尾端插入会比较复杂,因为需要将插入位置之后的元素挨个向后移动一位
    • 删除:与插入类似,在尾端删除非常简单,删除非尾端需要将删除位置之后的元素挨个向前移动
    • 查找:可以通过迭代器自增自减、operator+、operator-迭代至任意位置
    • 修改:通过查找到任意位置,需要通过解引用符(*)来存取该位置的数据,如:iter = 0,如果iter此时指向空间首位,则iter与iter[0]一样,都是将第一位上的元素修改为0

vector迭代器:

  • 普通指针都可以作为vector的迭代器而满足一切必要条件。

    • 因为vector迭代器需要的操作行为如operator*、operator->、operator+、operator-、operator++、operator–、operator+=、operator-=,普通指针天生具备,vector支持随机存储,指针也可以满足。
  • vector维护三个迭代器:start、finish、end_of_storage

    扫描二维码关注公众号,回复: 14893822 查看本文章
    • start:配置的连续空间的首端
    • finish:配置的连续空间中已使用的尾端
    • end_of_storage:配置的连续空间的尾端

常用API

	// 创建一个int类型的Vector数组
	// 并对数组进行初始化,其中长度为5、元素为4
	vector<int> nums(5, 4);
	// 向队尾插入元素10
	nums.push_back(10);
	// 向队尾插入元素15
	nums.push_back(15);
	// 插入元素2到nums.begin()迭代器之前
	nums.insert(nums.begin(), 2); 
	// 查询数组找到第一个元素为10的位置,返回其迭代器
	vector<int>::iterator it = find(nums.begin(), nums.end(), 10);
	if (it != nums.end())
		nums.insert(it, 7);
	nums.insert(nums.end(), 99);
	// 删除最后一位元素
	nums.pop_back();
	// 删除迭代器指向的元素
	nums.erase(nums.begin() + 1); 
	
	for (int i : nums)
		cout << i << " ";

请添加图片描述

list

list数据类型

  • 内部维护:环状双向链表
// 节点
tmplate <class T>
struct _list_node{
    
    
  typedef void* void_pointer;
  void_pointer prev;
  void_pointer next;
  T data;
}

// List
template <class T, class Alloc = alloc> // 缺省使用alloc为配置器
class List{
    
    
protected:
    typedef _list_node<T> list_node;
public:
    typedef list_node* link_type;
protected:
    link_type node; // 只要一个指针,便可表示整个环状双向链表
}
  • list元素操作
    • 只要将指针node刻意指向尾端的一个空白节点,那么在首端和尾端进行插入和删除操作都是一样的,插入和删除操作都是O(1)

list迭代器

  • 自定义的数据类型——双向迭代器,需要提供迭代器正确的递增、递减、取值、成员引用
    • 递增:指向下一个节点
    • 递减:指向上一个节点
    • 取值:取出节点的数据值
    • 成员引用:取用节点的成员
  • 迭代器失效:
    插入和接合不会造成原有的list迭代器失效;
    删除只会使指向被删除元素的迭代器失效
  • 指针node指向刻意置于尾端的空白节点、node下一个节点是链表首位、node上一个节点是链表尾位,node便可以满足STL前闭后开的区间要求,成为list迭代器

slist

slist数据类型

  • 内部维护:环状单向链表
  • 根据STL的习惯,插入操作一般将新元素插入于指定位置之前,而非之后,然而对于一个单向链表,如果需要插入到指定位置之前,需要重新遍历一遍,所以对于slist起点附近的区域之外,采用insert、erase都是非常不明智的选择。对此slist专门提供了insert_after()、erase_after()供灵活使用
  • 同样为了效率考虑,slist只提供push_back(),而不提供push_front(),因此元素次序会与插入次序相反

deque

deque数据类型

  • 内部维护:分段连续空间。deque内部包含一个map(这里的map并不是STL的map容器,而是一小段连续空间)作为主控,其中元素(此处称作节点node)都是指针,指向另一端连续空间,称作缓存区缓存区才是deque的储存空间主体

  • deque是双向开口的连续线性空间(至少在逻辑看来,其实内部是分段连续空间),无论从哪个位置插入删除元素速度都是常量

    vector是单向开口的连续线性空间,虽然允许从首部插入删除元素,但是效率极差。

  • deque避开了申请新空间——数据移动——释放旧空间,代价是复杂的迭代器架构

deque迭代器

  • 因为deque内部维护的是一个分段连续空间,为了维持其“整体连续”的假象,重任就落在了operator++、operator- -上了

    • 首先迭代器需要能够判断自己是否在缓存区的边缘上,如果是,那么前进或者后退就需要跳转到下一个或者上一个缓存区的边缘,为了能够准确的跳转,需要时刻掌握map(主控)
  • 迭代器内部除了维护一个指向map的指针,还维护了两个迭代器:start、finish

    • start:指向第一个缓存区的第一个元素
    • end:指向最后一个缓存区的最后一个元素的下一个位置

stack

  • stack是一个先进后出的数据结构,只有一个出口,只需要提供顶端插入、顶端删除、获取顶端元素,除了最顶端,没有任何其它方法可以存放stack的其它元素,也不需要有遍历行为,所以stack没有迭代器

  • stack在缺省情况下使用deque作为底部结构,也就是stack内部维护一个deque完成所有工作,而stack只需要做的就是将deque的头部封掉,只提供对尾部(stack的顶端)进行操作。

    • “修改某物接口,形成另一个风貌”——被称为配接器,stack就是一个配接器,因此STL stack往往不被归类为容器,而被归类为容器配接器
  • list也可作为stack的底部结构,list作为一个双向开口的数据结构,同时也提供了empty、size、back、push_back、pop_back,只需要将list头端开口封闭,一样能够非常轻松就形成一个stack

queue

  • queue是一种先进先出的数据结构,只可以从队首取出数据,从队尾插入数据,除此之外,不存在其它存取元素的方法,也不需要遍历行为,没有迭代器

  • 底部结构跟stack一模一样,只是底层结构的处理接口方式不一样,以达到不同的风貌

heap

  • heap并不是STL容器组件,只是一个幕后英雄,扮演priority_queue的助手。

  • 内部维护:一个vector数组、一个heap算法

    • vector数组:用于存储元素
    • heap算法:内部维护一个完全二叉树
      • 根据元素排列方法,heap可以分为max-heap、min-heap
        • max-heap:每个节点的键值都比子节点大
        • min-heap:每个节点的键值都比子节点小
        • STL提供的是max-heap,因此以下算法默认使用max-heap
      • push_heap算法:
        • 条件:两个迭代器:用来表示heap底层容器(vector)的头尾
        • 算法步骤:
          • 将新加入的元素一定放在最下层,并放在从左到右第一个空格,也就是插入底层vector的end()处
          • 上溯:将新节点与父节点进行键值比较,如果键值比父节点大,则父子对换位置,如此一直上溯,直到不需对换或直到根节点为止 (vector中父子位置肯定也需要对换位置)
      • pop_heap算法:
        • 条件:两个迭代器:用来表示heap底层容器(vector)的头尾
        • 先知条件:对于max-heap,最大值一定在heap根部位置,也就是在vector的首位,pop操作之后,必须割舍掉最下层最右边的叶节点
        • 算法步骤:
          • 首先将vector首尾元素对换,也就是将heap根节点与最下层最右边的叶节点对换
          • 下溯:将调整至根节点的键值与子节点比较,如果比子节点小则与较大子节点对调,如此一直下溯,直到不需对换或直到最底层为止
        • 注意事项:每次执行pop_heap算法之后,最大元素只是被置于底部容器的最尾端,尚未被取出,可以通过底层容器的back()获取,如果想移除,则使用底层容器的pop_back()函数
      • sort_heap算法:
        • 先知条件:每次执行一次pop_heap算法,都会获得键值最大的元素,但最大值并没有被清除,而是放置在底层容器的最尾端
        • 算法核心:一直调用pop_heap算法,每次将操作范围从后往前缩减一个元素(将最大的元素置于底层容器的尾端),当整个程序执行完毕的时候,底层容器内的元素就会变成递增序列
      • make_heap算法:用来将一段现有的数据转化为一个heap
        • 算法思路:因为完全二叉树整棵树内没有任何节点漏洞,所以对于任意位置i上的节点,它的左子节点的位置一定是2i,右子节点一定是2i+1
  • heap没有迭代器

priority_queue

  • priority_queue完全以底层容器(vector)为根据,再加上heap处理规则,最后在这些的基础上修改接口,最终提供独特的规则,是一个配接器
  • priority_queue的规则:权值高者先出
  • 只需要从队尾插入数据,从队首获取数据,不存在其它方法对其他位置的元素进行操作,也不需要遍历功能,所以没有迭代器

猜你喜欢

转载自blog.csdn.net/weixin_44081533/article/details/119509362