C++ STL容器之双端对列std::deque

deque概要

1、定义

        在C++中,deque(双端队列)是一种容器。deque是缩写形式,表示"double-ended queue",即双向队列。deque是C++标准库提供的一种方便、高效的双向队列容器,提供了在两端进行插入和删除操作的能力,同时支持随机访问。

2、详细简介

        std::deque是双端队列,可以高效的在头尾两端插入和删除元素,在std::deque两端插入和删除并不会使其它元素的指针或引用失效。在接口上和std::vector相似。与std::vector相反,std::deque中的元素并非连续存储:典型的实现是使用一个单独分配的固定大小数组的序列。std::deque的存储空间会自动按需扩大和缩小。扩大std::deque比扩大std::vector要便宜,因为它不涉及到现有元素复制到新的内存位置。

        双端队列(Double-ended queue,缩写为Deque)是一个大小可以动态变化(Dynamic size)且可以在两端扩展或收缩的顺序容器。顺序容器中的元素按照严格的线性顺序排序。可以通过元素在序列中的位置访问对应的元素。不同的库可能会按不同的方式来实现双端队列,通常实现为某种形式的动态数组。但不管通过哪种方式,双端队列都允许通过随机迭代器直接访问各个元素,且内部的存储空间会按需求自动地扩展或收缩。容器实际分配的内存数超过容纳当前所有有效元素所需的,因为额外的内存将被未来增长的部分所使用。就因为这点,当插入元素时,容器不需要太频繁地分配内存。因此,双端队列提供了类似向量(std::vector)的功能,且不仅可以在容器末尾,还可以在容器开头高效地插入或删除元素。但是,相比向量,双端队列不保证内部的元素是按连续的存储空间存储的,因此,不允许对指针直接做偏移操作来直接访问元素。在内部,双端队列与向量的工作方式完全不同:向量使用单数组数据结构,在元素增加的过程中,需要偶尔的内存重分配,而双端队列中的元素被零散地保存在不同的存储块中,容器内部会保存一些必要的数据使得可以以恒定时间及一个统一的顺序接口直接访问任意元素。因此,双端队列的内部实现比向量的稍稍复杂一点,但这也使得它在一些特定环境下可以更高效地增长,特别是对于非常长的序列,内存重分配的代价是及其高昂的。对于大量涉及在除了起始或末尾以外的其它任意位置插入或删除元素的操作,相比列表(std::list)及正向列表(std::forward_list),deque 所表现出的性能是极差的,且操作前后的迭代器、引用的一致性较低。

3、特点

  • 双向访问:deque允许在队列的两端进行插入和删除操作。这意味着你可以在队列的头部和尾部同时执行插入和删除操作,而不仅仅限制在一端。
  • 动态大小:deque的大小是动态调整的,它可以根据需要自动增加或减少。这使得deque能够灵活地应对不同的数据量和操作需求。
  • 高效插入和删除:与vector相比,在deque的头部和尾部进行插入和删除操作的时间复杂度是常数时间O(1)。这意味着deque对于频繁的插入和删除操作非常高效。
  • 随机访问:与vector类似,deque也支持随机访问。你可以使用索引来访问deque中的元素,这使得deque在需要快速访问元素的情况下非常有用。
  • 存储连续性:deque在内部使用一系列分段的固定大小数组来存储元素。每个分段都具有连续的内存,但这些分段之间不要求连续。这种内部结构使得deque能够提供高效的双向访问和动态大小调整。

4、实现原理


deque的基本使用

1、定义与构造

#include <deque>








#include<iostream>
#include<deque>
 
template<typename T>
void printDeque(std::deque<T>& tmp_deque)
{
    for(auto iter = tmp_deque.begin(); iter != tmp_deque.end(); iter++)
    {
        std::cout << *iter <<" ";
    }
    std::cout  <<"" << std::endl;
}
 
int main()
{
    //1.默认构造
    std::deque<int> myDeque; //创建一个空的deque

    //2.带有初始元素个数和初始值的构造函数
    std::deque<int> myDeque(5,10); //创建一个包含5个值为10的元素的deque
    
    std::deque<int> tmp_deque1(6, 8);
    printDeque(tmp_deque1);

    //3. 使用迭代器范围构造函数
    std::deque<std::string> words1{"the", "frogurt", "is", "also", "cursed"};
    std::cout << "1: " << words1;
 
    // words2 == words1
    std::deque<std::string> words2(words1.begin(), words1.end());
    std::cout << "2: " << words2;


    std::deque<int> tmp_deque2;
    tmp_deque2 = {1, 2 , 3, 4, 5, 6};
    printDeque(tmp_deque2);
 
    return 0;
}

2、赋值方法

1、等号赋值

 std::deque<int> myDeque(5,10); //创建一个包含5个值为10的元素的deque
 std::deque<int> Adeque;
 Adeque = myDeque;  //使用myDeque中的元素赋值给A_deque



2、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'}

3、基本操作(增、删、查、改)

插入元素:

  • push_back(value): 在deque的尾部插入一个元素。
  • push_front(value): 在deque的头部插入一个元素。
  • insert(const_iterator pos, const T& value)   主要用于插入元素到容器的指定位置

删除元素:

  • pop_back(): 删除deque的尾部元素。
  • pop_front(): 删除deque的头部元素。

访问元素:

  • front(): 返回deque的头部元素的引用。
  • back(): 返回deque的尾部元素的引用。

判断容器是否为空:

  • empty(): 如果deque为空,则返回true;否则返回false。

获取容器的大小:

  • size(): 返回deque中元素的个数。

清空容器:

  • clear(): 删除deque中的所有元素,使其变为空。
#include <iostream>
#include <deque>

int main() {
  std::deque<int> myDeque;

  // 插入元素
  myDeque.push_back(10);
  myDeque.push_front(5);

  // 访问元素
  std::cout << "Front element: " << myDeque.front() << std::endl;
  std::cout << "Back element: " << myDeque.back() << std::endl;

  // 删除元素
  myDeque.pop_back();
  myDeque.pop_front();

  // 判断容器是否为空
  if (myDeque.empty()) {
    std::cout << "Deque is empty" << std::endl;
  } else {
    std::cout << "Deque is not empty" << std::endl;
  }

  // 获取容器的大小
  std::cout << "Deque size: " << myDeque.size() << std::endl;

  // 清空容器
  myDeque.clear();


//insert

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}




  return 0;
}

随机访问元素:

  • at(index): 返回位于指定索引位置的元素的引用,索引从0开始。
  • operator[](index): 返回位于指定索引位置的元素的引用。
    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::cout_of_range const& exc){
      std::cout<<exc.what()<<std::endl; 
      //打印输出:deque::_M_range_check: __n (which is 6)>= this->size() (which is 6)
    }
    
    
    
     std::deque<int> tmp_deque2 = {1, 2 , 3, 4, 5, 6};
              // Read Element
        std::cout << tmp_deque2.at(1) << std::endl;
        std::cout << tmp_deque2[2] << std::endl;
        std::cout << tmp_deque2.front() << std::endl;
        std::cout << tmp_deque2.back() << std::endl;
    
    
    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' 中,

deque 底层结构

  • deque(双端队列)的底层结构通常由多个固定大小的缓冲区组成,每个缓冲区是一个连续的存储块。这些缓冲区通过一个指向前一个缓冲区和一个指向后一个缓冲区的指针进行连接,形成了一个双向链表。
  • deque的内部缓冲区以分块的形式存储元素。每个缓冲区有一个固定的大小,它通常是2的幂次方,例如512、1024等。缓冲区中的元素被存储在数组中,以保持元素的连续性。
  • deque的双向链表由一个或多个缓冲区组成,每个缓冲区都包含一个指向前一个缓冲区和一个指向后一个缓冲区的指针。第一个缓冲区的指向前一个缓冲区的指针为空指针,最后一个缓冲区的指向后一个缓冲区的指针也为空指针。
  • 当需要在deque的头部或尾部插入或删除元素时,只涉及到相关缓冲区的操作,而不会涉及其他缓冲区。这种设计使得deque的插入和删除操作时间复杂度为常数级别(O(1))。

deque和vector的区别

1、内部实现

2、性能特征

3、适用场景

