priority_queue
适配器
适配器简单的来说就是一种设计模式,C++的STL中的大部分模板都有着相似的功能,与相同的接口函数名,类似于这样的:
- push_back(); pop_back(); push_front(); pop_front();
- size(); empty();
所以说对于这种类型的容器,就可以对他们进行封装,从而实现一些列的功能,就比如说:
- stack,queue
- deque,priority_queue
虽然对于队列和栈,我们也可以用一些数据结构来实现,但是他们的功能STL中的其他容器都具备,所以STL中并没有将stack和queue划分在容器这一类,而是用了一个容器适配器。
有了容器适配器,我们就可以根据不同的使用场景来确定使用不同的容器
- 经常要头插元素的话,我们就可以使用
lis
t当做适配器的类型,因为他的底层是一个带头结点的双向循环链表,头插时间复杂度是O(1)
- 如果要经常访问元素的话,我们就可以使用
vector
作为适配器的类型,他的底层是一个数组,数组对于访问的时间复杂度是O(1)
- 在STL中他是用一个
双端队列deque
来实现的,他结合了list插入效率高,以及vector访问效率高的特点。但是他却不适合遍历,他的内部是一段一段的有长度的地址区间,每一次访问都需要检测是否越界,所以遍历有些困难。
priority_queue
priority_queue
,翻译过来就是优先队列的意思。
他是一个封装在queue
中的一个容器适配器,底层默认是用vector
来实现,并且默认是降序,也就是默认是一个大堆。
与队列的不同点
队列作为一个先进先出的标准数据模型,它内部的数据不能保证有序。
STL基于这一特点,对queue做了改进,让他每一次出队的数据保证是当前队列的最值(最大值,最小值),但是却不满足了先进先出的特点。
因为优先队列需要满足每次front()
都是队列的最值,还有不停的插入元素,那么单纯的用sort()
函数进行排序是不行的,因为他每一次的时间复杂度都是O(n * log(n))
。
这个时候就可以使用堆,建一个大根堆或者小根堆,每一次获取队头元素都是堆顶的元素,都是可以保证是最值,而且插入删除再次建堆的时间复杂度也只是O(log(n))
。
priority_queue实现的功能
- pushk();,尾插一个数据 ,内部进行向上建堆
- pop();,删除队头数据,也就是队列中的一个最值,然后内部进行向下建堆
- top();,返回队列中头部元素,也就是队列中的最值
- size();,返回队列中元素的数量
- empty();,判断队列是否为空
复习一下堆
堆是一种特殊的二叉树结构,他的每一个根节点都是这棵子树的一个最值(最大值或者最小值)
- 根节点是最大值,这是一个
大根堆
- 根节点是最小值,这是一个
小根堆
他的存储可以用一个数组来实现,不必建一个二叉树。这里有一个注意的点就是:(假设父亲节点为parent)
- 当你选择的第一个元素的下标为0,那么他的左孩子节点就是
parent * 2 + 1
- 如果选择第一个元素下标为1,那么他的左孩子节点就是
parent * 2
说到这个不禁想起上学期期末的数据结构考试,考试之前自己实现了一下堆,当时用的是数组嘛,下标都是从0开始的,这样求左孩子节点是
parent * 2 + 1
考试的时候问了二叉树的左孩子节点,想都没想,直接
parent * 2 + 1
,事后才发现,二叉树根节点是按照1算的,也就是parent * 2
图解堆删除元素的过程
画图的步骤我就不好意思的用
leetcode
上的可视化来显示了
- 以一个大根堆为例,这是一开始的结构
- 每一次比较删除之后
堆的向下调整,对应pop
- 向下调整一般是建立在删除或者排序的时候,这里因为vector数组的原因,删除的时候选择先交换,再删除末尾元素。因为删除开始位置的元素开销太大
- 向下调整的开始点一般在根节点的位置
//向下调整,数组默认以0开始
void AdjestDown(vector<int> arr,int parent)
{
size_t child = parent * 2 + 1;
size_t len = arr.size();
while (child < len)
{
//找左右孩子中最大的
if (child + 1 < len && arr[child]< arr[child + 1