目录
为什么选择deque作为stack和queue的底层默认容器
前言
学习完了标准库中的string、vector、list这些容器之后,就是使用这些容器了。C++中因为类对结构体的升级,所以能够更加方便的使用这些容器用来适配于我们所需要面对的对象。本节将继续介绍一个新的容器,随后利用适配器原理模拟构建stack以及queue。
在比较容器各种优缺点之前,还要介绍一个为了提供更多接口而诞生出来的容器——deque。这个容器是vector和list在性质上的结合,像链表又像顺序表。
一、deque介绍
deque(通常发音为“deck”)是双端队列的不规则缩写。双头队列是具有动态大小的序列容器,可以在两端(无论是前端还是后端)进行扩展或收缩。
特定的库可能以不同的方式实现双端队列,通常是某种形式的动态数组。但无论如何,它们允许通过随机访问迭代器直接访问单个元素,并根据需要通过扩展和收缩容器自动处理存储。
因此,它们提供了一种类似于载体的功能,但不仅在序列的末尾,而且在序列的开头都能有效地插入和删除元素。但是,与向量不同,双端队列不能保证将其所有元素存储在连续的存储位置:通过将指针偏移到另一个元素来访问双端队列中的元素会导致未定义的行为。
vector和deque都提供了一个非常相似的接口,可以用于类似的目的,但在内部都以截然不同的方式工作:虽然vector使用一个需要偶尔重新分配以供增长的数组,但deque的元素可以分散在不同的存储块中,容器在内部保存必要的信息,以便在恒定时间内通过统一的顺序接口(通过迭代器)直接访问其任何元素。因此,deque在内部比vector复杂一点,但这允许它们在某些情况下更有效地增长,特别是在很长的序列中,重新分配变得更加昂贵。
对于涉及在开始或结束以外的位置频繁插入或删除元素的操作,与列表和前向列表相比,双端队列的性能更差,迭代器和引用的一致性也更低。
实际存储数据的方式如上图所示,有一个中控数组控制存放数据的子数组,中控数组中会有指针指向子数组,所有子数组中元素存放的个数是一样的。这样的结构既能首插也能尾插,也能够支持迭代器的随机访问。空间不足的时候会拓宽中控数组,将原来的指针拷贝过来,然后在空的地方存新开子数组的指针。
关于迭代器iterator,deque的iterator非常复杂,包括了4个指针,分别记录现在的内容、子数组开头、子数组结尾、中控数组的位置,如下图所示:
所以说很复杂,但是它支持“++”“+=”这样的随机指针的操作,根据上图所示迭代起来也很复杂,计算的方式在不同编译器下有不同的实现逻辑。这里不方便深入讲解,接下来就来看看这个容器中有什么样的接口吧。
二、deque函数
deque具有vector和list的性质,所以它所提供的接口也非常丰富,不过作用都是相似的。
1、构造函数
(1) 空容器构造函数(默认构造函数)
构造一个没有元素的空容器。
(2) 填充构造函数
构造一个包含n个元素的容器。每个元素都是if val的副本。
(3) 范围构造器
构造一个包含与范围[first,last)一样多的元素的容器,每个元素都按照相同的顺序从该范围内的相应元素构造而成。
(4) 复制构造函数
构造一个容器,其中包含x中每个元素的副本,顺序相同。
容器保存alloc的内部副本,用于在其整个生命周期内分配存储。如果没有将alloc参数传递给构造函数,则使用默认构造的分配器,以下情况除外:-复制构造函数(4)创建了一个容器,该容器保存并使用x分配器的副本。
元素的存储是使用此内部分配器分配的。
#include <iostream>
#include <deque>
// deque的构造
void test_deque1()
{
unsigned int i;
// 构造函数的使用顺序与上述相同:
std::deque<int> first; // 默认构造
std::deque<int> second (4,100); // 存4个100
std::deque<int> third (second.begin(),second.end()); // 迭代器构造
std::deque<int> fourth (third); // 拷贝构造
// 迭代器构造函数可用于复制数组:
int myints[] = {16, 2, 77, 29};
std::deque<int> fifth (myints, myints + sizeof(myints) / sizeof(int) );
std::cout << "The contents of fifth are:";
for (std::deque<int>::iterator it = fifth.begin(); it != fifth.end(); ++it)
std::cout << ' ' << *it;
std::cout << '\n';
}
int main ()
{
test_deque1();
return 0;
}
![](https://i-blog.csdnimg.cn/direct/fb03372e1441478684571249dd5fa510.png)
2、析构函数
结束的时候自动调用,释放申请的内存,举例可参考构造函数。
3、赋值重载
为容器分配新内容,替换其当前内容,并相应地修改其大小。
将x中的所有元素复制到容器中。
容器保留其当前分配器。
在调用之前,容器中保存的任何元素都会被分配或销毁。
// 赋值重载
void test_deque2()
{
std::deque<int> first (3); // 存入3个默认值
std::deque<int> second (5); // 存入5个默认值
second = first;
first = std::deque<int>();
std::cout << "Size of first: " << int (first.size()) << '\n';
std::cout << "Size of second: " << int (second.size()) << '\n';
}
![](https://i-blog.csdnimg.cn/direct/4c56068dd821489bacf76c29bf18d349.png)
4、迭代器函数
和vecter、list相同,deque也有8个和迭代器相关的函数,按照上图的顺序分别返回:初始位置的迭代器、末尾位置的迭代器、反向的初始位置迭代器、反向的末尾位置迭代器、初始位置的const迭代器、末尾位置的const迭代器、反向的初始位置const迭代器、反向的末尾位置const迭代器。
迭代器使用的方式都是一致的,前后对于其他函数的举例也包含了迭代器的举例,故迭代器不再单独举例。
5、容量类函数
容量函数共有5个,其中max_size()没有实质性意义,所以不做介绍。其他的函数会在下面仔细介绍,并给出使用举例。
5.1、size()
返回双端队列容器中的元素数量。
// size()使用
void test_deque3()
{
std::deque<int> myints;
std::cout << "0. size: " << myints.size() << '\n';
for (int i = 0; i < 5; i++) myints.push_back(i);
std::cout << "1. size: " << myints.size() << '\n';
myints.insert (myints.begin(), 5, 100);
std::cout << "2. size: " << myints.size() << '\n';
myints.pop_back();
std::cout << "3. size: " << myints.size() << '\n';
}
在举例中还使用了insert(),push_back(),pop_back()。毕竟size()和这些函数在使用性上关联是很高的,所以单独使用不方便观察。
5.2、resize()
更改大小
调整容器大小,使其包含n个元素。
如果n小于当前容器大小,则内容将减少到其前n个元素,删除(并销毁)超出的元素。
如果n大于当前容器大小,则通过在末尾插入尽可能多的元素来扩展内容,以达到n的大小。如果指定了val,则新元素将初始化为val的副本,否则将进行值初始化。
请注意,此函数通过插入或删除容器中的元素来更改容器的实际内容。
// resize()使用
void test_deque4()
{
std::deque<int> mydeque;
std::deque<int>::iterator it;
// 载入一些数据到deque:
for (int i = 1; i < 10; ++i) mydeque.push_back(i);
mydeque.resize(5);
mydeque.resize(8, 100);
mydeque.resize(12);
std::cout << "mydeque contains:";
for (std::deque<int>::iterator it = mydeque.begin(); it != mydeque.end(); ++it)
std::cout << ' ' << *it;
std::cout << '\n';
}
该代码将9个数字的序列设置为mydeque的初始内容。然后,它首先使用resize将容器大小设置为5,然后使用新元素的值100将其大小扩展到8,最后使用默认值将其大小延伸到12(对于int元素,这是零)。输出:
5.3、empty()
测试容器是否为空
返回双端队列容器是否为空(即其大小是否为0)。
此函数不会以任何方式修改容器。要清除dequa容器的内容,请参见dequa::clear。
// empty()使用
void test_deque5()
{
std::deque<int> mydeque;
int sum (0);
for (int i = 1;i <= 10; i++) mydeque.push_back(i);
while (!mydeque.empty())
{
sum += mydeque.front();
mydeque.pop_front();
}
std::cout << "total: " << sum << '\n';
}
该示例将容器的内容初始化为一系列数字(从1到10)。然后,它逐一弹出元素,直到容器为空,并计算它们的总和。
5.4、shrink_to_fit()
该函数需要C++11以上的版本。
收缩以适应
请求容器减少内存使用以适应其大小。
双端队列容器可能分配了比容纳其当前元素所需更多的内存:这是因为大多数库将双端队列实现为动态数组,可以保留已删除元素的分配空间,或提前分配额外容量,以实现更快的插入操作。
此函数请求内存使用情况适应容器的当前大小,但该请求是非绑定的,容器实现可以自由地优化其内存使用情况。
请注意,此函数不会改变容器的大小(为此,请参阅调整大小)。
该函数实用性不高,所以跳过举例环节。
6、元素访问
deque支持各种各样的元素访问,能够随机访问使用“[]”和“at”,也能够使用“front”和“back”访问最初的元素和末尾的元素。
这类的使用非常简单,故不单独做举例。
7、修改器函数
根据元素访问函数和修改器类函数,就可以看出deque既能想vector一样支持“[]”的随机访问,又能像链表一样支持push_back()。也就是说,deque的接口是最多的。但是根据结构来说,访问起来会复杂很多。相当于是小六边形战士,都能打但是效率差一些。
接口大致相同也是为了方便程序员使用这些函数,所以学会C++中stl容器的使用是很简单的。难的是底层的逻辑,怎么实现vector、list、deque的。
三、容器性能比较
1、vector1.1、优点
(1)尾插尾删效率不错,支持高效的下标随机访问。
(2)物理空间连续,所以高速缓存利用率高。
1.2、缺点
(1)开空间扩容,扩容需要拷贝数据的代价。
(2)头部插入和中间插入删除效率低。
2、list
2.1、优点
(1)按需申请和释放空间,不需要拷贝数据扩容。
(2)任意位置插入删除、方便。
2.2、缺点
(1)不支持下标随机访问。
(2)结构体中含有前后指针,占用额外的空间。
3、deque
3.1、优点
(1)提供多种接口,支持各种删除插入的操作,能够使用随机迭代器访问元素。其底层是连续空间,空间利用率比较高,不需要存储额外字段(与 list 比)。
(2)头尾中的删除和插入效率都不错,不需要搬移元素,效率特别高(与vector比),空间利用率比list高。
(3)扩容也不需要搬移大量的元素(与vector比)只用拷贝一些指针、代价变小了。
3.2、缺点
(1)结构复杂,“[]”访问消耗更多的时间。
(2)中间插入删除效率低下,极为不便。
deque 有一个致命缺陷:不适合遍历,因为在遍历时, deque 的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下 ,而序列式场景中,可能需要经常遍历,因此 在实际中,需要线性结构时,大多数情况下优先考虑 vector 和 list , deque 的应用并不多,而 目前能看到的一个应用就是, STL 用其作为 stack 和 queue 的底层数据结构 。
四、适配器
简介
什么是适配器 ?
![](https://i-blog.csdnimg.cn/direct/7f3d6714e5b2484382f846d334759d50.png)
为什么选择deque作为stack和queue的底层默认容器
stack 是一种 后进先出 的特殊线性数据结构,因此只要具有 push_back() 和 pop_back() 操作的线性结构,都可以作为stack 的底层容器,比如 vector 和 list 都可以;queue 是 先进先出 的特殊线性数据结构,只要具有push_back 和 pop_front 操作的线性结构,都可以作为 queue 的底层容器,比如list。但是 STL 中对 stack 和 queue 默认选择 deque 作为其底层容器,主要是因为:1. stack 和 queue 不需要遍历 ( 因此 stack 和 queue 没有迭代器 ) ,只需要在固定的一端或者两端进行操作。2. 在 stack 中元素增长时, deque 比 vector 的效率高 ( 扩容时不需要搬移大量数据 ) ; queue 中的元素增长时, deque 不仅效率高,而且内存使用率高。结合了 deque 的优点,而完美的避开了其缺陷。
使用
适配器,封装容器后,用容器的接口完成对应的任务。
例如之前所缺的 stack——栈、queue——队列,都可以用如今学的vector、list、deque进行封装实现。虽然实现出来的功能相同,但是底层逻辑却完全不同。另外如果提供的容器没有类似的接口,就不能使用该容器进行封装,需更换更为合适的容器。
C++中因为有类的存在,对比C语言封装变得极为简单。就直接在新的类里面套用对应的适配器接口即可。
1、stack和queue
1.1、功能
栈相信大家都知道,是先进后出的。。
对列和栈相对应,是先进先出的代表
1.2、模拟实现
#include<deque>
namespace zhc
{
template<class T, class Con = std::deque<T>>
class stack
{
public:
stack()
{}
void push(const T& x)
{
_c.push_back(x);
}
void pop()
{
_c.pop_back();
}
T& top()
{
return _c.back();
}
const T& top()const
{
return _c.back();
}
size_t size()const
{
return _c.size();
}
bool empty()const
{
return _c.empty();
}
private:
Con _c;
};
template<class T, class Con = std::deque<T>>
class queue
{
public:
queue()
{}
void push(const T& x)
{
_c.push_back(x);
}
void pop()
{
_c.pop_front();
}
T& back()
{
return _c.back();
}
const T& back()const
{
return _c.back();
}
T& front()
{
return _c.front();
}
const T& front()const
{
return _c.front();
}
size_t size()const
{
return _c.size();
}
bool empty()const
{
return _c.empty();
}
private:
Con _c;
};
}
1.3、使用测试
#include <iostream>
#include <string>
using namespace std;
#include "test.h"
void test_stack()
{
zhc::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
cout << "st中的元素依次打印出栈: ";
for(int i = 0; i < 4; ++i)
{
cout << st.top() << " ";
st.pop();
}
cout << endl;
}
void test_queue()
{
zhc::queue<string> qu;
qu.push("csdn");
qu.push("I");
qu.push("like");
qu.push("it");
cout << "qu中的元素依次打印出队列: ";
for(int i = 0; i < 4; ++i)
{
cout << qu.front() << " ";
qu.pop();
}
cout << endl;
}
int main()
{
test_stack();
test_queue();
return 0;
}
队列功能是没有问题的,如果想要更换底层逻辑(例如栈使用vector的逻辑)也可以用改用如下代码:
lcs::stack<int, vector<int>> st1;
在创建对象的时候注明使用的容器即可更换底层容器。
2、priority_queue
2.1、作用介绍
优先级队列
- 优先级队列是一种容器适配器,根据一些严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的一个。
- 此上下文类似于堆,其中可以随时插入元素,并且只能检索最大堆元素(优先级队列中位于顶部的元素)。
- 优先级队列被实现为容器适配器,这些类使用特定容器类的封装对象作为其底层容器,提供一组特定的成员函数来访问其元素。元素从特定容器的“后面”弹出,这被称为优先级队列的顶部。
- 底层容器可以是任何标准容器类模板或一些其他专门设计的容器类。容器应可通过随机访问迭代器访问,并支持以下操作:
empty() :检测容器是否为空size() :返回容器中有效元素个数front() :返回容器中第一个元素的引用push_back() :在容器尾部插入元素pop_back() :删除容器尾部元素
- 标准容器类vector和deque满足这些要求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用标准容器向量。
- 需要支持随机访问迭代器,以便始终在内部保持堆结构。这是由容器适配器通过在需要时自动调用算法函数make_heap、push_eap和pop_heap来自动完成的。
2.2、模拟实现
#include<vector>
#include<functional>
#include<assert.h>
namespace zhc
{
template <class T, class Container = std::vector<T>, class Compare = std::less<T>>
class priority_queue
{
public:
// 函数都是自定义类型,自动调用自己的默认构造
priority_queue()
{}
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last)
: _c(first, last)
{
size_t parent = (_c.size() + 1) / 2;
while(parent--)
{
adjust_down(parent);
}
}
void adjust_down(size_t parent)
{
assert(!empty());
size_t child = 2 * parent + 1;
const size_t size = _c.size();
while(child < size)
{
if(child + 1 < size && _comp(_c[child + 1], _c[child]))
{
child++;
}
if(_comp(_c[child], _c[parent]))
{
std::swap(_c[child], _c[parent]);
parent = child;
child = 2 * parent + 1;;
}
else
{
break;
}
}
}
bool empty() const
{
return _c.empty();
}
size_t size() const
{
return _c.size();
}
const T& top() const
{
return _c.front();
}
void adjust_up(size_t child)
{
size_t parent = (child - 1) / 2;
while(child > 0 && _comp(_c[child], _c[parent]))
{
std::swap(_c[child], _c[parent]);
child = parent;
parent = (child - 1) / 2;
}
}
void push(const T& x)
{
_c.push_back(x);
adjust_up(_c.size() - 1);
}
void pop()
{
assert(!empty());
std::swap(_c[0], _c[size() - 1]);
_c.pop_back();
if(!empty())
{
adjust_down(0);
}
}
private:
Container _c;
Compare _comp;
};
}
接口都是相互套用的,唯一需要手搓的就是向上遍历和向下遍历。
2.3、使用测试
#include <iostream>
#include <string>
using namespace std;
#include "test.h"
void test_priority_quque1()
{
zhc::priority_queue<int> my_pq;
my_pq.push(5);
my_pq.push(9);
my_pq.push(16);
my_pq.push(1);
my_pq.push(-4);
my_pq.push(-2);
while(!my_pq.empty())
{
std::cout << my_pq.top() << " ";
my_pq.pop();
}
cout << endl;
}
void test_priority_quque2()
{
std::vector<string> v{"abcd", "aaa", "ddd", "abc"};
// v.push_back("abcd");
// v.push_back("aaa");
// v.push_back("ddd");
// v.push_back("abc");
zhc::priority_queue<string> my_pq(v.begin(), v.end());
while(!my_pq.empty())
{
std::cout << my_pq.top() << " ";
my_pq.pop();
}
cout << endl;
}
int main()
{
test_priority_quque1();
test_priority_quque2();
return 0;
}
结果无误。
当然这个类中使用了仿函数等操作,这里Compare实例化出来的就是仿函数。实例化出来的类的使用方法就和函数指针相同。
template <class T, class Container = std::vector<T>, class Compare = std::less<T>>
class priority_queue
这里利用Compare来实现了堆的比较关系,如果想改变堆中比较的逻辑,可以把小堆改成大堆。例如:
void test_priority_quque3()
{
lcs::priority_queue<int, std::vector<int>, std::greater<int>> my_pq;
my_pq.push(5);
my_pq.push(9);
my_pq.push(16);
my_pq.push(1);
my_pq.push(-4);
my_pq.push(-2);
while(!my_pq.empty())
{
std::cout << my_pq.top() << " ";
my_pq.pop();
}
cout << endl;
}
int main()
{
// test_priority_quque1();
// test_priority_quque2();
test_priority_quque3();
return 0;
}
仿函数
仿函数的特点
1.仿函数即使定义相同,也可能有不同的类型;
2.仿函数通常比一般函数速度快;
3.仿函数使程序代码变简单。
结语
本篇文章到此也就告一段落了,没有实际举例仿函数的使用。这里简单介绍几句,仿函数就是一个类重载了“()”,从而达到类向函数一样使用的目的。需要注意的是,这里使用的仿函数是类而不是类型。传给对象的是类型,但是使用需要实例化出类的对象。