目录
一.stack
1.stack的使用
2.适配器
3.stack相关的题目
最小栈. - 力扣(LeetCode)
编辑
栈的弹出压入序列栈的压入、弹出序列_牛客题霸_牛客网
用两个栈实现队列. - 力扣(LeetCode)
4.stack的模拟实现
二.queue队列
1.底层容器deque
2.queue的使用
练习题:用栈实现队列. - 力扣(LeetCode)
3.queue的模拟实现
三.priority_queue优先队列
1.priority_queue的使用
2.priority_queue模拟实现
一.stack
1.stack的使用
stack就是数据结构里常见的栈,具有先进后出的特点,stl把它封装成了容器了,拿来就可以使用不用再从轮子开始造起了。
stack增删与之前类似就不多加赘述了
值得注意的是stack没有迭代器,所以无法通过迭代器进行访问。也没有vector和string容器里的方括号下标访问。栈每次只能访问栈顶元素,访问完了后再pop删除然后才能访问下一个元素。
2.适配器
从构造函数来看没有fill填充构造也没有范围构造,出现次数最多的构造是Alloc,这个是什么呢
构造方面不支持:initializer_list构造,fill填充构造,范围构造也不支持
Alloc是适配器模式,适配器和迭代器一样也是一种C++设计模式。
适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总 结),该种模式是将一个类的接口转换成客户希望的另外一个接口。比如说插座不同的国家插座插头形状不同,所以需要适配器这个中间商转换一下,使插头能够使用别的国际的插头。放到stl,stack是用别的容器封装过的,利用别的容器比如vector的push_back来实现自己的push插入。所以很多时候stack都不太算是容器,因为它不能自己存储东西都要利用别的容器来装东西
正常stack的构造函数写成
或者采用list作为底层容器
但是呢这里就有一个问题, stack<int,vector<int>> s;尖括号里填的模版类型,而stack构造基本都是传的类型,所以stack就没法在构造的时候就加入数据,也无法先构造一个vector容器,然后利用这个容器把数据加入到stack当中,只能慢慢push进数据
因为要满足stack的先进先出的规则,所以底层的选择上也有条件,容器内部必须要有以下几个函数
empty:判空操作
back:获取尾部元素操作
push_back:尾部插入元素操作
pop_back:尾部删除元素操作
标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器, 默认情况下使用deque。 deque也是一种容器,是list和vector折中化的容器,往后再详细介绍。
3.stack相关的题目
最小栈. - 力扣(LeetCode)
这题的解题思路就是拿两个栈相互配合,tamp2栈专门存最小值,插入数据的时候先给tamp1栈插入,如果tamp2也为空那么也给tamp2直接插入数据。如果tamp2不为空,那么就拿tamp1当中插入的数据(此时是tamp1栈顶数据)和tamp2栈顶相比较,如果tamp1比较小或者相等的话tamp2当中就也插入val值(更新栈顶最小值,tamp2必须保持栈顶的值是最小的)
删除数据的时候要注意,如果tamp1当中删除的数据正好是tamp2当中的栈顶值,那么tamp2的栈顶也得删除。其余的时候是不需要删除的,因为tamp2是tamp1当中所有现存元素的最小值,所以如果tamp1删除了与tamp2堆顶相等的数据,就相当于tamp1当中的最小值要更新了,栈顶元素是无效数据了
举个例子
插入数据1,4,0,5,2,8
删除数据,同时更新最小值
当两边栈顶元素都相等时,说明此时栈里面的最小值需要更新了,所以两边都需要删除0
删除4与之前一样,直接在tamp1当中删除
最后栈顶元素相等都是1,所以两边栈都得删除
完整代码如下
#include<iostream>
using namespace std;
#include<stack>
class MinStack {
public:
MinStack() {
}
void push(int val) {
tamp1.push(val);
if (tamp2.empty())
tamp2.push(val);
else
{
if (tamp1.top() <= tamp2.top())
tamp2.push(val);
}
}
void pop() {
if (tamp1.top() == tamp2.top())
{
tamp2.pop();
}
tamp1.pop();
}
int top() {
return tamp1.top();
}
int getMin() {
return tamp2.top();
}
stack<int> tamp1;
stack<int> tamp2;
};
栈的弹出压入序列栈的压入、弹出序列_牛客题霸_牛客网
这题大致思路就是模拟栈的进栈出栈顺序,如果进栈数据数量与出栈顺序不相等,说明肯定不适合,直接返回false就行了。进栈直接进就行,如果此时栈顶与出栈的数据相同那么就出栈。最后如果栈为空那么就说明按照这个出栈顺序已经全出栈了,说明这个出栈顺序是有效的
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* @param pushV int整型vector
* @param popV int整型vector
* @return bool布尔型
*/
bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {
if(pushV.size()!=popV.size())
return false;
stack<int> tamp;
int k = 0;
for (int i = 0; i < pushV.size(); i++) {
tamp.push(pushV[i]);
// 当栈不为空且栈顶元素和当前弹出元素相等时,弹出栈顶元素
while (!tamp.empty() && tamp.top() == popV[k]) {
tamp.pop();
k++;
}
}
return tamp.empty();
}
};
用两个栈实现队列. - 力扣(LeetCode)
这题也是用两个栈来处理,利用栈先进后出的特性来模拟先进先出。因为先进后出,所以循环每次取栈顶出来放到另一个栈中,在另一个站中的出栈顺序正好与第一个栈的出栈顺序相反这样就模拟了先进先出的特性。举个例子,1,2,3,4,5进第一个栈,此时出栈再进第二个栈的顺序是5,4,3,2,1,这时候第二栈的栈顶元素是1,再挨个取栈顶元素出栈为1,2,3,4,5.这样就模拟成功了先进先出的特性了。值得注意的是模拟的进队列函数直接进第一个栈就行,出队列和取栈顶元素把第一个栈的元素倒着插入第二个栈时要考虑第二栈是否为空,只有为空的时候才能挪动第一个栈的数据。这是因为如果第二个栈的数据不为空,此时中途又插入了数据,然后再执行出队列如果是无条件地把第一个栈的数据转移到第二个栈中就会破坏先进先出的特性。举个例子,第一个栈先进1,2,3,然后转移到第二个栈中(第二个栈此时为3,2,1,第一个栈为空了)。此时第一个栈又进了一个数据5,然后执行出队列,如果是无条件把第一个栈的数据转移到第二个栈中,此时第二个栈中的数据就变成了3,2,1,5。5是最后进的反而成了队顶,就破坏了先进先出的特性
完整代码如下
class MyQueue {
public:
MyQueue() {
}
void push(int x) {
tamp1.push(x);
}
int pop() {
if (tamp2.empty())
{
while (!tamp1.empty())
{
tamp2.push(tamp1.top());
tamp1.pop();
}
}
int ret = tamp2.top();
tamp2.pop();
return ret;
}
int peek() {
if (tamp2.empty())
{
while (!tamp1.empty())
{
tamp2.push(tamp1.top());
tamp1.pop();
}
}
return tamp2.top();
}
bool empty() {
return tamp1.empty() && tamp2.empty();
}
stack<int> tamp1;
stack<int> tamp2;
};
4.stack的模拟实现
首先需要明确的是为什么他叫容器适配器呢,虽然前面已经概述过了,但是可能不太直观。stack之所以叫这个是因为他不能直接装数据,都是通过别的容器来辅助存数据的。比如如果以vector为容器的话,那么它的push其实就是通过vector的push_back来实现的,pop删除也是类似的方法。几乎所有的函数都得依赖别的容器来实现。
所以有些时候你可以看到stack定义会写成这样stack<int,vector<int>>,第二个参数vector模版类型其实就是指明stack的底层容器是vector,你也可以写成list
以vector来举例实现stack的话,因为要在stack类里面封装插入删除,所以直接定义一个vector对象来作为stack的成员(大部分适配器几乎没有什么别的成员,指向底层构成就可以了)
push和pop的实现其实就是直接用尾插和尾删就行了,因为栈只在栈顶出,实际上就是队尾出
empty判空,因为所有数据都存在vector当中,所以直接调用vector的判空函数就行
top函数,栈顶函数就是最后一个元素,vector中有函数back可以直接取到最后一个元素
返回size,也是和上面类似
但是上面只是用vector举个例子,一般来讲只要能用判空,尾插尾删,取队尾元素的容器都可以作为stack的底层容器。所以要把底层容器类型也写成模版类型,template<class T,class alloc=vector<T>>(库里面一般默认的是deque)
完整代码如下
namespace mystack
{
template<class T,class alloc=vector<T>>
class stack
{
public:
void push(const T& val)
{
tamp.push_back(val);
}
void pop()
{
tamp.pop_back();
}
const T& top() const
{
return tamp.back();//返回队尾元素
}
T& top()
{
return tamp.back();
}
bool empty()const
{
return tamp.empty();
}
size_t size() const
{
return tamp.size();
}
private:
alloc tamp;
};
}
测试
二.queue队列
1.底层容器deque
queue与stack一样都是容器适配器,但是栈是先进先出,它是先进先出,也就是从尾部插入从头部出。vector是没有直接头删函数,可以用insert间接做到,但是效率太低了。虽然list可以使用头删,但是是以单个元素开辟空间存储,所以在大量插入删除数据时效率低下。因此大佬们设计了vector和list折中的容器deque,这个容器其实是双向队列,支持在头尾插入删除,融合了vector随机访问连续存储的优点(所以它也可以使用下标[ ]的访问方式),同时包含list的两端插入删除方便的优点
从下图可以看出了deque融合了vector和list的功能
具体底层实现其实是按块进行处理,比如说我有50个数据,那么就直接开50个内存块分别存10个数据,同时记录开一个中控数组(实际上就是指针数组)记录这十块空间的地址。为了方便头插所以中控数组会空出一些空间(如果是头插,就得再原先的头空间块的基础上再开辟一块空间存新的数据,这个新的空间就是新的头空间块了,中控数组多出来的部分就是存这个新内存块地址的) ,有点类似vector<vector<int>>二维数组
那么怎么实现vector里的随机访问呢,比如我要找下标为43的数据,首先我这里设定的一个内存块存10个数据(库里实现可能更复杂一点),所以直接int j=43/10,j就是具体存哪个数据块,然后int i=43%10,i是具体在内存块里的数据位置,这样就找到了下标为43的数据是在第四个数据块里的第三个位置。
库里的实现要复杂一点(见下图),cur是当前节点位置,first是内存块的开始位置,last是末尾数据的下一位,当遍历访问的时候cur从first开始挨个往后挪动,当cur等于last时说明这个内存块访问完了,中控那里的node节点位置++,找到下一个内存块的地址,然后cur重新赋值为新内存块first地址,从头开始往后找
那么插入时扩容是怎么扩容的呢(参考下图),又定义了两个迭代器,start指向头内存块,finish指向尾内存块,当cur等于finish内存块的last位置时,说明已经存满需要扩容。这时会开辟一个新内存块,使finish的所有成员指向新内存块中新的位置。如果中控数组满了也需要扩容,头插扩容了内存块的地址在中控数组中满了没法存了,也会发生扩容
结合上面可以很明显发现deque的缺点,首先是实现复杂,其次头尾插入倒是方便了,但是如果在中间插入,要么后面所有的数据都往后挪动空一个位置出来然后完成新数据插入(数组不方便插入删除的缺点)。要么就是在当前内存块直接扩容,但是这样就直接破坏了随机存取规则,因为这种模拟的随机存取有一个前提就是所有内存块容量长度都相等,如果有一个内存块长度不相等就破坏随机存取的规则了(正好是list的缺点)
所以deque效率其实不是很高,虽然吸收了vector和list的优点,但是缺点没有完全修复。因此一般都不怎么会用deque,要随机存取查找方便就用vector,要插入删除方便就用list。虽然作为容器效率不高过于复杂,但是作为容器适配器的底层容器还是挺方便的,因为容器适配器压根就不修改中间元素只会在末尾和头部插入删除,所以stack栈,queue队列,priority_queue优先队列基本默认底层容器是deque
2.queue的使用
queue的使用与stack类似,只有简单的增删判空,只不过队列是从队尾插入,从队头出,先进先出所以出队的顺序与进队顺序一致。stack是每次都通过top栈顶来访问不同元素,而queue队列提供了访问队尾和队头元素值的两个函数,因为每次改变的是队头元素,所以每次访问队头元素就可以访问到所有元素了
举个例子
练习题:用栈实现队列. - 力扣(LeetCode)
大致思路是用两个队列来模拟栈的先进后出,也就是说进1,2,3,4,第一次pop的时候要想办法取到4,但是这个4在队尾,那应该怎么取到呢。实际上就是用第二个队来进行辅助,第一个队的数据挪动size-1个数据到第二个队列当中,此时第一个队数据就剩一个数据4了,让第一个栈pop掉4(这样就实现了pop掉第一个队的尾部数据)。处理完了尾部数据后再把第二个队的数据全部挪动回来进行下一轮操作。对于模拟取栈顶元素,与pop的操作类似,只是挪动完了第一队的数据后不需要pop掉最后一个元素,取到它的值后也依旧挪动到第二队当中(取队顶和pop唯一差别就是要不要pop掉第一个队列的最后一个元素)
完整代码如下
class MyStack {
public:
MyStack() {}
void push(int x) {
tamp1.push(x);
}
int pop() {
// 将 tamp1 中的元素转移到 tamp2,直到只剩下一个元素
while (tamp1.size() > 1) {
tamp2.push(tamp1.front());
tamp1.pop();
}
// 取出最后一个元素(即栈顶元素)
int ret = tamp1.front();
tamp1.pop();
// 将 tamp2 中的元素转回 tamp1
while (!tamp2.empty()) {
tamp1.push(tamp2.front());
tamp2.pop();
}
return ret; // 返回栈顶元素
}
int top() {
// 与 pop 相同,只是不移除元素
while (tamp1.size() > 1) {
tamp2.push(tamp1.front());
tamp1.pop();
}
int ret = tamp1.front(); // 获取栈顶元素
// 将最后一个元素再压回 tamp1
tamp2.push(tamp1.front());
tamp1.pop();
// 将 tamp2 中的元素转回 tamp1
while (!tamp2.empty()) {
tamp1.push(tamp2.front());
tamp2.pop();
}
return ret; // 返回栈顶元素
}
bool empty() {
return tamp1.empty();
}
private:
std::queue<int> tamp1; // 主队列
std::queue<int> tamp2; // 辅助队列
};
3.queue的模拟实现
queue的实现与stack类似,只是pop时是从头开始出的
完整代码如下
namespace myqueue
{
template<class T,class alloc=deque<T>>
class queue
{
public:
void push(const T& val)
{
tamp.push_back(val);
}
void pop()
{
tamp.pop_front();
}
T& front()
{
return tamp.front();
}
const T& front()const
{
return tamp.front();
}
const T& back()const
{
return tamp.back();
}
bool empty()const
{
return tamp.empty();
}
size_t size() const
{
return tamp.size();
}
private:
alloc tamp;
};
}
测试
三.priority_queue优先队列
1.priority_queue的使用
priority_queue虽然名字叫优先队列但是它底层其实是堆,而且默认是大堆,它的成员函数也和stack以及queue类似,都是这几个接口
值得注意的是优先队列没有单独的头文件,它是一起定义在queue头文件里的,但是它的遍历规则并不是和队列一样先进先出。默认情况下是遵循大堆的规则,每次堆顶都是最大值,而是遍历打印出来是降序
那么怎么打印出升序,也就是底层处理成小堆,在前期的学习中可以知道建堆都是通过向下调整来处理的(向上调整nlogn,向下调整logn),而向下调整只需要改变大于和小于符号就可以完成调整成大堆还是小堆的转换了。在库里实际上是通过仿函数来进行调整比较符号的,仿函数不是函数而是一种对象,它可以像函数一样被调用。
下面的例子中small<int>其实是个匿名对象
针对大堆小堆库里其实直接给了less<int>(虽然翻译是小,但是其实是大堆,库里面默认缺省是填的这个),greater<int>(这才是小堆)两个仿函数,可以直接拿来用
自定义仿函数可以用来排序复杂的自定义类型,比如日期类,你光按年份比较如果年份相等就无法比较了,所以要定义一个仿函数在年份相等的情况下按月份去排序,在月份相等的情况下按天数去比较。
#include<iostream>
using namespace std;
#include<queue>
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date & d)const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
bool operator>(const Date & d)const
{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
friend ostream & operator<<(ostream & _cout, const Date & d)//打印
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
void TestPriorityQueue()
{
// 大堆,需要用户在自定义类型中提供<的重载
priority_queue<Date> q1;
q1.push(Date(2018, 10, 29));
q1.push(Date(2018, 10, 28));
q1.push(Date(2018, 10, 30));
cout << q1.top() << endl;
// 如果要创建小堆,需要用户提供>的重载
priority_queue<Date, vector<Date>, greater<Date>> q2;
q2.push(Date(2018, 10, 29));
q2.push(Date(2018, 10, 28));
q2.push(Date(2018, 10, 30));
cout << q2.top() << endl;
}
int main()
{
TestPriorityQueue();
}
2.priority_queue模拟实现
不加仿函数版本
#pragma once
#include <iostream>
#include <deque>
using namespace std;
namespace mypriority {
template<class T, class alloc = deque<T>>
class priority_queue {
public:
// 下沉调整
void adjustdown(size_t parent) {
size_t child = 2 * parent + 1; // 假设最左边的孩子就是最大的
while (child < tamp.size()) {
// 如果右边的孩子更大,更新孩子节点下标
if (child + 1 < tamp.size() && tamp[child] < tamp[child + 1]) {
child++;
}
// 父节点比孩子节点小,进行交换
if (tamp[parent] < tamp[child]) {
swap(tamp[parent], tamp[child]);
// 更新父节点下标,继续调整
parent = child;
child = 2 * parent + 1;
}
else {
break; // 如果不需要交换,退出
}
}
}
// 向上调整
void adjustup(size_t child) {
size_t parent = (child - 1) / 2;
while (child > 0) {
// 如果父节点小于子节点,进行交换
if (tamp[parent] < tamp[child]) {
swap(tamp[parent], tamp[child]);
// 更新child和parent
child = parent;
parent = (child - 1) / 2;
}
else {
break; // 如果不需要交换,退出
}
}
}
// 添加元素
void push(const T& val) {
tamp.push_back(val); // 在末尾添加新元素
adjustup(tamp.size() - 1); // 调整新添加的元素位置
}
// 移除最大元素
void pop() {
if (!empty()) { // 确保队列不为空
swap(tamp[0], tamp[tamp.size() - 1]); // 将最大元素移到末尾
tamp.pop_back(); // 移除末尾元素
adjustdown(0); // 调整根元素
}
}
// 获取最大元素
const T& top() const {
if (!empty())
return tamp[0]; // 确保队列不为空
throw out_of_range("Priority queue is empty"); // 抛出异常处理
}
// 检查队列是否为空
bool empty() const {
return tamp.empty();
}
// 获取队列中的元素数量
size_t size() const {
return tamp.size();
}
priority_queue() = default;//强制生成默认构造函数
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last) {
while (first != last) {
tamp.push_back(*first);
++first;
}
for (int i = (tamp.size() - 1 - 1) / 2; i >= 0; --i) {
adjustdown(i);
}
}
private:
alloc tamp; // 用于存储优先队列的元素
};
}
如果直接用符号进行比较的话只能单独处理大堆或者小堆(除非再重新写一个类),解决办法是通过仿函数来进行处理,在仿函数内部直接进行元素的比较
仿函数版本
#pragma once
#include <iostream>
#include <vector> // Include vector to use as default container
#include <deque>
using namespace std;
namespace mypriority {
template<class T>
class mygreater {
public:
bool operator()(const T& x, const T& y) const {
return x >y;
}
};
template<class T>
class myless {
public:
bool operator()(const T& x, const T& y) const {
return x < y;
}
};
template<class T, class alloc = std::deque<T>, class Judge = myless<T>>
class priority_queue {
public:
void adjustdown(size_t parent) {
Judge fun;
size_t child = 2 * parent + 1;
while (child < tamp.size()) {
if (child + 1 < tamp.size() && fun(tamp[child], tamp[child+1])) {
child++;
}
if (fun(tamp[parent], tamp[child])) {
std::swap(tamp[parent], tamp[child]);
parent = child;
child = parent * 2 + 1;
}
else {
break;
}
}
}
void adjustup(size_t child) {
Judge fun;
size_t parent = (child - 1) / 2;
while (child > 0) {
if (fun(tamp[parent], tamp[child])) { // Here swap if child is less than parent
std::swap(tamp[parent], tamp[child]);
child = parent;
parent = (child - 1) / 2;
}
else {
break;
}
}
}
priority_queue() = default;
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last) {
while (first != last) {
tamp.push_back(*first);
++first;
}
for (int i = (tamp.size() - 1 - 1) / 2; i >= 0; --i) {
adjustdown(i);
}
}
void push(const T& val) {
tamp.push_back(val);
adjustup(tamp.size() - 1);
}
void pop() {
std::swap(tamp[0], tamp[tamp.size() - 1]);
tamp.pop_back();
adjustdown(0);
}
const T& top() const {
return tamp[0];
}
bool empty() const {
return tamp.empty();
}
size_t size() const {
return tamp.size();
}
private:
alloc tamp;
};
}
模拟实现取堆顶,判空,取size与之前类似就不细讲了
push插入
void push(const T& val) {
tamp.push_back(val);
adjustup(tamp.size() - 1);
}// 向上调整
void adjustup(size_t child) {
Judge fun;//实例化仿函数对象
size_t parent = (child - 1) / 2;
while (child > 0) {
if (fun(tamp[parent], tamp[child])) { // Here swap if child is less than parent
std::swap(tamp[parent], tamp[child]);
child = parent;
parent = (child - 1) / 2;
}
else {
break;
}
}
}插入一般在末尾直接插入就可以了,但是直接插入不符合堆的规则,所以要向上调整进行处理一下。
size_t parent = (child - 1) / 2;先求出父节点下标,右节点是减2,但是(child-2)/2值是一样的。比如父亲节点是1,两个孩子节点是2和3,结果算出来都是1;
while (child > 0) {
if (fun(tamp[parent], tamp[child])) { // Here swap if child is less than parent
std::swap(tamp[parent], tamp[child]);
child = parent;
parent = (child - 1) / 2;
}这一段循环是具体处理堆元素,用fun控制大小比较,默认情况下如果父节点值比子节点值小就交换值,然后通过child = parent; parent = (child - 1) / 2;一直往下处理
pop删除
void pop() {
std::swap(tamp[0], tamp[tamp.size() - 1]);
tamp.pop_back();
adjustdown(0);
}
void adjustdown(size_t parent) {
Judge fun;
size_t child = 2 * parent + 1;while (child < tamp.size()) {
if (child + 1 < tamp.size() && fun(tamp[child], tamp[child+1])) {
child++;
}
if (fun(tamp[parent], tamp[child])) {
std::swap(tamp[parent], tamp[child]);
parent = child;
child = parent * 2 + 1;
}
else {
break;
}
}
}
删除每次删除都是堆顶元素,也就是下标为0的元素,如果是直接堆顶删除是非常困难的,但是在尾部删除是很简单的,所以直接进行交换到最后进行删除,然后从堆顶出发向下调整至符合堆的规则向下调整函数
向上调整是用孩子节点的值与自己的父节点进行比较,而向下调整一开始就是最上面的父亲节点,所以是和孩子节点中的大的那个进行比较。但是我们并不知道哪个孩子节点是最大节点,所以先假设左孩子节点是大的节点,在循环里面再去比较和右孩子节点哪个大(这个是每一趟都要进行比较的) if (child + 1 < tamp.size() && fun(tamp[child], tamp[child+1])) { child++;}
范围构造函数
与queue和stack不同的是优先队列提供了范围构造函数
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last) {
while (first != last) {
tamp.push_back(*first);
++first;
}
for (int i = (tamp.size() - 1 - 1) / 2; i >= 0; --i) {
adjustdown(i);
}
}范围构造第一步都是先全部将这个范围内的所有数值都插入到容器中,堆里面还得多一步按照堆的规则进行调整,向下建堆时间复杂度是O(n),而向上建堆的O(nlogn)(详情请看排序堆排序那一节),所以我们直接用向下调整建堆。