所谓序列化容器,就是其中的元素都可序,但不一定有序。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
- 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的规则:权值高者先出
- 只需要从队尾插入数据,从队首获取数据,不存在其它方法对其他位置的元素进行操作,也不需要遍历功能,所以没有迭代器