C++栈的模拟实现
今天,我们来讲解一下Stack和queue的模拟实现,由于有了我们C语言数据结构的Stack和queue的模拟实现的铺垫,现在我们写出C++的Stack和queue的模拟实现就变得非常容易了。
数据结构——栈的实现-CSDN博客
数据结构——队列_队列可以插入字符吗-CSDN博客
在此之前,我们先简单认识一下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();// 先拷贝到vectorfor (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;};
好了,本次分享到处结束了,希望你我共进步!