一. deque简单介绍
1.1 deque的功能介绍
deque(双端队列):是一种双开口的连续空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要挪动元素;与list比较,空间利用率比较高。
1.2 deque的本体框架
虽说是连续性存储空间,但这种连续性只是表面上的,实际上它在堆上分配了一块一块的动态储存区,叫做缓冲区(buff),每一块缓冲区本身是连续的,deque自身的机制把这一块一块的缓冲区虚拟地连在一起,即把它们的首元素地址存在一个指针数组map里,注意这里的map不是STL里的map容器,而是deque的中控器,是个指针数组。
template<class T>
class deque
{
protected:
iterator start;
iterator finish;
T** map;
size_t map_size;
};
为了满足头尾的插入删除,一开始的buff会放到中间位置,这样如果有需要的话,往前往后都可以存buff。当map指向的这块空间不够存放内存指针的时候,就会另觅一块更大的连续性空间,然后把指针一个一个复制过去,并销毁旧的空间。利用这种数据结构,deque就能方便地模拟自身的存储区是连续性空间的假象,并且可以实现双向插入删除的功能。
1.3 deque的迭代器框架
迭代器就是模拟指针的操作,比如解引用得到具体的数据,箭头访问内部成员变量,自增、自减等等操作。那么deque的迭代器应该具备怎么结构?我们可以考虑以下几点。首先,需要知道该元素位于哪个buff的哪个位置;其次,一旦迭代器前进和后退有可能会跳跃至上一个或下一个缓冲区,为了判断跳跃的条件就需要知道,当前元素所在buff的首尾指针。最后,如果前进或后退必须跳跃至下一个或上一个缓冲区。为了能够正确跳跃,deque必须随时掌握管控中心(map),通过map可以知道跳跃的缓冲区。 所以在迭代器中需要定义如下参数:
template<class T, class Ref, class Ptr, size_t N>
struct _deque_iterator
{
T* cur;
T* first;
T* last;
T** node;
}
迭代器和中控器之间的关系如下图所示
1.2 deque的优缺点
优点
- 与vector相比:头部和中间的插入删除不需要大量挪动数据,时间复杂的为O(1);扩容时也不需要大量挪动数据。
- 与list相比:空间利用率较高,内存碎片少,不是需要一个才开辟一个。
缺点 不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下。
总结 因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。
二. 容器适配器
2.1 什么是适配器
适配器是一种设计模式,该种模式是将一个类进行封装,利用原来类的接口转换成客户希望的另外一个接口。
2.2 STL标准库中的stack和queue底层结构
虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和队列只是对其他容器的接口进行了包装,其中STL中stack和queue默认容器使用deque。
2.3 stack和queue的模拟实现
2.3.1 stack容器剖析
两者的实现原理一样,下面我们主要分析stack的实现
stack要做到存储不同类型数据,那就应该是个类模板,模板参数是要存储的数据的类型和底层容器的种类。
注意:容器也是可以作为模板参数的,因为模板作用就是处理不同类型但逻辑相同的问题,我们对传入的不同类型统一管理,像vector,list等容器算自定义类型,我们也可以作为模板参数传入。
stack的框架 成员变量只有一个,就是底层的容器的一个实例化对象
template<class T, class Container = deque<T>>
class stack
{
public:
stack()
:_con()
{}
private:
Container _con;
};
入栈 只能栈顶入数据,就是deque的尾插,对应调用底层容器deque的push_back()
PS:直接调用就行,不用考虑空间不够增容的问题,这些归底层容器deque管
void push(const T& data)
{
_con.push_back(data);
}
出栈 从栈顶出,就是deque的尾删,封装deque的back_pop()
void pop()
{
_con.pop_back();
}
获取栈顶元素 就是获取deque的最后一个元素,这里要注意不同stack对象调用对应的返回值不同,所以有下面两种写法:const的栈对象返回const的值,非const的栈对象返回非const的值。
T& top()
{
return _con.back();
}
const T& top()const
{
return _con.back();
}
stack的完整模拟实现
template<class T,class Container=deque<T>>
class stack
{
public:
stack()
:_con()
{}
void push(const T& data)
{
_con.push_back(data);
}
void pop()
{
_con.pop_back();
}
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
T& top()
{
return _con.back();
}
const T& top()const
{
return _con.back();
}
private:
Container _con;
};
2.3.2 queue的完整模拟实现
与stack大同小异,由于两者的特性不同(stack是后进先出,queue是先进先出),对应容器调用的接口也就不一样。
template<class T,class Container=deque<T>>
class queue
{
public:
queue()
:_con()
{}
void push(const T& data)
{
_con.push_back(data);
}
void pop()
{
_con.pop_front();
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
T& front()
{
return _con.front();
}
const T& front()const
{
return _con.front();
}
T& back()
{
return _con.back();
}
const T& back()const
{
return _con.back();
}
private:
Container _con;
};
三. priority_queue
3.1 priority_queue介绍
- 优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆。所有需要用到堆的场景,都可以考虑使用priority_queue(注意默认情况下priority_queue是大堆)
- 它的接口和栈的接口类似
push():数据进堆 pop():删除堆顶数据 top():拿到堆顶数据 empty():堆是否为空?为空的话返回true,不空返回false size():当前堆元素的数量
3.2 priority_queue的模拟实现
PS:容器vector的数据位置要用到堆结构处理,堆的知识和相关堆算法,这些堆的内容可以参考下面文章:二叉树和堆
仿函数: 定义一个类专门重载operator(),使该类实例化出来的对象可以像调用函数一样的调用,从而实现特定功能,你如比较两个数据的大小。 priority的基本框架
template<class T>
struct less
{
bool operator()(const T& x1, const T& x2)
{
return x1 < x2;
}
};
template<class T>
struct greater
{
bool operator()(const T& x1, const T& x2)
{
return x1 > x2;
}
};
template<class T, class Container = vector<T>, class Compare = less<T>>
class priority_queue
{
public:
priority_queue()
:_con()
{}
private:
Container _con;
};
堆的向上和向下调整算法 我们尾部插入元素要利用向上调整算法调堆,删除堆顶元素要用向下调整算法调堆。只是在插入和删除操作时调用,一般外部不用直接调用(因为没意义),所以我们把这两个算法设置成私有。
但不能把他们设置为内联,因为他们内部有循环操作,且代码较长。
private:
void AdjustDown(int parent)
{
int n = _con.size();
int child = parent * 2 + 1;
while(child < n)
{
if (child + 1 < n&&Compare()(_con[child], _con[child + 1]))
{
++child;
}
if (Compare()(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
return;
}
}
}
void AdjustUp(int child)
{
int n = _con.size();
while (child > 0)
{
int parent = (child - 1) / 2;
if (Compare()(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
child = parent;
}
else
{
return;
}
}
}
Container _con;
用迭代器区间构造一个priority_queue对象 像下图一样构造一个优先级队列的对象
template<class Iterator>
priority_queue(Iterator first, Iterator last)
: _con(first, last)
{
int n = _con.size();
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(i);
}
}
入队列 就是vector的尾插,再把插入的元素向上调整,保持堆的结构
void push(const T& data)
{
_con.push_back(data);
AdjustUp(_con.size()-1);
}
出队列 删除堆顶元素。交换一下堆顶和堆底元素,不用重新建堆,vector尾删,最后在给堆顶元素进行向下调整算法。
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
获取堆顶元素 堆顶元素不允许修改,因为:堆顶元素修改可以会破坏堆的特性。
const T& top()const
{
return _con.front();
}
完整代码
#include<iostream>
#include<vector>
using namespace std;
namespace MyPriorityQueue
{
template<class T>
struct less
{
bool operator()(const T& x1, const T& x2)
{
return x1 < x2;
}
};
template<class T>
struct greater
{
bool operator()(const T& x1, const T& x2)
{
return x1 > x2;
}
};
template<class T, class Container = vector<T>, class Compare = less<T>>
class priority_queue
{
public:
priority_queue()
:_con()
{}
template<class Iterator>
priority_queue(Iterator first, Iterator last)
: _con(first, last)
{
int n = _con.size();
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(i);
}
}
void push(const T& data)
{
_con.push_back(data);
AdjustUp(_con.size()-1);
}
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
int size()const
{
return _con.size();
}
bool empty()const
{
return _con.empty();
}
const T& top()const
{
return _con.front();
}
private:
void AdjustDown(int parent)
{
int n = _con.size();
int child = parent * 2 + 1;
while(child < n)
{
if (child + 1 < n&&Compare()(_con[child], _con[child + 1]))
{
++child;
}
if (Compare()(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
return;
}
}
}
void AdjustUp(int child)
{
int n = _con.size();
while (child > 0)
{
int parent = (child - 1) / 2;
if (Compare()(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
child = parent;
}
else
{
return;
}
}
}
private:
Container _con;
};
void test()
{
vector<int> v = { 2,1,3,3,2,0,6 };
priority_queue<int> p(v.begin(), v.end());
}
}
|