stack
关于栈和队列,我们在之前数据结构部分就已经学习了,并且用C实现了它们,在这里只是用C++做一样的工作。目的是复习和理解C++的优越性。所以在这里只对模拟实现着重介绍。对于STL中的栈和队列,用C++实现是比较简单的,因为我们只需要保证我们实现出来的数据结构符合栈和队列的特点即可,至于用什么容器存放数据,STL库中有vector和list,任君选择。
下面以vector为例。
如果有需要的话,强烈建议阅读栈和队列,这里结合图示详细介绍了栈和队列的特性,只需要阅读介绍部分即可。
stack的使用
#include <stack> void test1() { stack<int> st; //入栈 st.push(1); st.push(2); st.push(3); st.push(4); st.push(5); cout << "size:" << st.size() << endl; //先打印后出栈 while(!st.empty()) { cout << st.top() << " "; st.pop(); } cout << endl; } //也可以指定适配器为vector或list //需要包含相应头文件 void test2() { stack<int, vector<int>> st; st.push(1); st.push(2); st.push(3); st.push(4); st.push(5); st.push(6); cout << "size:" << st.size() << endl; while(!st.empty()) { cout << st.top() << " "; st.pop(); } cout << endl; } void STL_stack_test() { test1(); test2(); } int main() { STL_stack_test(); return 0; }
【输出】
size:5
5 4 3 2 1
size:6
6 5 4 3 2 1
stack的实现
通过查阅文档:
STL的每个容器底层都有我们要学习的新东西,这里第二个模板参数Container
就是我们要在栈和队列中学习的。
容器适配器
适配器的含义
「适配器」是一个泛称,例如我们生活中有适用各种设备的电源适配器。那么我们STL中也有容器适配器。
- 对于上层,STL面对的是程序员,电源适配器面对的是用户,把插头插入插孔,每个插孔的电压都是220V交流电;
- 对于底层,STL面对的是底层实现,电源适配器面对的是各种不同的设备,这些设备需要的电压大小、交流电还是直流电都是不同的。
结合文档中模板的声明来看,stack里的适配器Container
其实没什么高大上的,其实就是因为stack本身可以用list或vector等数据结构实现,所以增加了一个参数让使用者自己选择底层是用哪个容器保存stack元素的数据。这里给了Container以缺省值deque<T>
,表明stack底层默认以deque为容器。
- 容器适配器本质上还是容器,只不过此容器模板类的实现,利用了大量其它基础容器模板类中已经写好的成员函数。当然,如果必要的话,容器适配器中也可以自定义新的成员函数。
- deque是STL中的双端队列,是一种具有队列和栈性质的抽象数据类型。双端队列中的元素可以从两端弹出,插入和删除操作限定在队列的两边进行。它定义在
<queue>
头文件中。
不管是使用STL中的vector还是list,或者是默认的deque,stack的模拟实现都非常简单,因为STL各个容器的接口十分类似。而且它们对外都是封装好了的,所以我们实现stack只需要保证使用它们的接口,达到“先进先出”的效果即可。
用双端队列(数组)来看,stack只能从后入栈,从头弹出,那么也就对应这deque中的尾插和头删接口。
【代码】
#pragma once #include <iostream> using namespace std; namespace xy { //定义适配器Container template<class T, class Container = deque<T>>//这里的deque可以设置自己想要的结构,比如vector或list 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(); } bool empty() { return _con.size() == 0; } const size_t size() const { return _con.size(); } void swap(stack<T, Container>& st) { std::swap(st._con); } private: Container _con;//定义成员变量 }; /// __TEST__ void test1() { stack<int> st; st.push(1); st.push(2); st.push(3); st.push(4); st.push(5); cout << "size:" << st.size() << endl; while(!st.empty()) { cout << st.top() << " "; st.pop(); } cout << endl; } }
可以通过命名空间xy内的函数测试一下:
【输出】
size:5
5 4 3 2 1
queue
queue的使用
同样,queue在数据结构部分已经学习,在这里简要介绍STL中的各个接口的使用。
void STL_queue_test() { queue<int, list<int>> q; q.push(1); q.push(2); q.push(3); q.push(4); cout << "size:" << q.size() << endl; //4 while (!q.empty()) { cout << q.front() << " "; q.pop(); } cout << endl; }
【输出】
size:4
1 2 3 4
底层实现也是和stack的实现类似,都是使用已经封装好的接口达到“先进先出”的效果即可。
#pragma once #include <iostream> #include <queue> using namespace std; namespace xy { //定义适配器Container template<class T, class Container = std::deque<T>>//这里的deque可以设置自己想要的结构,比如vector或list class queue{ public: void push(const T& x) { _con.push_back(x); } void pop() { _con.pop_front(); } T& front() { return _con.front(); } const T& front() const { return _con.front(); } T& back() { return _con.back(); } const T& back() const { return _con.back(); } bool empty() { return _con.size() == 0; } const size_t size() const { return _con.size(); } void swap(queue<T, Container>& st) { std::swap(st._con); } private: Container _con;//定义成员变量 }; /// __TEST__ void test1() { queue<int> st; st.push(1); st.push(2); st.push(3); st.push(4); st.push(5); cout << "size:" << st.size() << endl; while(!st.empty()) { cout << st.front() << " "; st.pop(); } cout << endl; } }
测试一下:
【输出】
size:5
1 2 3 4 5
priority_queue
priority_queue的使用
priority_queue,优先级队列,通过查阅文档可以知道,它其实就是一个存放大小堆的容器,由于数组比链表更有优势,所以它默认是以vector为容器。
严格来说,priority_queue是容器适配器,因为它底层可以指定不同类型的容器存放数据,不过对于我们的「使用」而言,它就是个容器,我们依然用push、pop等操作,只是在「实现」时我们多了一个指定底层容器的步骤。
从文档的描述来看,优先级队列默认是大堆。由于之前已经学习过二叉树的大小堆算法,所以在这里模拟实现优先级队列一是把之前知识[复习一遍](3. 二叉树的顺序存储)(主要是向上调整和向下调整算法),二是学习优先级队列中新的东西:「仿函数」。
仿函数(functor)
从第二个模板参数可以看到,优先级队列多了一个Compare
的参数,它的内容是还与适配器有关。
其实没那么高大上,就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了。
举个例子:
下面单独封装了一个类Add,类中重载了operator(),在这个重载函数中,实现了加法功能
#include <iostream> using namespace std; class Add { public: int operator()(int& x, int& y) { return x + y; } }; int main() { int a = 1, b = 2; Add add; cout << add(a, b) << endl; return 0; }
【输出】
3
如果但看main函数中的打印函数部分,不管是形式还是传参,都和函数无异,但是它的底层就是一个被重载的operator()。
仿函数就是让operator()有了函数的特性,在C中我们用函数指针和回调函数实现仿函数。
那么仿函数在这里有什么作用呢?
在向上调整和向下调整算法中,需要比较孩子和父亲结点的值的大小,然后交换,调整,这样才能得到大堆或小堆(堆顶结点的值一定是极值)。
那么既然要比较,就直接让父亲结点和孩子节点在调整函数内部比较不就好了,为什么还要另外写两个类(less和greater)来封装这两个具有比较功能的重载函数呢?
要知道,模板参数中的Compare是有缺省值的,它默认是less,也就是说它可以改变比较方式,就像一个开关一样。用户切换大小堆只要传入一个参数即可,而不是翻到源代码,将里面的<
改成>
。
【代码】
#pragma once #include <iostream> #include <vector> #include <queue> using namespace std; namespace xy { //定义比较方式以确定结构为大堆或小堆 template<class T> class less { bool operator()(const T& x, const T& y) { return x < y; } }; template<class T> class greater { 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: void push(const T& x) { _con.push_back(x); adjust_up(_con.size() - 1); //因为是用vector,所以头插也可以,但是这样就要向下调整,时间复杂度更高 } //弹出优先级队列(大小堆)的堆顶元素 void pop() { //将堆顶元素和堆尾元素互换 swap(_con[0], _con[_con.size() - 1]); //弹出 _con.pop_back(); //从现有堆的第一个元素向下调整(此时没有"堆顶"可言) adjust_down(_con.size(), 0); } T& top() { return _con[0]; } const T& top() const { return _con[0]; } const size_t size() const { return _con.size(); } bool empty() { return _con.empty(); } //向上调整 void adjust_up(int child) { int parent = (child - 1) / 2; while(child > 0)//即使child=0,parent也是0,所以还要比较一次 { if(_cmp(_con(parent), _con(child))) { //父子结点交换 swap(_con(parent), _con(child)); //更新父子结点,比较范围向上移动 //先修改下面的,即child child = parent; parent = (child - 1) / 2; } else break; } } //向下调整 void adjust_down(int n, int parent) { int child = 2 * parent + 1; while (child < n) { if (child + 1 < n&&_comp(_con[child], _con[child + 1])) { child++; } if (_cmp(_con[parent], _con[child])) { //父子结点交换 swap(_con[child], _con[parent]); //继续向下 parent = child; child = 2 * parent + 1; } else break; } } private: Container _con;//定义底层实现的容器 Compare _cmp;//定义比较方式 }; }
模拟实现优先级队列的核心部分是实现向上调整和向下调整算法的代码,这也是面试中常见的问题。
【测试】
void my_priority_queue_test() { xy:priority_queue<int> pq; pq.push(1); pq.push(5); pq.push(9); pq.push(7); pq.push(10); pq.push(3); pq.push(0); while (!pq.empty()) { cout << pq.top() << " "; pq.pop(); } cout << endl; }
【输出】
10 9 7 5 3 1 0