STL之优先级队列,以及其仿函数实现
温馨提示,在学习优先级队列之前,建议大家可以将之前的数据结构中的堆复习一遍
1.容器适配器
我们之前讲了string串,vector动态数组,以及list的链表,按照我们之前数据结构的顺序按道理,我们今天应该学习stack以及queue,了,但是在我们stl这里我打算把它俩大致说明一下,而不作为重点来讲解,因为,stack以及queue在我们数据结构中的实现,就是依托于数组和链表来实现的,而我们STL作为数据结构的类集合,对于stack以及queue的实现也是通过包装一个vector,以及list来实现的,就这样这种,依托于别的容器来实现自己管理数据的模式就叫做容器适配器。
1.1STL中stack的接口
1.2STL中queue的接口
2.STL中的优先级队列
priority_queue
与前面两个相同,优先级队列也是一种容器适配器,所以对于容器适配器的了解是非常有必要的。
- 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。
- 此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元
素)。 - 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特
定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。 - 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭
代器访问,并支持以下操作:
empty():检测容器是否为空
size():返回容器中有效元素个数
front():返回容器中第一个元素的引用
push_back():在容器尾部插入元素 - 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。
- 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。
2.1priority_queue的使用
优先级队列,默认的使用vector来作为存储数据容器,在vector上有使用了堆算法将vector的数据存储数据进行了更换,所以我在文章的开头提到,大家要复习之前学习的堆的实现。,同时注意默认情况下priority_queue默认实现的是大堆。
函数接口:
1.priority_queue()/priority_queue(first,last),优先级队列的构造函数,后面的first和list是指我们依托容器的迭代器。
2.empty(),检查我们的优先级队列是否为空
3.top()返回优先级队列的第一个值
4.push()尾插数据
5.pop()删除优先级队列中最大或最小元素,就是堆顶元素,
。
2.2容器适配器在优先级队列中的设计
我们先看标准库中stack和queue这种是怎么设计的:
这里就是模板的另一个应用,就是类模板的参数,我们可以传容器的类型模板,同时支持函数缺省值的玩法,所以我们通常给优先级队列设计模板就会有下面这个参数:
class Container =vector
默认用vector作为底层容器。
2.3重温堆知识
记得当时我们的堆有三个重点:
1:弄明白,堆物理结构是个数组,逻辑结构是个父子节点大小严格的完全二叉树结构。
2.堆建立的规则——我们用建立大堆,即父节点总是大于任意子节点,来拍一个升序;我们用建立小堆即父节点总是小于两个子节点,来实现升序。
3.堆建立的两种算法:向上调整算法,向下调整算法。
3.优先级队列的底层实现
3.1优先级队列的成员
前面我们介绍过了,优先级对了是种容器适配器,所以我们的成员只用来包装容器即可,所以成员就是一种容器:
3.2优先级队列的构造函数
我们知道。优先级队列的构造函数有两种,一种是无参数的默认构造,一种是包含容器迭代器的的构造:
对于前者,我们学习了c++的类和对象知识后,如果成员是自定义类型,那么他的默认构造就会调用自定义类型的默认构造,因此非常简单:
而对于后面带参数的默认构造,就是让我们在构造是就完成堆的搭建,对于堆的搭建,我们学过,可以从最后一个非叶子节点开始,向下调整,来搭建,这样直接节省了一半的数据处理:
向下调整算法,我也放在这里:
3.3优先级队列的push接口
对于优先级队列的push,因为我们是容器的适配器,因此我们可以直接用包装容器的push_back接口来实现,但是对于已经插入的数据,我们必须完成这样堆得搭建,这样才算是完整的push过程,而对于这种,由0个元素开始插入的过程,我们可以使用向上调整算法,来进行堆得搭建,这样效率最高:
向上调整算法:
3.4优先级队列的pop以及top接口
学过堆得我们知道,对最大的特性就是,我们可以保证堆的堆顶元素一定是整个堆中最大或最小的一个元素,因此,返回堆顶元素非常重要:
对于优先级队列的pop接口,我们在堆的pop中知道,堆得pop我们可以采用将堆顶元素,与堆最后一个元素进行交换,在删除堆尾的操作来进行堆的pop这里也是,删除后要重新建一个新的堆:
4.仿函数
这里我们就要介绍以下一个新的概念,那就是c++中的仿函数,仿函数我们光看他的名字就知道,他是模仿的函数的功能,那么他本质是什么呢?他的本质就是一个只有成员函数的类,而且这个成员函数也是operator()也就是重载的()。
4.1仿函数在优先级队列中的应用
优先级队列是堆,那么我们就要搭建堆,搭建堆,我们就要考虑搭建大堆还是小堆,对于两种选择,我们学过怎么选择,,两种堆的搭建只有比较大小的不同,其他的步骤都相同,那我们就会想,我们能不能也实现一种泛型思想的编程,就是只改变传递的参数就改变搭建的方式呢,这运用到了仿函数。
以排大堆为例,我们和库保持一样,实现一个less类,(为什么拍大堆要实现less类,因为这体现了一种优先级的思想,即数字大的他的优先级反而小,就像水泡一样浮到了水面)
有了这么一个类,我们再看我们写的向上调整和向下调整的算法:
就知道我们为什么要用一个Less com了,
仿函数的体现,一样我们可以在模板中表示,他同时和Container一样也支持,缺省参数玩法,所以最终我们就可以确定最后完整的模板了
5.deque(扩展)
我们再将容器适配器时提到了库中stack和queue的模板参数:
这怎么和我们讲的不一样呢?stack的默认容器不宜该市vector吗,queue的默认容器不应该是list吗,为什么这里用的是deque?
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。
所以说deque是综合了list和vecctor的优点,既可以下标来表示元素,插入删除的效率又高,这么厉害的一种数据结构我们为什么不学呢?
答案就是他的缺点也非常多。
deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组,其底层结构如下图所示:
所以说,虽然说他的插入删除效率高,那也仅仅是对于两端的插入删除,中国是中间插入删除,那就可能导致后面所有的迭代器失效;虽然数可以标遍历,但是由于不是连续的空间,所以下标的计算就很复杂,因此我们很少使用deque.
但是,deque这样的特性不久恰好适合stack、
和queue的这种只会在两端操作数据的数据结构吗,因此,库里面我们用deque来当stack和queue的默认容器。
6.总结
通过这篇文章我们要掌握,容器适配器,仿函数的玩法,并学会融会贯通。