今天,我们来讲解一下Stack和queue的模拟实现,由于有了我们C语言数据结构的Stack和queue的模拟实现的铺垫,现在我们写出C++的Stack和queue的模拟实现就变得非常容易了。
在此之前,我们先简单认识一下C++ Stack中库的接口有哪些:
预知道:
什么是 容器适配器?
它是基于现有的容器类型(比如vector,list,deque),提供了不同的接口和特定的行为(对底层容器的接口进行封装和调整)。
栈的函数接口
认识:
1.栈的操作是后进先出,而且只能从栈顶那里出入或者删除的。
2.stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
3.栈的底层容器的容器类应支持
empty:判空操作
back:获取尾部元素操作
push_back:尾部插入元素操作
pop_back:尾部删除元素操作
因此,标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque(我们在后面讲解)。
函数说明 接口说明 stack() 构造空的栈 empty() 检测stack是否为空 size() 返回stack中元素的个数 top() 返回栈顶元素的引用 push() 将元素val压入stack中 pop() 将stack中尾部的元素弹出 有了之前的基础,相信看到这些函数名字都基本能看懂并可以使用的。
队列的函数接口:
认识:
1.queue是一种先进先出的模式,一端进,一端出(即队头出,队尾进)。
2.队列的底层容器的容器类应支持
empty:检测队列是否为空
size:返回队列中有效元素的个数
front:返回队头元素的引用
back:返回队尾元素的引用
push_back:在队列尾部入队列
pop_front:在队列头部出队列
因此,标准容器deque、list均符合这些需求,默认情况下,如果没有为queue指定特定的底层容器,默认情况下使用deque(我们在后面讲解)。
函数声明 接口说明 queue() 构造空的队列 empty() 检测队列是否为空,是返回true,否则返回false size() 返回队列中有效元素的个数 front() 返回队头元素的引用 back() 返回队尾元素的引用 push() 在队尾将元素val入队列 pop() 将队头元素出队列
从上面的接口我们知道,Stack实质上就是特殊的vector,为了让大家更好理解,所以我们先以vector标准容器来实现。
使用vector标准容器来实现stack
成员函数的框架建立
namespace bai //避免与库里面的stack命名冲突
{
template<class T>
class stack
{
public:
private:
std::vector<T> _st; //利用vector标准容器
};
构造函数(初始化)
因为它是自定义类型,不写初始化列表,自己默认去找编译器的构造函数去初始化
stack() { }
插入push
void push(const T& x)
{
_st.push_back(x);
}
删除pop
void pop()
{
_st.pop_back();
}
栈顶元素top
T& top()
{
return _st.back();
}
返回size个数
int size()const
{
return _st.size();
}
判断空empty
bool empty()const
{
return _st.empty();
}
从上面的接口我们知道,queue需要大量的头删,尾插。如果使用vector的话,效率太低了,所以实质上就是特殊的list,为了让大家更好理解,所以我们先以list标准容器来实现。
使用list标准容器来实现queue
成员函数的框架建立
#include <list>
namespace bai
{
template<class T>
class queue
{
public:
private:
std::list<T> _Q;
};
}
构造函数
queue()
{}
插入push
void push(const T& x)
{
_Q.push_back(x);
}
删除pop
void pop()
{
_Q.pop_front();
}
返回队尾元素
T& back()
{
return _Q.back();
}
//不可修改,只读
const T& back()const
{
return _Q.back();
}
返回队头元素front
T& front()
{
return _Q.front();
}
const T& front()const
{
return _Q.front();
}
返回size个数
size_t size()const
{
return _Q.size();
}
判断是否空empty
bool empty()const
{
return _Q.empty();
}
经过上面两个的实现,我们发现它们都大差不差。
好了,有了上面的实现,现在我们再来认识一下关于deque的知识。
适配器:
概念:适配器是一种设计模式,它允许将一个类的接口转换成客户端所期望的另一种接口,从而使原本不兼容的类能够一起工作。
我们可以看到,无论是stack,queue还是priority_queue,它这里使用了类模板,都使用了容器适配器,并且stack和queue里面的deque是一个容器。
那么duque是什么呢?
deque的介绍(简单了解):
deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高
注意:deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维数组
双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落在了deque的迭代器身上。
借助其迭代器维护其假想连续的结构
我们知道,vector可以支持下标随机访问,但是它存在扩容问题,中间和头部的插入删除效率问题。list可以任意位置的插入或者删除,按需申请释放空间,不存在扩容问题,但是它不支持随机访问。而deque可以认为是结合这两个的优点进行结合的。
那么,有人会问了,既然deque都结合了它们的优点了,解决了它们之间的缺点,能不能就完全取代了它呢?显然这是不能的,deque还是在一些情况下有非常大的缺点的。
相比vector:
deque确实极大缓解了扩容问题和中间,头部的插入删除,但是它的[]效率不够vect快,因为它还要计算在哪个buffer,buffer的第几个。
下面我们通过比较在deque的不同方式的排序来证明它们的差异:
一个是通过拷贝到vector再排序。
另一个是之间利用deque的函数接口下排序。
#include<iostream>
using namespace std;
#include<deque>
#include<vector>
#include<algorithm>
void test_op()
{
srand(time(0));
const int N = 1000000;
vector<int> v1;
vector<int> v2;
v1.reserve(N);
v2.reserve(N);
deque<int> dq1;
deque<int> dq2;
for (int i = 0; i < N; ++i)
{
auto e = rand();
//v1.push_back(e);
//v2.push_back(e);
dq1.push_back(e);
dq2.push_back(e);
}
// 拷贝到vector排序,排完以后再拷贝回来
int begin1 = clock();
// 先拷贝到vector
for (auto e : dq1)
{
v1.push_back(e);
}
// 排序
sort(v1.begin(), v1.end());
// 拷贝回去
size_t i = 0;
for (auto& e : dq1)
{
e = v1[i++];
}
int end1 = clock();
int begin2 = clock();
//sort(v2.begin(), v2.end());
sort(dq2.begin(), dq2.end());
int end2 = clock();
// 11:46继续
printf("deque copy vector sort:%d\n", end1 - begin1);
printf("deque sort:%d\n", end2 - begin2);
}
我们可以发现,deque的效率确实比vector低。
同样,与list相比,deque可以支持随机访问,cpu高速缓存效率高,头插头删,尾插尾删效率高。但是中间插入删除效率是非常低的,原因也是要找在哪个buffer,buffer的第几个。
那么,为什么它适用于stack于queue?
你想想stack与queue的性质,它们是不是只要都是在头和尾进行操作的?这样它们是不是就对于deque就非常适用了。
所以,STL中,我们的stack实现方式
namespace bai
{
template<class T,class Container=deque<T>>
class stack
{
public:
stack()
{ }
void push(const T& x)
{
_st.push_back(x);
}
void pop()
{
_st.pop_back();
}
T& top()
{
return _st.back();
}
int size()const
{
return _st.size();
}
bool empty()const
{
return _st.empty();
}
private:
Container _st;
};
queue
namespace bai
{
template<class T,class Container=deque<T>>
class Queue
{
public:
void push(const T&x)
{
_q.push_back(x);
}
void pop()
{
_q.pop_front();
}
T& front()
{
return _q.front();
}
T& back()
{
return _q.back();
}
int size()const
{
return _q.size();
}
bool empty()const
{
return _q.empty();
}
private:
Container _q;
};
好了,本次分享到处结束了,希望你我共进步!