4

[ C++ ] STL priority_queue(优先级队列)使用及其底层模拟实现,容器适配器,deque(双...

 1 year ago
source link: https://blog.51cto.com/xingyuli/5638387
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1.priority_queue

1.1 priority_queue的介绍

 ​priority_queue 官方文档介绍​

[ C++ ] STL priority_queue(优先级队列)使用及其底层模拟实现,容器适配器,deque(双端队列)原理了解_deque

1. 优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。

2. 此上下文类似于,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。

3. 优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。

4. 底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:

        empty():检测容器是否为空

        size():返回容器中有效元素个数

        front():返回容器中第一个元素的引用

        push_back():在容器尾部插入元素

        pop_back():删除容器尾部元素

5. 标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector

6. 需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数

make_heap、push_heap和pop_heap来自动完成此操作。

1.2 priority_queue 的使用及模拟实现

优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意: 默认情况下priority_queue是大堆

函数声明

接口说明

 ​priority_queue() / priority_queue(first,last)

构造一个空的优先级队列

 ​empty()

检测优先级队列是否为空,是返回true,否则返回 false

 ​top()

返回优先级队列中最大(最小元素),即堆顶元素

 ​push(x)

在优先级队列中插入元素x

 ​pop()

删除优先级队列中最大(最小)元素,即堆顶元素

1、默认情况下,priority_queue是大堆

int main(){
vector<int> v = { 3,2,5,7,1,10,9,8,6,4 };
priority_queue<int> q1;
for (auto& e : v)
q1.push(e);

while (!q1.empty())
{
cout << q1.top() << " ";
q1.pop();
}

cout << endl;

//如果要创建小堆,将第三个模板参数换成greater比较方式
priority_queue<int, vector<int>, greater<int>> q2(v.begin(), v.end());
while (!q2.empty())
{
cout << q2.top() << " ";
q2.pop();
}


return 0;
}
[ C++ ] STL priority_queue(优先级队列)使用及其底层模拟实现,容器适配器,deque(双端队列)原理了解_优先级队列_04

 2.、如果在priority_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();
return 0;
}

 模拟实现:

我们刚才已经明确priority_queue就是堆,并且默认是大堆,并且priority_queue的底层存储数据的容器是vector,因此priority_queue也是非常好实现的

//优先级队列 -- 大堆 < 小堆 >
template<class T, class Container = vector<T>,class Compare = less<T>>
class priority_queue
{
public:
void AdjustUp(int child)
{
Compare comFunc;
int parent = (child - 1) / 2;
while (child > 0)
{
//if (_con[parent] < _con[child])
if (comFunc(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}

void AdjustDown(int{
Compare comFunc;

size_t child = parent * 2 + 1;
while (child < _con.size())
{
if (child + 1 < _con.size() && comFunc(_con[parent], _con[child]))
{
++child;
}
if (comFunc(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void push(const{
_con.push_back(x);
AdjustUp(_con.size() - 1);
}
void pop(){
assert(!_con.empty());
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}


const T& top(){
return _con[0];
}

size_t size(){
return _con.size();
}

bool empty(){
return _con.empty();
}


private:
Container _con;
};

 ​堆中元素的删除详细分析过程​

[ C++ ] STL priority_queue(优先级队列)使用及其底层模拟实现,容器适配器,deque(双端队列)原理了解_deque_08

2.容器适配器

2.1 什么是适配器

适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。

2.2 STL标准库中stack和queue的底层结构

虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和队列只是对其他容器的接口进行了包装,STL中stack和queue默认使用deque,比如:

[ C++ ] STL priority_queue(优先级队列)使用及其底层模拟实现,容器适配器,deque(双端队列)原理了解_容器适配器_10

3.deque 

3.1deque的介绍

vector是单向开口的连续线型空间,deque则是一种双向开口的连续线型空间,所谓双向开口,意思是可以在头尾两端分别做元素的插入和删除操作。vector当然也可以在头尾两端进行操作。但是其头部操作效率很差。

[ C++ ] STL priority_queue(优先级队列)使用及其底层模拟实现,容器适配器,deque(双端队列)原理了解_deque_12

deque和vector的差异:

1:deque允许在常数时间内对头部元素插入和删除操作。

2:deque没有所谓容量(capacity)的概念,因为他是动态的以分段连续空间组合而成的,随时可以增加一段新的空间并链接起来,换句话说,像vector那样“因旧空间不足而重新配置一块更大的空间,然后复制元素,再释放旧空间”,deque是不会让这件事情发生的。因此,deque也没有必要提供所谓的空间保留(reserve)功能。

deque的空间到底是怎么样的?

deque是由一段一段的定量连续空间组成,一旦要在deque的前端或者尾端增加新的空间,便需要配置一段定量连续的空间,串联在整个deque的头端或者尾端。deque的最大任务,便是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取的接口,避开vector中"重新配置,复制,释放"的轮回,这样做虽然方便插入删除,但是其代价是复杂的迭代器架构。

[ C++ ] STL priority_queue(优先级队列)使用及其底层模拟实现,容器适配器,deque(双端队列)原理了解_容器适配器_14

双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上,因此deque的迭代器设计就比较复杂,如下图所示:

[ C++ ] STL priority_queue(优先级队列)使用及其底层模拟实现,容器适配器,deque(双端队列)原理了解_容器适配器_16

那deque是如何借助其迭代器维护其假想连续的结构呢?

[ C++ ] STL priority_queue(优先级队列)使用及其底层模拟实现,容器适配器,deque(双端队列)原理了解_容器适配器_18

3.2 deque的中控器

上图我们看到有一个所谓的map,那么这个map是什么东西呢?

        其实,deque采用一块所谓的map (注意,不是STL的map容器)作为主控,这里所谓的map是一块连续的空间,其中每个元素都是指针,指向另一端(较大的)连续线性空间,称为缓冲区。缓冲区才是deque的储存空间主体。SGI版本的STL库下允许我们指定缓冲区的大小,默认值是0表示将使用512bytes缓冲区。

实际deque类似于一个动态的二维数组

3.3 deque的迭代器

deque是分段连续的空间,维持其逻辑意义上的 ” 整体连续 “ 假象的任务落在了迭代器的 operator++ 和 operator-- 两个运算子身上。

首先我们可以想到,deque的迭代器一定是非常复杂的?那么我们不妨从deque的迭代器的需求入手,deque迭代器必须能够指出分段连续空间(缓冲区)在哪里,其次他必须能够判断自己是否已经处于其所在缓冲区的边缘,如果是,一旦前进或者后退时就必须跳跃至下一个或者上一个缓冲区。为了能够正确的跳跃,deque必须随时掌握管控中心(map)。

[ C++ ] STL priority_queue(优先级队列)使用及其底层模拟实现,容器适配器,deque(双端队列)原理了解_deque_20
[ C++ ] STL priority_queue(优先级队列)使用及其底层模拟实现,容器适配器,deque(双端队列)原理了解_优先级队列_22

3.4 deque的缺陷

与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。

与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。

deque有一个致命缺陷:不适合遍历。

因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构

4.为什么选择deque作为stack和queue的底层默认容器

stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可以作为stack的底层容器,比如vector和list都可以;

queue是先进先出的特殊线性数据结构,只要具有 push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如list。但是STL中对stack和queue默认选择deque作为其底层容器,主要是因为:

        1. stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。

        2. 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。

结合了deque的优点,而完美的避开了其缺陷。

(本篇完)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK