在学c++的过程中,我们必不可少的一大工具就是STL,并且要一定程度的了解STL背后的原理。
今天我们来探讨一下STL中stack和queue的容器适配器,以及priority_queue是什么,以及一点仿函数的知识。
在我们了解STL中栈和队列或者模拟实现相似的栈和队列时,在翻阅STL相关文件时,不免会碰到如下东西:
左边的class T我们认识那是模板参数,实例化对象的时候会用到,但是右边的那一坨是什么东西呢?我们在平常使用中从来没有用到过。
但是看起来他也应该是一个模板参数,那就说明,在写stack的类模板的时候也会用到他,至于右边的deque是什么,我们待会再说。
container的意思是容器。
容器是我们STL中六大部件中的一个,我们所了解的list,vector都是容器,容器顾名思义就是存储数据的东西。只不过每个容器存储数据的方式有所不同,而对于stack来说,他的容器可以是vector,或者list,因为这两个容器的特性都可以满足stack,所以在这里我们假如自己要写一个stack,我们容器的选择可以是这两者中的一个。那我们就可以这么写。
template <class T>
class stack {
typedef vector<T> Container;
public:
//.....
private:
Container _con;
};
他的理念就是使用vector来作为我们stack的存储数据的一种容器以便于我们实现stack的各种功能及特性。那我们也可以使用list作为容器。
但是如果这么写的话显然需要我们再写一个list作为容器的stack,浪费时间,所以就有了容器适配器,他的写法如下:
template <class T, class Container = deque<T>>
class stack {
public:
//...
private:
Container _con;
};
那当我们在实例化的时候就可以实例化自己想要的存储容器了。
stack<int, vector<int>> st1;
stack<int, list<int>> st2;
那我们想要模拟实现一个stack可就便捷多了。
代码如下:
namespace xxx {
template <class T, class Container = deque<T>>
class stack {
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_back();
}
T& top()
{
return _con.back();
}
const T& top()const
{
return _con.back();
}
size_t size()const
{
return _con.size();
}
bool empty()const
{
return _con.empty();
}
private:
Container _con;
};
}
这个类模板的容器似于缺省值,不填写的话默认使用这里的容器。
deque也是STL中的一个容器,他叫双端队列。
可以看到他支持vector的随机访问,也有list的头插头删。
他的原理有一点复杂,他先是有一个数组,这个数组的元素的类型是指针。这个指针指向的内容是一个大小为十(不等)的数组。deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的。
大致如下图:
他会使用这样的数组来维护许多小但连续的数组,并且这个指针数组的元素,他是从中间开始向两边扩散的,假如我们头插deque,最前面的数组指针所指向的数组满了,那我们就需要开辟一个新的小数组,并且在指针数组中插入指向这个小数组的数组指针。并且这个头插的元素是在小数组的最后一个位置。他的尾插即相反。这种方式,就导致deque的迭代器难以实现。
这样的设计会使得假如我们要删除中间的一个元素那就会使得我们移动元素非常麻烦。
但是当我们进行头尾操作时,我们可以做到是O(1)的复杂度,并且随机访问也是趋近于O(1)。这样的特性非常适合stack和queue,所以会让他俩的默认容器为deque。
他的名字叫优先级队列,但是他的底层本质上是堆。他也需要容器适配器。
那知道是堆之后他的模拟实现也很简单。
代码如下:
namespace xxx
{
template <class T>
class Less
{
public:
bool operator()(const T x, const T y)
{
return x < y;
}
};
template <class T, class Container = vector<T>, class Compare = Less<T>>
class priority_queue
{
public:
priority_queue()
{}
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last)
{
while (first < last)
{
push(*first++);
}
}
bool empty() const
{
return c.empty();
}
size_t size() const
{
return c.size();
}
const T& top() const
{
assert(c.size() > 0);
return c.front();
}
void push(const T& x)
{
c.push_back(x);
adjust_up(c.size() - 1);
}
void pop()
{
swap(c[0], c[c.size() - 1]);
c.pop_back();
adjust_down(0);
}
private:
void adjust_up(size_t child)
{
size_t parent = (child - 1) / 2;
while (child > 0)
{
//if (c[parent] < c[child])
if (comp(c[parent], c[child]))
swap(c[parent], c[child]);
else
break;
child = parent;
parent = (child - 1) / 2;
}
}
void adjust_down(size_t parent)
{
size_t child = parent * 2 + 1;
while (child < c.size())
{
if (child + 1 < c.size() && comp(c[child], c[child + 1]))
child++;
if (comp(c[parent], c[child]))
swap(c[parent], c[child]);
else
break;
parent = child;
child = child * 2 + 1;
}
}
Container c;
Compare comp;
};
};
但是我们发现,他多了一个成员Compare类型的,那这又是什么呢?这就是仿函数。
在上面我们可以看到优先级队列中模板参数不仅有类型,适配器,还有一个什么比较。比较我们知道,在堆中大小堆的比较方式是不同的。而我们在上面的实现中有有这么一个东西:
我们其实也用过这么一个相似的东西,那就是C语言里的qsort。
这也是一个compare,但是它是一个函数指针,这里是一个类。其实仿函数的出现一部分原因就是为了取缔C语言中的函数指针,因为这东西太恶心了。
相反,仿函数书写简单、好调用。也使得回调函数相关的书写变得简单。
那关于仿函数的简单使用几乎和我们普通的函数调用一样,不过作为仿函数的类,都必须重载 operator() 运算符。因为调用仿函数,实际上就是通过类对象调用重载后的 operator() 运算符。