stack,queue,priority_queue的模拟实现及常用接口
目录
1.stack和queue应用
1.1stack的使用
1.2stack的模拟实现
1.3queue的使用
1.4queue的模拟实现
2.容器适配器
2.1什么是适配器
2.2 deque介绍
2.3 list和vector的区别
3.priority_queue
3.1priority_queue的使用
3.2 priority_queue的模拟实现
3.3 仿函数
1.stack和queue应用
stack(栈):先进后出
queue(队列):先进先出
1.1stack的使用
函数说明 | 接口说明 |
stack() | 构造空栈 |
empty | 判断是否为空 |
size | 返回栈中有效数据个数 |
top | 返回栈顶元素 |
push | 将元素val压入栈中 |
pop | 将栈顶元素弹出 |
#include<stack>
#include<iostream>
using namespace std;
int main()
{stack<int> s1;s1.push(1);s1.push(2);s1.push(3);s1.push(4);while (!s1.empty()){cout << s1.top() << " ";s1.pop();}return 0;
}
1.2stack的模拟实现
对于stack的模拟实现,从栈的接口中可以看出,栈实际是一种特殊的vector,因此使用vector完全可以模拟实现stack
#include<iostream>
#include<vector>
using namespace std;
namespace chuxin
{template<class T>class stack{public:stack(){}void push(const T& x){_s.push_back(x);}void pop() { _s.pop_back(); }T& top() { return _s.back(); }const T& top()const { return _s.back(); }size_t size()const { return _s.size(); }bool empty()const { return _s.empty(); }private:std::vector<T> _s;};
}
1.3queue的使用
函数说明 | 接口说明 |
queue() | 构造空的队列 |
empty | 检测队列是否为空,是返回true,否则返回false |
size | 返回队列中有效元素的个数 |
front | 返回队头元素的引用 |
back | 返回队尾元素的引用 |
push | 在队尾将元素val入队列 |
pop | 将队头元素出队列 |
#include<queue>
#include<iostream>
using namespace std;
int main()
{queue<int> s1;s1.push(1);s1.push(2);s1.push(3);s1.push(4);cout << "s1.back()"<<" :";while (!s1.empty()){cout<< s1.back() << " ";s1.pop();}cout << endl;s1.push(1);s1.push(2);s1.push(3);s1.push(4);cout << "s1.front()" << " :";while (!s1.empty()){cout <<s1.front()<<" ";s1.pop();}return 0;
}
1.4queue的模拟实现
因为queue的接口中存在头删和尾插,因此使用vector来封装效率太低,故可以借助list来模拟实
现queue
#include <list>
namespace chuxin
{template<class T>class queue{public:queue() {}void push(const T& x) { _c.push_back(x); }void pop() { _c.pop_front(); }T& back() { return _c.back(); }const T& back()const { return _c.back(); }T& front() { return _c.front(); }const T& front()const { return _c.front(); }size_t size()const { return _c.size(); }bool empty()const { return _c.empty(); }private:std::list<T> _c;};
}
2.容器适配器
2.1什么是适配器
适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设
计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口,虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为容器适配器,这是因为stack和队列只是对其他容器的接口进行了包装,STL中stack和queue默认使用deque,对于stack,queue在上文中,容器的类型已经写死了,stack只能用vector类初始化,queue只能用list初始化,此时就可以利用模版进行改动
stack:
#include<iostream>
#include<deque>
using namespace std;
namespace chuxin
{template<class T, class Container = deque<T>>//适配多种容器class stack{public:stack(){}void push(const T& x){_s.push_back(x);}void pop() { _s.pop_back(); }T& top() { return _s.back(); }const T& top()const { return _s.back(); }size_t size()const { return _s.size(); }bool empty()const { return _s.empty(); }private:Container _s;};
}
queue:
#include <deque>
#include<iostream>
using namespace std;
namespace chuxin
{template<class T, class Container = deque<T>>class queue{public:queue() {}void push(const T& x) { _c.push_back(x); }void pop() { _c.pop_front(); }T& back() { return _c.back(); }const T& back()const { return _c.back(); }T& front() { return _c.front(); }const T& front()const { return _c.front(); }size_t size()const { return _c.size(); }bool empty()const { return _c.empty(); }private:Container _c;};
}
2.2 deque介绍
在上文,发现stack和queue容器的缺省值是deque,为什么用这个容器呢?
优点:
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端 进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高,相当于vector和list的缝合怪
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并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于动态的二维数组,deque的插入,删除则涉及它的迭代器,由于deque的迭代器较复杂本文不做叙述
缺陷:
与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实 际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。
2.3 list和vector的区别
vector与list都是STL中非常重要的序列式容器,由于两个容器的底层结构不同,导致其特性以及
应用场景不同,其主要不同如下:
vector | list | |
底层结构 | 动态顺序表,一段连续空间 | 带头结点的双向循环链表 |
随机 访问 | 支持随机访问,访问某个元素效率O(1) | 不支持随机访问,访问某个元素效率O(N) |
插入 和删 除 | 任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N),插入时有可能需要增容,增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低 | 任意位置插入和删除效率高, 不需要搬移元素,时间复杂度 为O(1) |
空间 利用 率 | 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1) | 底层节点动态开辟,小节点容 易造成内存碎片,空间利用率 低,缓存利用率低 |
迭代 器 | 原生态指针 | 对原生态指针(节点指针)进行 封装 |
迭代 器失 效 | 在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效,删除时,当前迭代器需要重新赋值否则会失效 | 插入元素不会导致迭代器失 效,删除元素时,只会导致当 前迭代器失效,其他迭代器不 受影响 |
使用 场景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入和删除操作,不关心 随机访问 |
3.priority_queue
3.1priority_queue的使用
priority_queue的使用和queue的一样,不同的是他们的底层的一些结构,以及处理数据不同,
优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意:默认情况下priority_queue是大堆。
函数声明 | 接口说明 |
priority_queue() | 构造一个空的优先级队列 |
priority_queue(first, last) | 利用迭代器区间建立 |
empty | 检测优先级队列是否为空,是返回true,否 则返回false |
top | 返回优先级队列中最大(最小元素),即堆顶元 素 |
push | 在优先级队列中插入元素x |
pop | 删除优先级队列中最大(最小)元素,即堆顶元 素 |
#include<queue>
#include<iostream>
#include<list>
using namespace std;
int main()
{priority_queue<int> s1;s1.push(5);s1.push(3);s1.push(7);s1.push(2);s1.push(6);while (!s1.empty()){cout <<s1.top()<<" ";s1.pop();}return 0;
}
3.2 priority_queue的模拟实现
priority_queue的底层就是一个堆,因此它的模拟实现,和堆的类似
#include<vector>
namespace chuxin
{// 默认是大堆template<class T, class Container = vector<T>>class priority_queue{public:void AdjustUp(int child){int parent = (child - 1) / 2;while (child > 0){if (_con[parent] < _con[child]){swap(_con[child], _con[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}}void push(const T& x){_con.push_back(x);AdjustUp(_con.size() - 1);}void AdjustDown(int parent){// 先假设左孩子小size_t child = parent * 2 + 1;while (child < _con.size()) // child >= n说明孩子不存在,调整到叶子了{// 找出小的那个孩子if (child + 1 < _con.size() && _con[child] < _con[child + 1]){++child;}if (_con[parent] < _con[child]){swap(_con[child], _con[parent]);parent = child;child = parent * 2 + 1;}else{break;}}}void pop(){swap(_con[0], _con[_con.size() - 1]);_con.pop_back();AdjustDown(0);}const T& top(){return _con[0];}size_t size() const{return _con.size();}bool empty() const{return _con.empty();}private:Container _con;};
}
这种方法还是有缺陷,发现只能排降序,有没有一种方法既能排升序又能排降序?这时候就需要仿函数了
3.3 仿函数
来看看下面这段冒泡排序
void BubbleSort(int* a, int n)
{for (int j = 0; j < n; j++){// 单趟int flag = 0;for (int i = 1; i < n - j; i++){if (a[i] < a[i - 1]){swap(a[i - 1], a[i]);flag = 1;}}if (flag == 0){break;}}
}
发现它的逻辑是排一个升序,如果要排一个降序怎么办,为了一个比较重载一个大部分相似的函数,显然不怎么好,而且比较的数据可能不同,有时候比较整形,有时候是浮点型,这时候就可以利用模版自己写一个比较函数,而这个函数称为仿函数
// 仿函数:本质是一个类,这个类重载operator(),他的对象可以像函数一样使用
template<class T>
class Less
{
public:bool operator()(const T& x, const T& y){return x < y;}
};
template<class T>
class Greater
{
public:bool operator()(const T& x, const T& y){return x > y;}
};
template<class Compare>
void BubbleSort(int* a, int n, Compare com)
{for (int j = 0; j < n; j++){// 单趟int flag = 0;for (int i = 1; i < n - j; i++){//if (a[i] < a[i - 1])if (com(a[i], a[i - 1])){swap(a[i - 1], a[i]);flag = 1;}}if (flag == 0){break;}}
}
int main()
{Less<int> LessFunc;Greater<int> GreaterFunc;// 函数对象cout << LessFunc(1, 2) << endl;cout << LessFunc.operator()(1, 2) << endl;int a[] = { 9,1,2,5,7,4,6,3 };BubbleSort(a, 8, LessFunc);BubbleSort(a, 8, GreaterFunc);BubbleSort(a, 8, Less<int>());BubbleSort(a, 8, Greater<int>());return 0;
}
此时就发现,改变是否是升降序,就可以利用两个类对象就可以做到,此时priority_queue的比较就是这样
#include<vector>
template<class T>
class Less
{
public:bool operator()(const T& x, const T& y){return x < y;}
};template<class T>
class Greater
{
public:bool operator()(const T& x, const T& y){return x > y;}
};
namespace chuxin
{// 默认是大堆template<class T, class Container = vector<T>, class Compare = Less<T>>class priority_queue{public:void AdjustUp(int child){Compare com;int parent = (child - 1) / 2;while (child > 0){//if (_con[parent] < _con[child])if (com(_con[parent], _con[child])){swap(_con[child], _con[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}}void push(const T& x){_con.push_back(x);AdjustUp(_con.size() - 1);}void AdjustDown(int parent){// 先假设左孩子小size_t child = parent * 2 + 1;Compare com;while (child < _con.size()) // child >= n说明孩子不存在,调整到叶子了{// 找出小的那个孩子//if (child + 1 < _con.size() && _con[child] < _con[child + 1])if (child + 1 < _con.size() && com(_con[child], _con[child + 1])){++child;}//if (_con[parent] < _con[child])if (com(_con[parent], _con[child])){swap(_con[child], _con[parent]);parent = child;child = parent * 2 + 1;}else{break;}}}void pop(){swap(_con[0], _con[_con.size() - 1]);_con.pop_back();AdjustDown(0);}const T& top(){return _con[0];}size_t size() const{return _con.size();}bool empty() const{return _con.empty();}private:Container _con;};
}