关于我:
睡觉待开机:个人主页
PDF版免费提供:倘若有需要,想拿我写的博客进行学习和交流,可以私信我将免费提供PDF版。
留下你的建议:倘若你发现本文中的内容和配图有任何错误或改进建议,请直接评论或者私信。
倡导提问与交流:关于本文任何不明之处,请及时评论和私信,看到即回复。
1.前言
栈与队列,是我们平常经常用到的数据结构之一,尤其做题的时候会经常用到。那栈是怎么实现的?为什么这么实现?本文将简单回答并整理。
2.栈
2.1栈的简介
栈 是一种 特殊的线性表
,具有数据 先进后出 特点。
CPP库参考文档:stl_stack
栈提供了常见的几个接口:push\pop\top\size\empty
2.2栈接口的认识
construct
//可以构造一个空的stack
stack<int> s;
//可以先构造一个deque,再将其值赋值给sv进行构造
deque<int> v(6, 6);
stack<int> sv(v);
while (!sv.empty())
{
cout << sv.top() << " ";
sv.pop();
}//6 6 6 6 6 6
cout << endl;
swap
stack<int> s1;//空的栈
deque<int> v(6, 6);
stack<int> s2(v);//有内容的栈
s2.swap(s1);
cout << "this is s1" << ":" << endl;
while (!s1.empty())
{
cout << s1.top() << " ";
s1.pop();
}
cout << endl;
cout << "this is s2" << ":" << endl;
while (!s2.empty())
{
cout << s2.top() << " ";
s2.pop();
}
cout << endl;
/*this is s1:
6 6 6 6 6 6
this is s2:*/
栈中的迭代器在哪?
栈作为一种容器,是没有迭代器的。之所以不提供迭代器,因为迭代器具有随机访问的功能,这会打破栈先进后出的特性。
2.3栈的简化模拟实现
#pragma once
#include<vector>
#include<iostream>
using namespace std;
namespace szg
{
template<class T, class Container = vector<T>>
class stack
{
private:
Container _st;
public:
void push_back(const T& num)
{
_st.push_back(num);
}
void pop_back()
{
_st.pop_back();
}
bool empty()
{
return _st.empty();
}
size_t size()
{
return _st.size();
}
const T& top()
{
return _st.back();
}
};
}
2.4适配器模式
适配器模式是指软件开发中的一种经典设计模式,适配器模式是一种将一个类的接口转换成客户希望的另外一个接口的设计模式。由于其转变方便,而受到软件设计者们的喜爱,在上文模拟stack的过程中,template<class T, class Container = vector<T>>
的container就充当了适配器的角色。
实际开发:
在实际开发过程中,我们经常遇到这样的事情,我们根据初步的需求制定了一个基类,在开发过程中才了解到详细的需求或者需求发生了变动。而开发工作中的接口早已经定义完毕,并已经大规模投入编码。此时若改动接口的定义会造成很多编码上重复性的修改工作,并进而有可能造成修改不完全而导致的语义错误或逻辑错误。语义错误尚可以在编译阶段发现,而一旦发生逻辑性的错误,后果将会非常严重,甚至足以导致系统崩溃。此时就需要用到适配器模式的设计方法。
主要应用:
适配器模式主要应用于,当接口里定义的方法无法满足客户的需求,或者说接口里定义的方法的名称或者方法界面与客户需求有冲突的情况。
两类模式:
- 对象适配器模式 - 在这种适配器模式中,适配器容纳一个它我包裹的类的实例。在这种情况下,适配器调用被包裹对象的物理实体。
- 类适配器模式 - 这种适配器模式下,适配器继承自已实现的类(一般多重继承)。
既然说到适配器这个话题,我们简单来介绍一下STL库中最常用的适配器——deque
3.deque双端队列
deque虽然叫做双端队列,但实在是跟队列没什么关系,甚至说底层完全不是队列。deque是STL中的容器之一,是经典的适配器容器,他被创作出来最重要的应用场景就是做类适配器而存在。
deque是一种“全面发展”容器选手,融合了vector和list的特性。
3.1deque的特性
之所以说deque是一种融合vector和list的特性,是因为deque的特性:
除此之外,他不但支持vector的[]随机访问,还支持list的头插头删效率很高的特点。
可谓是“能文能武”,这么“全能”的deque底层结构是如何的呢?
3.2deque的内部构造
deque的内部控制是依靠迭代器实现的。
● cur是指向当前的访问元素
● first是指向当前buff的开始元素
● end是指向当前buff的末尾元素的下一个地址
● node是指向当前buff在中控数组中存放的位置
3.3deque的操作逻辑
deque的插入和删除,效率很高:
deque的头插尾插效率是挺高的。这是因为尾插一个元素后,迭代器会看看中控数组最后一个buff是否还有空间,如果有则尾插到最后一个buff,如果没有就新开一个buff插入。头插一个元素,他会现在中控数组的头部开一个buff,因为默认是从中控数组中间开始新增的,所以可以支持常数时间开空间,之后同尾插同理。
中间插入插入元素处理比较麻烦
deque中间插入有两种设计,
- 如果中间插入元素后面所有元素都往后挪动一位,效率比较低
- 如果中间插入元素改变buff的大小,那么上面[]访问规则就不适用,会很麻烦。
deque的元素[]访问计算规则,且[]访问效率一般
一般情况下,buff每个都是相同大小并且没有头插新元素时候,下标访问可以采用
● 先找是第几个buff,n/buff.size()
● 在确定是这个buff中的第几个元素,n%buff.size()
但是如果有头插元素,首先应该减去第一个buff元素的个数,然后在进行上面步骤。
● n-=buff1.size()
void test_op1()
{
srand(time(0));
const int N = 1000000;
deque<int> dq;
vector<int> v;
for (int i = 0; i < N; ++i)
{
auto e = rand() + i;
v.push_back(e);
dq.push_back(e);
}
int begin1 = clock();
sort(v.begin(), v.end());
int end1 = clock();
int begin2 = clock();
sort(dq.begin(), dq.end());
int end2 = clock();