4、deque缺陷特点

  • deque(双端队列)在大多数情况下是非常高效且灵活的数据结构,但它也有一些缺点需要注意。
  • 相对于vector,deque的内存占用更高:deque的底层实现通常由多个固定大小的缓冲区组成。这导致deque在存储元素时可能需要更多的内存空间,相对于vector而言。
  • 不支持随机访问:尽管deque支持常数时间的插入和删除操作,但由于内部缓冲区是分块存储的,因此对于deque而言,随机访问的性能较差。在deque中进行随机访问需要根据元素的索引进行计算,而不是直接通过指针操作。
  • 迭代器的失效:如果在deque中插入或删除元素,会导致原始deque的迭代器失效。这意味着在操作之前获取的迭代器可能不再有效,需要重新获取或使用新的迭代器。
  • 对于大量元素的频繁插入和删除,效率较低:当大量元素需要在deque的中间位置插入或删除时,由于涉及到内存的重分配和数据的移动,deque的性能可能受到影响。
  • 可能不利于缓存:由于deque的底层实现包含多个不相邻的缓冲区,这可能导致在连续访问元素时,对CPU缓存的使用效率不高。
     


补充std::pair的使用方法

pair简介
        pair是一种模板类型,一个pair可以保存两个元素first、second,这两个元素没有类型的限制,可以是C++基本数据类型、结构体、类自定义类型。创建后的新元素为<first, second>。

        当一个函数需要返回2个或多个相同类型的数据的时候可以考虑使用数组,当一个函数需要返回2个不同类型、不同属性的数据的时候,可以考虑使用pair。当需要返回多个不同类型、不同属性的数据的时候可以考虑使用tuple。

pair类型定义在#include<utility>头文件中,定义如下:

类模板:

template<
    class T1,
    class T2
> struct pair;

参数:
T1是第一个值的数据类型,T2是第二个值的数据类型。

功能:
将一对元素T1、T2组合成一个新元素<T1, T2>T1T2没有类型限制,可以为不同的类型,通过pair的两个共有函数firstsecond访问T1T2

pair创建和初始化

pair对象创建时,需要指定两个类型名,类型名可以不同,例如:

pair<string, string> a;       	// 创建一个空对象a,两个元素类型都是string
pair<string, int> b;     		// 创建一个空对象 b, 两个元素类型分别是string和int类型
pair<string, vector<int> > c;  	// 创建一个空对象c,两个元素类型分别是string和vector类型

pair<int ,int >d (5,6);			//创建d对象,两个元素类型都是int,并默认初始值为5,6

pair<int ,int > e= make_pair(5,6);//直接调用make_pair函数生成pair对象e,两个元素类型都是int,并默认初始值为5,6

pair<string,double> f ("aa",5.0);//创建f对象,两个元素类型是string,double 并默认初始值为"aa", 5.0

pair <string ,double> g = make_pair("aa",5.0);//直接调用make_pair函数生成pair对象g,两个元素类型是string,double 并默认初始值为"aa", 5.0

typedef pair<string,string> h;	//若定义多个相同的pair类型对象,可以使用typedef简化声明
h h_1("aa","bb");
h h_2("cc","dd");

pair<int, double> i1(1, 1.1);	//变量间接赋值
pair<int, double> i2 = i1;     
pair<int, double> i3;
i3 = i1;   

举例:

#include <iostream>
#include<utility>
using namespace std;

int main() {
	pair<int ,double> p;
 
	p.first = 1;
	 
	p.second = 1.1;
	 
	cout<<"first value is " << p1.first << endl;
	cout<<"second value is " << p.second << endl;
	return 0;
}

输出:
first value is 1
second value is 1.1

我们使用pair的构造函数之外也可以使用make_pair函数来生成我们需要的pair对象。make_pair函数定义如下

template pair make_pair(T1 a, T2 b) { return pair(a, b); }

#include <iostream>
#include<utility>
using namespace std;

int main() {
 
	int a = 1;
 
	string b = "test";
 
	pair<int, string> p;
 
	p = make_pair(a, b);

	cout<<"first value is " << p.first << endl;
	cout<<"second value is " << p.second << endl;
	
	return 0;
}

deque和pair的组合使用


  std::deque<std::pair<std::uint_fast8_t, double>> min_A;
 
  std::deque<std::pair<std::uint_fast8_t, double>> max_A;


  if (min_A.empty()) {
    return std::numeric_limits<double>::infinity();
  } else {
    return min_A.front().second;
  }

本文针对deque相关知识进行学习和整合,仅作学习回顾,详细内容清参考下述文章。

参考:

1、https://blog.51cto.com/u_16531685/9789070

2、百度安全验证

3、20.deque双端队列_哔哩哔哩_bilibili

4、C++ -- 学习系列 std::deque 的原理与使用-CSDN博客

5、【C++】std::pair基本使用方法-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值