双端队列和C++ std::deque详解

C/C++
214
0
0
2024-01-12

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. 总结

双端队列的的优劣:

优点

  • 支持恒定时间内随机访问,且开销小。
  • 支持快速遍历,适合线性搜索。
  • 两端插入和删除性能好。
  • 插入不会使指向元素的引用/指针无效。

劣势

  • 如果在随机位置的插入/擦除操作占主导地位,则可能会变慢。
  • 如果元素类型具有较高的复制/分配成本,则可能会变慢(重新排序元素需要复制/移动它们)。
  • 对于非常大量的值,分配时间可能很长。