小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
c++系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
目录
前言
【c++】STL容器-list的模拟实现(迭代器由浅入深逐步完善2w字讲解)——书接上文 详情请点击<——
本文由小编为大家介绍——【c++】STL容器-stack和queue的使用与模拟实现(附加deque的简单讲解)
小编有写的使用c语言实现的栈和队列,关于栈和队列的基本性质在下面两个文章中都有讲解,在阅读本文前请读者友友先阅读下方两篇文章,本文会基于下面两篇文章进行讲解:
一、stack的介绍
stack是一种容器适配器,专门用于先进后出,只能从容器的一端进行数据的插入和删除操作
stack是作为容器适配器被实现的,容器适配器即对特定类进行封装作为底层容器, 并且提供一组特定的成员函数来访问其元素
stack的底层容器可以是任何标准容器类模板或其它特定的容器类,这些容器应该支持以下接口
- push_back,尾部插入元素
- pop_back,尾部删除元素
- back,获取尾部元素的引用
- size,获取元素的总个数
- empty,判断容器中的元素是否为空
标准容器vector,list,deque均符合这些要求,默认情况下,如果我们没有为stack指定特定的底层容器,默认采用deque作为其底层容器,针对deque,小编后文会进行简单介绍的
- 并且由于是封装的底层容器,所以对于构造、拷贝、构造析构、赋值等等,我们都不需要进行实现,底层封装的容器都有这些函数,所以都由封装的底层容器进行默认调用实现
二、stack的模拟实现
铺垫
- stack是类模板,由于模板的声明和定义不分离,以及为了将我们模拟实现的stack和STL中的stack进行区分,所以我们将我们的模拟实现放在stack.h中的命名空间中
- 在test.cpp包含main函数以及相关的头文件进行测试
- 那么我们实现stack的时候同样要使用类模板来进行实现,T作为stack存储的数据类型,并且观察上图,模板参数列表中的模板参数还可以有缺省值,这个缺省值与函数的缺省值(函数的缺省值的是数据)不同的是模板参数的缺省值是类型,默认情况下我们采用deque作为stack的底层容器,即deque<T>这个类型作为Container这个模板参数的缺省值
- stack是容器适配器,即通过对底层的容器进行一定的控制,封装进而实现stack的预期接口,毕竟不能直接对容器进行控制,所以我们要控制容器实例化出的对象,并且封装这个对象的接口进而实现stack的需求接口,所以我们应该使用Container定义一个私有成员变量_con作为stack进行控制容器的入口
- 在使用deque这个容器的时候要包头文件#include <deque>
template<typename T,typename Container = deque<T>>
class stack
{
public:
private:
Container _con;
};
push
- 函数参数由于我们不知道T是内置类型还是自定义类型的,如果是自定义类型的传参消耗大,所以我们这里传引用传参,并且我们对其不进行修改,所以加const进行修饰
- push是在栈顶(数据序列的的结尾)入数据,那么我们直接调用尾插即可实现
void push(const T& val)
{
_con.push_back(val);
}
pop
- pop是进行出栈(删除尾部数据),这里我们调用尾删即可实现
void pop()
{
_con.pop_back();
}
top
- top是用于获取栈顶的引用(尾部的数据的引用),这里我们调用back即可,其返回的就为尾部数据的引用
T& top()
{
return _con.back();
}
empty
- empty是进行判断容器中的数据是否为空,这里我们调用empty即可实现
- 这里仅仅是进行判断,并不对数据进行修改,所以这里我们加const进行修饰this指针指向的对象,让普通对象和const对象都可以进行调用
bool empty() const
{
return _con.empty();
}
size
- size是进行获取容器中的数据个数,这里我们调用size即可实现
- 这里仅仅是进行获取数据个数,并不对数据进行修改,所以这里我们加const进行修饰this指针指向的对象,让普通对象和const对象都可以进行调用
- 返回值是个数,断然不可能为负数,所以返回值类型我们采用size_t
size_t size() const
{
return _con.size();
}
测试
- 这里我们使用如下代码进行测试stack中的全部接口
void test_stack()
{
stack<int> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
cout << st.size() << endl;
while (!st.empty())
{
cout << st.top() << ' ';
st.pop();
}
cout << endl;
cout << st.size() << endl;
}
测试结果如下,正确
三、queue的介绍
队列是一种容器适配器,专门用于先进先出,从容器的一端插入数据,从容器的另一端提取数据
队列作为容器适配器实现。容器适配器即将特定的容器类进行封装作为器底层容器类,提供一组特定的成员函数来访问元素。元素从队尾入队列,从队头出队列
queue的底层容器可以是任何标准容器类模板或其它特定的容器类,这些容器应该支持以下接口
- push_back,尾部插入元素
- pop_front,头部删除元素
- front,获取头部元素的引用
- back,获取尾部元素的引用
- size,获取元素的总个数
- empty,判断容器中的元素是否为空
标准容器list,deque均符合这些要求,默认情况下,如果我们没有为queue指定特定的底层容器,默认采用deque作为其底层容器,针对deque,小编后文会进行简单介绍的
- 标准容器vector不作为queue的底层容器,因为要queue头删数据,vector的头删数据要进行挪动数据,并且时间复杂度为O(N),并且vector的接口中没有提供pop_front,当然如果非要使用vector我们通常对于pop的底层接口使用erase(_con.begin())
- 并且由于是封装的底层容器,所以对于构造、拷贝、构造析构、赋值等等,我们都不需要进行实现,底层封装的容器都有这些函数,所以都由封装的底层容器进行默认调用实现
四、queue的模拟实现
铺垫
- queue是类模板,由于模板的声明和定义不分离,以及为了将我们模拟实现的queue和STL中的queue进行区分,所以我们将我们的模拟实现放在queue.h中的命名空间中,注意由于是同一个项目中,这里的命名空间名称和queue中的相同会和queue的命名空间合并
- 在test.cpp包含main函数以及相关的头文件进行测试
- 那么我们实现queue的时候同样要使用类模板来进行实现,T作为queue存储的数据类型,并且使用deque<T>这个类型作为Container这个模板参数的缺省值
- queue是容器适配器,即通过对底层的容器进行一定的控制,封装进而实现queue的预期接口,毕竟不能直接对容器进行控制,所以我们要控制容器实例化出的对象,并且封装这个对象的接口进而实现queue的需求接口,所以我们应该使用Container定义一个私有成员变量_con作为queue进行控制容器的入口
- 在使用deque这个容器的时候要包头文件#include <deque>
push
- 函数参数由于我们不知道T是内置类型还是自定义类型的,如果是自定义类型的传参消耗大,所以我们这里传引用传参,并且我们对其不进行修改,所以加const进行修饰
- push是在队尾(数据序列的结尾)入数据,那么我们直接调用尾插即可实现
void push(const T& val)
{
_con.push_back(val);
}
pop
- pop是进行在队头(序列数据的头部)删除数据,这里我们调用头删即可实现
void pop()
{
_con.pop_front();
}
front
- front是用于获取队头的引用(头部的数据的引用),这里我们调用front即可,其返回的就为头部数据的引用
T& front()
{
return _con.front();
}
back
- back是用于获取队尾的引用(尾部的数据的引用),这里我们调用back即可,其返回的就为尾部数据的引用
T& back()
{
return _con.back();
}
empty
- empty是进行判断容器中的数据是否为空,这里我们调用empty即可实现
- 这里仅仅是进行判断,并不对数据进行修改,所以这里我们加const进行修饰this指针指向的对象,让普通对象和const对象都可以进行调用
bool empty() const
{
return _con.empty();
}
size
- size是进行获取容器中的数据个数,这里我们调用size即可实现
- 这里仅仅是进行获取数据个数,并不对数据进行修改,所以这里我们加const进行修饰this指针指向的对象,让普通对象和const对象都可以进行调用
- 返回值是个数,断然不可能为负数,所以返回值类型我们采用size_t
size_t size() const
{
return _con.size();
}
测试
- 这里我们使用如下代码进行测试queue中的全部接口
void test_queue()
{
queue<int> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
cout << q.size() << endl;
while (!q.empty())
{
cout << q.front() << ' ';
q.pop();
}
cout << endl;
cout << q.size() << endl;
}
测试结果如下,正确
五、deque的简单介绍
deque双端队列,是一种双开口的“连续”空间的数据结构,双开口的含义是可以在头尾两端进行数据的插入和删除操作,且时间复杂度为O(1)
- 观察,deque的接口很神奇,可以同时支持了operator[]即随机访问(vector的性质),和头插头删(list的性质)尾插尾删,那么它实际上并没有撼动vector和list的地位,并不能达到替代vector和list的目的
六、为什么deque不能替代vector和list
相比于vector
- 极大的缓解了头部中间插入/扩容的问题
- 但是operator[]不够极致,需要计算在哪一个空间(buff),在哪一个空间(buff)的第几个,基于这个原因,在需要进行频繁的进行下标访问的情况下,其效率和性能不及vector,所以无法替代
operator[]的计算过程:
- 先看是否在第一个用于头插的空间(buff)中,如果在那么使用位置进行访问
- 将下标 i 减去第一个空间(buff)的元素个数大小sz,得出新的下标i
- 使用下标 i / 空间中的数据个数——>得出是在第几个空间(buff)
- 使用下标 i % 空降中的数据个数——>得出是在空间中的第几个位置
相比于list
- 可以支持下标的随机访问
- 缓存利用率较高,不需要频繁的去堆上申请空间
- 头尾的插入和删除效率高O(1),但是中间数据插入删除需要进行挪动数据,因为不能将插入位置所在的空间进行扩容,一旦扩容,那么operator[]的计算就没有办法确定具体是在哪一个空间(buff),空间的第几个位置了,只能从头开始依次遍历空间(buff)去寻找所在位置了,所以一旦进行扩容之后operator[]的效率变差很多,所以这里不能进行对插入位置所在空间进行扩容,针对插入和删除,只能进行挪动数据
作为stack和queue容器适配器
- 那么作为stack和queue的容器适配器来讲是不是就很合适了呢?
- 因为由于stack和queue的性质就决定了不能够去随便的去中间位置进行插入和删除数据,并且不能够使用迭代器或operator[]去随便的访问或遍历数据
- stack中进行了高频的尾插尾删和queue中进行了高频的尾插头删,即头插头删,尾插尾删对于deque来讲时间复杂度可是O(1),所以作为stack和queue的容器适配器完美契合了
deque的迭代器
- 观察,deque同时也支持迭代器,并且deque的迭代器是随机迭代器,可以支持sort算法进行排序
- 那么既然有了那么多的指针指向的不连续的空间(buff),那么迭代器又是如何运作的呢?
- deque的迭代器同样也是一个模板类,其成员变量包括四个指针,cur指向当前所处空间的位置,first指向当前所处空间的起始位置,end指向当前所处空间的结束位置,node指向中控指针数组的位置,并且由于中控指针数组的物理空间是连续的,所以使用 *(node+1) 就可以找到下一个空间的指针,进而进行空间(buff)的的跳转
- 那么接下来就可以从begin返回的迭代器指向的空间(buff)的cur位置处开始进行访问遍历,当遍历到当前空间(buff)的last指针位置处的时候通过node指针使用 *(node+1) 进行跳转空间(buff),跳转空间继续执行遍历操作,再跳转进行遍历,直到跳转遍历到end迭代器指向的空间(buff)的cur指针指向的位置处结束遍历
七、deque的简单使用
- 在包了头文件 #include <iostream> 和 #include <deque> 以及将命名空间 using namespace std 展开之后就可以使用deque了
- 与常规容器使用规则一样deque是类模板,需要我们显示实例化(显示传入类型)之后才能进行使用
void test_deque()
{
deque<int> dq;
dq.push_back(1);
dq.push_back(2);
dq.push_back(3);
dq.push_back(4);
deque<int>::iterator it = dq.begin();
while (it != dq.end())
{
cout << *it << ' ';
it++;
}
cout << endl;
}
运行结果如下
八、stack和queue实现的源代码
stack.h
#pragma once
#include <deque>
namespace wzx
{
template<typename T,typename Container = deque<T>>
class stack
{
public:
void push(const T& val)
{
_con.push_back(val);
}
void pop()
{
_con.pop_back();
}
T& top()
{
return _con.back();
}
bool empty() const
{
return _con.empty();
}
size_t size() const
{
return _con.size();
}
private:
Container _con;
};
void test_stack()
{
stack<int> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
cout << st.size() << endl;
while (!st.empty())
{
cout << st.top() << ' ';
st.pop();
}
cout << endl;
cout << st.size() << endl;
}
}
queue.h
#pragma once
#include <deque>
#include <vector>
namespace wzx
{
template<typename T, typename Container = deque<T>>
class queue
{
public:
void push(const T& val)
{
_con.push_back(val);
}
void pop()
{
_con.pop_front();
//_con.erase(_con.begin()); //直接将vector禁掉
}
T& front()
{
return _con.front();
}
T& back()
{
return _con.back();
}
bool empty() const
{
return _con.empty();
}
size_t size() const
{
return _con.size();
}
private:
Container _con;
};
void test_queue()
{
queue<int> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
cout << q.size() << endl;
while (!q.empty())
{
cout << q.front() << ' ';
q.pop();
}
cout << endl;
cout << q.size() << endl;
}
}
test.cpp
#include <iostream>
using namespace std;
#include "stack.h"
#include "queue.h"
void test_deque()
{
deque<int> dq;
dq.push_back(1);
dq.push_back(2);
dq.push_back(3);
dq.push_back(4);
deque<int>::iterator it = dq.begin();
while (it != dq.end())
{
cout << *it << ' ';
it++;
}
cout << endl;
}
int main()
{
//wzx::test_stack();
//wzx::test_queue();
test_deque();
return 0;
}
总结
以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!