浅浅介绍一下STL的stack和queue的知识,若有兴趣,不妨垂阅!
目录
首先stack和queue不属于容器了,而是容器适配器(配接器),这是一个新的概念,在模拟实现部分可以更好的理解这个概念。
1.容器适配器(配接器)
1.1.什么是适配器
适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设 计经验的总结),该种模式是将一个类的接口转换成客户希望的另外一个接口。
1.2.STL标准库中stack和queue的底层结构
虽然stack和queue中也可以存放元素,但在STL中并没有将其划分在容器的行列,而是将其称为 容器适配器(配接器),这是因为stack和队列只是对其他容器的接口进行了包装,STL中stack和queue默认 使用deque。如果不理解,请看下文:
2.stack
我们看一下stack文档 对于stack的介绍:
是一个类模板,有两个模板参数,第二个模板参数有缺省值deque<T>,这是个啥东西我们不用理先,也就是说不必理会第二个模板参数先,先学会stack的使用即可。
其实stack的接口就那么几个,只要浅浅涉猎过数据结构中的栈,那么这几个接口一眼秒懂,所以我就随意介绍一下几个常用接口:
1.stack()
接口说明:构造空的栈。
2.empty()
接口说明:检查stack是否为空。
3.size()
接口说明:返回stack中元素的个数。
4.top()
接口说明:返回栈顶元素的引用。
5.push()
接口说明:将元素val压入stack中。
6.pop()
接口说明:将stack中尾部的元素弹出。
其他的接口可以去查询文档。
我们可以简单使用一下这些接口,当然记得包含头文件<stack>:
#include <stack>
#include <iostream>
using namespace std;
int main()
{
stack<int> s;
cout << s.empty() << endl;
s.push(1);
cout << s.empty() << endl;
s.push(2);
cout << s.size() << endl;
cout << s.top() << endl;
s.pop();
cout << s.size() << endl;
cout << s.top() << endl;
return 0;
}
看到运行结果是符合预期的:
3.模拟实现stack
从stack的接口中可以看出,stack实际是一种特殊的vector,因此使用vector完全可以模拟实现stack。所以我们使用适配器模式来模拟实现stack:
我将模拟实现的stack放在了myStack.h文件中,并用命名空间HD将其封装:
#pragma once
namespace HD
{
template <class T, class Container = vector<T>>
class stack
{
Container _con;
typedef T value_type;
typedef size_t size_type;
public:
void push(const value_type& val);
void pop()
{
_con.pop_back();
}
value_type& top()
{
return _con.back();
}
const value_type& top()const
{
return _con.back();
}
size_type size()const
{
return _con.size();
}
bool empty()const
{
return _con.empty();
}
};
template<class T, class Container>
typename void stack<T, Container>::push(const stack<T, Container>::value_type& val)
{
_con.push_back(val);
}
}
模拟实现stack的精髓就在于第二个模板参数Container。仔细看就可以看到stack的实现就是vector套了一层壳,包括STL的stack也是这样弄的,只不过STL的stack默认不是使用vector来适配,而是选择了另一个容器deque来进行默认适配,所以说stack是容器适配器(配接器),而非容器。
至于deque是啥,有兴趣的读者老爷可以自行去学习,我就不仔细介绍了。
可以在test.cpp文件中测试一下模拟实现的stack:
#include <vector>
#include <iostream>
using namespace std;
#include "myStack.h"
int main()
{
HD::stack<int> ms;
cout << ms.empty() << endl;
cout << ms.size() << endl;
ms.push(1);
cout << ms.top() << endl;
ms.push(2);
cout << ms.size() << endl;
ms.push(3);
ms.push(4);
ms.push(5);
ms.push(6);
ms.pop();
cout << ms.empty() << endl;
cout << ms.size() << endl;
cout << ms.top() << endl;
return 0;
}
看一下运行结果,是符合预期的:
4.queue
我们看一下queue文档对queue的介绍:
见识过适配器模式的话,对于queue这个类模板应该很好理解,我就不解释了,我们直接来浅浅介绍一下常见接口:
1.queue()
接口说明:构造空的队列。
2.empty()
接口说明:检测队列是否为空,是返回true,否则返回false。
3.size()
接口说明:返回队列中有效元素的个数。
4.front()
接口说明:返回队头元素的引用。
5.back()
接口说明:返回队尾元素的引用。
6.push()
接口说明:在队尾将元素val入队列。
7.pop()
接口说明:将队头元素出队列。
其他接口可以去文档查阅,这里就不介绍了。
我们可以简单使用一下这些接口,当然记得包含头文件<queue>:
#include <queue>
#include <iostream>
using namespace std;
int main()
{
queue<int> q;
cout << q.empty() << endl;
q.push(1);
cout << q.empty() << endl;
q.push(2);
cout << q.size() << endl;
cout << q.front() << endl;
cout << q.back() << endl;
q.pop();
cout << q.size() << endl;
cout << q.front() << endl;
cout << q.back() << endl;
return 0;
}
看到结果符合预期:
5.模拟实现queue
同样的,我们使用适配器模式来模拟实现queue,但是我们仔细思考便知,如果跟模拟实现的stack一样使用vector来默认适配的话就不合适,因为quque需要满足队尾入数据、队头出数据,如果还用vector来默认适配的话效率低下,所以我们模拟实现queue使用list来做默认适配。
我将模拟实现的queue放在了myQueue.h文件中,并用命名空间HD将其封装:
#pragma once
namespace HD
{
template <class T, class Container = list<T>>
class queue
{
Container _con;
typedef T value_type;
typedef size_t size_type;
public:
void push(const value_type& val);
void pop()
{
_con.pop_front();
}
value_type& back()
{
return _con.back();
}
const value_type& back()const
{
return _con.back();
}
value_type& front()
{
return _con.front();
}
const value_type& front()const
{
return _con.front();
}
size_type size()const
{
return _con.size();
}
bool empty()const
{
return _con.empty();
}
};
//模板不适合声明和定义分离到两个文件下,所以此次声明和定义分离到同一个文件下
//其实这里本没有必要声明和定义分离,但是练练手
template<class T, class Container>
void queue<T, Container>::push(const queue<T, Container>::value_type& val)
{
_con.push_back(val);
}
}
可以在test.cpp中测试一下模拟实现的queue:
#include <list>
#include <iostream>
using namespace std;
#include "myQueue.h"
int main()
{
HD::queue<int> mq;
cout << mq.empty() << endl;
mq.push(1);
mq.push(2);
mq.push(3);
mq.push(4);
cout << mq.front() << endl;
cout << mq.back() << endl;
cout << mq.size() << endl;
mq.pop();
cout << mq.front() << endl;
cout << mq.back() << endl;
cout << mq.size() << endl;
cout << mq.empty() << endl;
return 0;
}
结果是符合预期的:
6.deque
本博客不去介绍deque的底层,感兴趣的话可以自己去了解。我在这里主要介绍:
deque这个容器是vector和list的缝合怪,那么为什么要弄deque这么一个容器呢?就是因为vector和list的优缺点是可以互补的,那么吸收他们俩的优点搞出来了一个deque。
我们先来分析一下vector和list的优缺点:
1.vector:
优点:
尾插尾删效率不错,支持高效下标随机访问。并且得益于物理空间连续,所以高速缓存利用率高。
缺点:
空间要扩容,扩容有一些代价(效率和空间浪费)。并且头部和中间插入删除效率低。
2.list:
优点:
按需申请释放空间,没有扩容导致的效率和空间浪费。并且任意位置的插入删除效率都不错。
缺点:
不支持下标随机访问。
deque确实吸收了他们俩的优点,但是其底层设计也导致deque又有了一些缺陷,这些缺陷导致deque的应用场景不多:
1. 不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。
2. 中间插入删除效率很低,要挪动数据,时间复杂度是O(N)。
当然deque吸收了他们俩的优点,且得益于其底层设计,所以:
deque和vector相比,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高;而且尾插效率也高于vector;最后在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。
deque和list相比,deque底层是连续空间,空间利用率比较高,不需要存储额外字段。而且头插和尾插效率高于list。
那么STL的stack和queue为什么deque作为stack和queue的底层默认容器,理由如下:
1. stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作,这就避开了deque不适合遍历的缺点。
2. 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。均发挥了deque的优点。
3. stack只需在栈顶操作,恰巧deque尾插效率高于vector,尾删效率也高;queue需要在队头和队尾操作,恰巧deque尾插效率高于list,头删效率也高。
7.小知识
7.1.stack和queue没有迭代器
stack和queue是没有迭代器的。我们可以想想如果有迭代器的话,还能保证stack的数据一定是先进后出吗?还能保证queue的数据一定是先进先出吗?
由于迭代器的功能,显然是不能保证的,所以也stack和queue也不应该设计迭代器。
7.2.可能出现的编译问题
为了介绍这个小知识,我创建了一个工程用于演示,该工程包含两个文件:test.h和test.cpp。
先来看一份代码:
在test.h中:
#pragma once
void func()
{
cout << "hello world" << endl;
}
在test.cpp中:
#include <iostream>
using namespace std;
#include "test.h"
int main()
{
func();
return 0;
}
看一下运行结果:
看起来没什么问题,确实也没什么问题。可是也许有一个疑问:为什么在test.h中没有包含<iostream>,并且没有使用命名空间std,那么为啥test.h中的函数func可以正常使用cout和endl呢?
因为编译器在编译的时候是没有.h这样的概念的,编译器编译的第一个过程叫预处理,预处理后所有的.h文件就在.c文件或者.cpp文件中展开,那么说函数func在预处理阶段就在test.cpp中展开了,画图方便理解:
编译器只会去编译.cpp文件或者.c文件。 预处理展开test.h文件后,在函数func中使用到了cout和endl时,编译器向上搜索可以搜索到<iostream>,并且也使用了命名空间std,所以函数func可以正常使用cout和endl。
再来看一份代码:
在test.h中:
#pragma once
void func()
{
cout << "hello world" << endl;
}
在test.cpp中:
#include <iostream>
#include "test.h"
using namespace std;
int main()
{
func();
return 0;
}
看一下运行结果:
编译不通过,为什么?
我们先来看看预处理后,test.h的展开情况:
编译器只会去编译.cpp文件或者.c文件。 预处理展开test.h文件后,在函数func中使用到了cout和endl时,编译器向上搜索可以搜索到<iostream>,但是还没有使用命名空间std,所以函数func不可以正常使用cout和endl,所以报错信息为: error C2065: “cout”: 未声明的标识符、error C2065: “endl”: 未声明的标识符。
感谢阅读,欢迎斧正!