1. 双端队列和std::duque
双端队列实际上是队列的一种变形,队列要求只能在队尾添加元素,在队头删除元素,而双端队列在队头和队尾都可以进行添加和删除元素的操作。双端队列是限定插入和删除操作在表的两端进行的线性表。C++中提供deque容器来实现双端队列的功能。
std::duque(double-venden queue, 双端队列)是C++容器库里中有下标顺序容器,它允许在首尾部两端快速的插入和删除元素。其与std::vector的存储方式不同,deque的元素不是连续存储的。
2. deque的用法
2.1 deque的定义和声明
std::deque在头文件<deque>中定义,其声明如下:
template<
class T,
class Allocator = std::allocator<T>
> class deque;
namespace pmr {
template< class T >
using deque = std::deque<T, std::pmr::polymorphic_allocator<T>>; //C++17 起
}
其中,参数T为容器要存储的元素类型,对于T需要满足:
- 可复制赋值和可复制构造(C++11前)。
- 可擦除,即元素类型的对象能以给定的分配器(Allocator)销毁(C++11 起)。
Allocator为用于获取/释放内存及构造/析构内存中元素的分配器。
2.2 成员函数
2.2.1 元素访问
assign
assign函数的主要作用是将元素从 deque 中清除并将新的元素序列复制到目标deque。其函数声明如下:
//以count份value的副本替换内容。
void assign( size_type count, const T& value );
//以范围[first, last)中元素的副本替换内容。
template< class InputIt >
void assign( InputIt first, InputIt last );
//以来自initializer_list ilist的元素替换内容。
void assign( std::initializer_list<T> ilist ); //C++11 起
其具体用法如下:
std::deque<char> char_deque;
char_deque.assign(5, 'a');//此时char_deque = {'a', 'a', 'a', 'a', 'a'}
const std::string str(6, 'b');
char_deque.assign(str.begin(), str.end());//此时char_deque存储的元素分别为{'b', 'b', 'b', 'b', 'b', 'b'}
char_deque.assign({'C', '+', '+', '1', '1'});//此时char_deque存储的元素分别为{'C', '+', '+', '1', '1'}
at
at用于访问指定的元素,同时进行越界检查,该函数返回位于指定位置pos的元素的引用,如果pos不在容器的范围内,则抛出std::out_of_range
异常。其函数声明如下:
reference at( size_type pos );
const_reference at( size_type pos ) const;
其具体用法如下:
std::deque<int> data = {1, 2, 3};
std::cout<<data.at(1)<<std::endl; //2
data.at(1)=8; //此时data={1, 8, 3}
try{
data.at(6) = 6;
}catch(std::out_of_range const& exc){
std::cout<<exc.what()<<std::endl;
//打印输出:deque::_M_range_check: __n (which is 6)>= this->size() (which is 6)
}
operator[]
operator[]与at功能相同,即用来访问指定的元素,但其与at不同的是:operator[]不进行边界的检查。其函数声明如下所示:
reference operator[]( size_type pos ); const_reference operator[]( size_type pos ) const;
front
front用于访问容器的第一个元素,其返回值为容器首元素的引用,其函数原型如下:
reference front();
const_reference front() const;
back
back主要功能是用来访问容器最后一个元素,其返回值为容器最后一个元素的引用,其函数原型如下所示:
reference back();
const_reference back() const;
2.2.2 迭代器
begin、end和cbegin、cend
begin和cbegin返回指向deque首元素的迭代器,end和cend返回指向deque末元素后一元素的迭代器。其函数声明如下:
iterator begin(); //C++11 前
iterator begin() noexcept; //C++11 起
const_iterator begin() const; //C++11 前
const_iterator begin() const noexcept; //C++11 起
const_iterator cbegin() const noexcept; //C++11 起
iterator end(); //C++11 前
iterator end() noexcept; //C++11 起
const_iterator end() const; //C++11 前
const_iterator end() const noexcept; //C++11 起
const_iterator cend() const noexcept; //C++11 起
如果deque为空,则返回的迭代器将等于end或cend。end和cend指向deque末元素后一元素的迭代器,该元素的表现为占位符,试图访问它将导致未定义行为。
rbegin、rend和crbegin、crend
rbegin和crbegin返回指向deque首元素的逆向迭代器。它对应非逆向deque的末元素,若deque为空,则返回的迭代器等于rend或crend。rend和crend返回指向逆向deque末元素后一元素的逆向迭代器,它对应非逆向deque首元素的前一元素,此元素表现为占位符,试图访问它导致未定义行为。它们的声明如下:
reverse_iterator rbegin(); //C++11 前
reverse_iterator rbegin() noexcept; //C++11 起
const_reverse_iterator rbegin() const; //C++11 前
const_reverse_iterator rbegin() const noexcept; //C++11 起
const_reverse_iterator crbegin() const noexcept; //C++11 起
reverse_iterator rend(); //C++11 前
reverse_iterator rend() noexcept; //C++11 起
const_reverse_iterator rend() const; //C++11 前
const_reverse_iterator rend() const noexcept; //C++11 起
const_reverse_iterator crend() const noexcept; //C++11 起
2.2.3 容量
empty
empty用来检查容器是否为空,若为空则返回true,否则为false。其函数声明如下:
bool empty() const; //C++11 前
bool empty() const noexcept; //C++11 起, C++20 前
[[nodiscard]] bool empty() const noexcept; //C++20 起
其底层实现就是检查容器是否无元素,即判断是否begin() == end()
。
size
size函数返回容器中元素数量,即std::distance(begin(), end())
。其函数声明如下:
size_type size() const; //C++11 前
size_type size() const noexcept; //C++11 起
max_size
max_size函数返回根据系统或库实现限制的容器可保有的元素最大数量,此值通常反映容器大小上的理论极限,运行时,可用 RAM 总量可能会限制容器大小到小于 max_size()
的值。其函数声明为:
size_type max_size() const; //C++11 前
size_type max_size() const noexcept; //C++11 起
shrink_to_fit
shrink_to_fit函数主要是用来请求移除未使用的容量,通过释放未使用的内存来减少对内存的使用,但其是减少使用内存而不更改序列大小的非强制请求,其请求是否达成依赖于具体实现。其函数原型如下:
void shrink_to_fit();
2.2.4 修改器
clear
clear函数主要用来擦除所有元素,使用clear()
后,再次调用size()
,size函数返回0。clear函数的声明如下:
void clear(); //C++11 前
void clear() noexcept; //C++11 起
insert
insert函数主要用于插入元素到容器的指定位置,其函数原型如下所示:
//在pos前插入value,其返回值为指向被插入value的迭代器
iterator insert( const_iterator pos, const T& value );
//在pos前插入value,其返回值为指向被插入value的迭代器
iterator insert( const_iterator pos, T&& value ); //C++11 起
//在pos前插入count个value,其返回值为指向首个被插入元素的迭代器,或者在 count == 0 时返回 pos。
iterator insert( const_iterator pos, size_type count, const T& value );
//在pos前插入[first, kast)的元素,如果 first 和 last 是指向 *this 中的迭代器,那么该行为未定义。
//其返回值为指向首个被插入元素的迭代器,或者在 first == last 时返回 pos
template< class InputIt >
iterator insert( const_iterator pos, InputIt first, InputIt last );
//在pos前插入来自initializer_list ilist 的元素,其返回值为指向首个被插入元素的迭代器,或者在 ilist 为空时返回 pos。
iterator insert( const_iterator pos, std::initializer_list<T> ilist ); //C++11 起
具体用法示例如下:
std::deque<int> c1(3, 100); //初始化一个int行的双端队列c1,此时c1 = {100, 100, 100}
auto it = c1.begin();
it = c1.insert(it, 200); //在it前插入元素200
//c1 = {200,100, 100, 100}
c1.insert(it, 2, 300); //在it前插入两个元素值都为300
//c1 = {300,300,200,100, 100, 100}
// 将 it 重新指向开头
it = c1.begin();
std::deque<int> c2(2, 400); //c2 = {400, 400}
c1.insert(std::next(it, 2), c2.begin(), c2.end()); //在it后两个元素(即200)的前面插入c2
//c1 = {300,300,400,400,200,100, 100, 100}
int arr[] = {501, 502, 503};
c1.insert(c1.begin(), arr, arr + std::size(arr));
//c1 = {501,502,503,300,300,400,400,200,100, 100, 100}
c1.insert(c1.end(), {601, 602, 603});
//c1 = {501,502,503,300,300,400,400,200,100, 100, 100,601,602,603}
emplace
emplace函数的声明如下:
/*----------------------------------
pos:将构造新元素到其前的迭代器
args:转发给元素构造函数的参数
返回值iterator:指向被安置的元素的迭代器
------------------------------------*/
template< class... Args >
iterator emplace( const_iterator pos, Args&&... args ); //C++11 起
其主要作用就是原位构造元素并将其在pos前插入到容器中。
earse
earse的函数主要功能是擦除元素,其声明如下:
//移除位于pos的元素
//返回值:最后移除元素之后的迭代器。如果pos指代末元素,则返回end()迭代器
iterator erase( iterator pos ); //C++11 前
iterator erase( const_iterator pos ); //C++11 起
//移除范围[first, last)中的元素。
/*返回值:最后移除元素之后的迭代器。
如果在移除前last == end(),那么最终返回end()迭代器
如果范围[first, last) 为空,那么返回 last。*/
iterator erase( iterator first, iterator last ); //C++11 前
iterator erase( const_iterator first, const_iterator last ); //C++11 起
具体的用法如下所示:
std::deque<int> c{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
c.erase(c.begin());
//c = {1, 2, 3, 4, 5, 6, 7, 8, 9}
c.erase(c.begin() + 2, c.begin() + 5);
//c = {1, 2, 6, 7, 8, 9}
// 移除所有偶数
for (std::deque<int>::iterator it = c.begin(); it != c.end();)
{
if (*it % 2 == 0)
it = c.erase(it);
else
++it;
}
//c = {1, 7, 9}
push_back
push_back函数的主要作用是将元素添加到容器末尾,其声明如下:
void push_back( const T& value );
void push_back( T&& value ); //C++11 起
emplace_back
emplace_back函数与emplace类似,只不过是在容器末尾就地构造元素,其函数声明如下:
template< class... Args >
void emplace_back( Args&&... args ); //C++11 起, C++17 前
template< class... Args >
reference emplace_back( Args&&... args ); //C++17 起
由于emplace_back是原地构造元素,因此其插入效率要高于push_back。
pop_back
pop_back函数的主要作用就是移除末元素,其函数声明如下:
void pop_back();
如果在空容器上调用pop_back会导致未定义行为。
push_front
push_front函数的主要作用就是插入元素到容器起始位置,其函数原型如下:
void push_front( const T& value );
void push_front( T&& value ); //C++11 起
emplace_front
emplace_front函数的作用是在容器头部原位构造元素,即插入新元素到容器起始,由于其也是在容器所提供的位置原位构造函数,因此其效率也高于push_front。其函数声明为:
template< class... Args >
void emplace_front( Args&&... args ); //C++11 起, C++17 前
template< class... Args >
reference emplace_front( Args&&... args ); //C++17 起
pop_front
pop_front函数主要作用是移除容器首元素。若容器中无元素,则行为未定义。其函数声明为:
void pop_front();
resize
resize函数的主要作用是改变容器中可存储元素的个数,通过该函数可以重新设置容器大小,其函数声明如下:
/*
该函数重设容器的大小为count,在count==size()时不做任何操作。
如果当前大小大于 count,那么减小容器到它的开头 count 个元素。
如果当前大小小于 count,那么后附额外的默认插入的元素。
*/
void resize( size_type count );
/*
该函数重设容器的大小为count,在count==size()时不做任何操作。
如果当前大小大于 count,那么减小容器到它的开头 count 个元素。
如果当前大小小于 count,那么后附额外的 value 的副本
*/
void resize( size_type count, const value_type& value );
其具体用法示例如下:
std::deque<int> c = {1, 2, 3};
c.resize(5); //将其size增加大小到5
//c = {1, 2, 3, 0, 0}
c.resize(2); //将其size减少大小到2
//c = {1, 2}
c.resize(6, 4); //将其size增加大小到6,填充值为4";
//c = {1, 2, 4, 4, 4,4}
swap
swap函数的主要作用是交换两个deque容器的内容,不在单独的元素上调用任何移动、复制或交换操作。所有迭代器和引用保持有效。end()迭代器会失效。其函数声明如下:
void swap( deque& other ); //C++17 前
void swap( deque& other ) noexcept(); //C++17 起
其用法示例如下图所示:
std::deque<int> a1{1, 2, 3}, a2{4, 5};
auto it1 = std::next(a1.begin()); //*it1 = 2
auto it2 = std::next(a2.begin()); //*it2 = 5
int& ref1 = a1.front(); //ref1 = 1
int& ref2 = a2.front(); //ref1 = 4
std::cout <<*it1 << ' ' << *it2 << ' ' << ref1 << ' ' << ref2 << '\n';
//打印结果为2 5 1 4
a1.swap(a2);
//此时a1 = {4, 5},a2 = {1, 2, 3}
std::cout <<*it1 << ' ' << *it2 << ' ' << ref1 << ' ' << ref2 << '\n';
//打印结果仍为2 5 1 4
/*注:
交换后迭代器与引用保持与原来的元素关联,
例如尽管 'a1' 中值为 2 的元素被移动到 'a2' 中,
原来指向它的 it1 仍指向同一元素。*/
3. 总结
双端队列的的优劣:
优点
- 支持恒定时间内随机访问,且开销小。
- 支持快速遍历,适合线性搜索。
- 两端插入和删除性能好。
- 插入不会使指向元素的引用/指针无效。
劣势
- 如果在随机位置的插入/擦除操作占主导地位,则可能会变慢。
- 如果元素类型具有较高的复制/分配成本,则可能会变慢(重新排序元素需要复制/移动它们)。
- 对于非常大量的值,分配时间可能很长。