【深入探索 C++ STL 双端队列 】deque —— 数据时空的双端虫洞,扭曲常规操作的效率边界

STL系列专栏:

C++ STL系列__Zwy@的博客-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/bite_zwy/category_12838593.html?spm=1001.2014.3001.5482学习C++ STL的三个境界,会用,明理,能扩展,STL中的所有容器都遵循这个规律,下面我们就按照这三个境界来学习deque

目录

1、前言

2、STL标准库中的deque

2.1、 模板参数(Template Parameters)

2.2、成员类型(Member types)

2.3、成员函数(Member functions)

2.3.1、Constructor 构造函数

2.3.2、Iterators 迭代器

2.3.3、Capacity 容量操作

2.3.4、Element access 元素访问


1、前言

上期我们讲到C++ STL标准库中容器适配器stack的queue的默认底层容器都是deque,我们对deque的结构,存储空间,以及迭代器进行了简单的介绍,接下来我们会着重讲解deque的使用和底层结构,以及deque在实践中的应用场景。

温故而知新,可以为师矣!

2、STL标准库中的deque

参考文档:

cplusplus.com/reference/deque/deque/

2d53cbf8dc96413caeb431999391cd3d.png

deque又叫双端队列(Double ended queue),头文件为<deque>,deque是 C++ 标准模板库(STL)中的一个容器类,它允许在两端进行高效的插入和删除操作。

2.1、 模板参数(Template Parameters

3f858f9b4bfa45388d2b3ebb95fea682.png

模板参数T

表示元素的类型,别名为成员类型deque::value_type

模板参数Alloc

用于定义存储分配模型的分配器对象的类型。默认情况下,使用allocator类模板,它定义了最简单的内存分配模型,并且与值无关。别名为成员类型deque::allocator_type。

2.2、成员类型(Member types)

member typedefinitionnotes
value_typeThe first template parameter (T)
allocator_typeThe second template parameter (Alloc)defaults to: allocator<value_type>
referenceallocator_type::referencefor the default allocator: value_type&
const_referenceallocator_type::const_referencefor the default allocator: const value_type&
pointerallocator_type::pointerfor the default allocator: value_type*
const_pointerallocator_type::const_pointerfor the default allocator: const value_type*
iteratora random access iterator to value_typeconvertible to const_iterator
const_iteratora random access iterator to const value_type
reverse_iteratorreverse_iterator<iterator>
const_reverse_iteratorreverse_iterator<const_iterator>
difference_typea signed integral type, identical to: iterator_traits<iterator>::difference_typeusually the same as ptrdiff_t
size_typean unsigned integral type that can represent any non-negative value of difference_typeusually the same as size_t

value_type
定义:第一个模板参数(T)。
说明:这表示由分配器所处理的元素类型。例如,如果allocator用于int类型,那么value_type就是int。

allocator_type
定义:第二个模板参数(Alloc),默认情况下为allocator<value_type>。
说明:它代表分配器自身的类型。如果没有显式指定分配器类型,它会根据所处理的元素类型value_type来确定自身类型。v

reference 和 const_reference
reference 定义:对于默认分配器,是value_type&。
const_reference 定义:对于默认分配器,是const value_type&。
说明:这些类型别名用于处理对元素的引用。reference用于可修改的引用,const_reference用于常量引用。

pointer 和 const_pointer
pointer 定义:对于默认分配器,是value_type*。
const_pointer 定义:对于默认分配器,是const value_type*。
说明:它们分别用于指向元素的指针和指向常量元素的指针。

iterator 和 const_iterator
iterator 定义:一个指向value_type的随机访问迭代器,可转换为const_iterator。
const_iterator 定义:一个指向const value_type的随机访问迭代器。
说明:这些迭代器用于遍历由分配器管理的元素序列。const_iterator用于只读操作,而iterator可以用于读写操作。

reverse_iterator 和 const_reverse_iterator
reverse_iterator 定义:reverse_iterator<iterator>。
const_reverse_iterator 定义:reverse_iterator<const_iterator>。
说明:这些是反向迭代器,用于反向遍历元素序列。

difference_type
定义:一个有符号整数类型,等同于iterator_traits<iterator>::difference_type,通常与ptrdiff_t相同。
说明:用于表示迭代器之间的距离,例如在计算两个迭代器之间相差多少个元素时使用。

size_type
定义:一个无符号整数类型,能够表示difference_type的任何非负值,通常与size_t相同。
说明:用于表示容器的大小,例如元素的数量等。
这些成员类型在实现自定义分配器或理解标准库容器如何与分配器协同工作时非常重要。

2.3、成员函数(Member functions)

2.3.1、Constructor 构造函数

893da815103f47b8aea92fcdbd7a3976.png

Ⅰ、默认构造函数(default)

它接受一个可选的分配器参数alloc,如果没有提供,将使用默认的分配器构造空的deque

void TestConstructor()
{
	//默认构造,构造空的deque
	deque<int> deq_int;
	deque<string> deq2_str;
	deque<double> deq3_dou;
}
Ⅱ、填充构造函数(fill

构造含n个默认初始化元素的deque

void TestConstructor()
{
	//填充构造 构造含n个默认初始化元素的deque
	//含5个int 的默认值0的deque 
	deque<int> deq_int(5);
	//含5个stirng 的默认构造 "" 空串的deque 
	deque<string> deq_str(3);
	//含5个double 的默认值 0.000000的deque 
	deque<double> deq_dou(7);
}

构造含n个value元素的deque

void TestConstructor()
{
	//5个6
	deque<int> deq_i(5, 6);
	//3个hello world
	deque<string> deq_s(3, "hello world");
	//7个3.14
	deque<double> deq_d(7, 3.14);
}
Ⅲ、迭代器区间构造函数(range

构造一个包含与迭代器区间[first,last)范围相同数量元素的容器,每个元素以相同的顺序从该范围内的相应元素构造

void TestConstructor()
{
	//vector的迭代器去区间构造
	vector<int> v{ 1,2,3,4,5 };
	deque<int> deq_v(v.begin(), v.end());
	//list的迭代器去区间构造
	list<string> l{ "ab","bc","cd","de"};
	deque<string> deq_l(l.begin(), l.end());
	//string的迭代器去区间构造
	string s("helloworld");
	deque<char> deq_s(s.begin(), s.end());
}
n(), s.end());
Ⅳ、拷贝构造函数(copy

用被拷贝的deque对象中每个元素的副本按相同顺序构造一个容器。拷贝构造后两个容器的元素完全相同。

void TestConstructor()
{
	//vector的迭代器去区间构造deq_v
	vector<int> v{ 1,2,3,4,5 };
	deque<int> deq_v(v.begin(), v.end());
	
	//deq_cp拷贝构造
	deque<int> deq_cp(deq_v);
}
Ⅴ、移动构造函数(move

移动构造会使用来构造的deque对象的资源被move转移,此时该对象会变成空容器。

void TestConstructor()
{
	//vector的迭代器去区间构造deq_v
	vector<int> v{ 1,2,3,4,5 };
	deque<int> deq_v(v.begin(), v.end());
	
	//deq_mv移动构造
	deque<int> deq_mv(move(deq_v));
}
Ⅵ、初始化列表构造函数(initializer list

deque同样支持C++11提出的initializer list构造

void TestConstructor()
{
	//初始化列表构造函数
	deque<int> deq_i{ 1,2,3,4,5 };
	deque<string> deq_s{ "aa","bb","cc","dd" };
	deque<double> deq_d{ 1.1,2,2,3.14,5.28 };
}

2.3.2、Iterators 迭代器

deque(双端队列)的迭代器属于随机访问迭代器类型

随机访问,可以像指针一样进行算术运算

支持所有迭代器操作,除了随机访问操作外,还支持其他迭代器操作,如++(前缀和后缀)、--(前缀和后缀)、== != <= >=等比较操作。

随机访问迭代器的操作通常具有常数时间复杂度,这使得在deque这样的容器上进行元素访问和操作非常高效。

47a8aa8a5b37475e9b8592a573ad0137.png

Ⅰ、begin()

9a8f09d0d04a4de0b8780d8c9263c349.png

begin()返回指向deque容器中第一个元素的迭代器

Ⅱ、end()

c0d7aace1e934428923ec7aad7c396ca.png

end()迭代器返回的是指向deque容器中 “尾后元素”(past - the - end element)的迭代器。这个 “尾后元素” 是一个理论上存在的元素,它并不实际存在于deque容器中,位于容器最后一个元素之后。它不指向任何元素,因此不能被解引用。

如果容器为空,该函数返回与deque::begin相同的结果。

正向迭代器使用示例:

void TestIterator()
{
	//初始化列表构造函数
	deque<int> deq{ 1,2,3,4,5 };
	deque<int>::iterator it = deq.begin();
	while (it != deq.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

输出:

f1a6875c0fc642a781c1be2c39c3ba90.png

Ⅲ、rbegin()

a7d2cd17be5c46ec8df4f33114202c1e.png

返回一个指向容器中最后一个元素的反向迭代器(即容器的反向起始)。rbegin指向end所指向的元素的前一个。

Ⅳ、rend()

807b6c9278fe4a75b4e9816a2f22a057.png

返回一个反向迭代器,指向deque容器中第一个元素之前的理论元素(该元素被认为是其反向端),即指向begin()的前一个元素,该元素实际上不存在

反向迭代器使用示例:

void TestIterator()
{
	//初始化列表构造函数
	deque<int> deq{1,2,3,4,5};
	deque<int>::reverse_iterator it = deq.rbegin();
	while (it != deq.rend())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

输出:

956ab1b4699e4eb99a08faa64259401e.png

Ⅴ、const_iterators

如下几个迭代器是const_iterator,与普通迭代器用法相同,但是只可读不可写,通常用于遍历容器

7efeb0527a984d0891d3e798dd67ccb2.png

2.3.3、Capacity 容量操作

bd4a00cf5e75461998066d486861557f.png

Ⅰ、size()

返回当前deque容器中有效元素的个数 

fcf369d252384f76a6e8bc93c2c9fe2d.png

Ⅱ、max_size() 

返回最大尺寸,返回deque容器可以容纳的最大元素个数

42fa3c930a344795823a7d1b10b85801.png

Ⅲ、 resize()

调整容器的size,使其包含n个元素。

如果n小于当前容器的size,则容器将被减少到前n个元素,删除超出的元素(并销毁它们)。

如果n大于当前容器的size,则通过在末尾插入所需的元素来扩展内容,以达到n的大小。如果指定了val,则将新元素初始化为val的副本,否则将其进行值初始化(默认初始值)。

544ef30ff3ab43f99d256a7c18ffa8bb.png

示例:

void Testresize()
{
	deque<int> mydeque{ 1,2,3,4,5,6 };
	deque<int>::iterator it;
	//n小于size的情况
	mydeque.resize(5);
	//n大于size且没有value情况
	mydeque.resize(8, 100);
	//n大于size且指定value情况
	mydeque.resize(12);
	cout << "mydeque contains:" << endl;
	for (it = mydeque.begin(); it != mydeque.end(); ++it)
		cout << " " << *it;
	cout << endl;
}

输出:

896a8903239e4d43b7aa21e051324392.png

Ⅳ、empty()

判断容器是否为空,为空返回true,否则返回false

3d39c14e7f844ec79f22f8a71c11c994.png

Ⅵ、shrink_to_fit() 
eb04083c4f7948a7b131e9cca5f326a4.png

请求容器减少其内存使用以适应其大小。deque容器所分配的内存可能比存储其当前元素所需的内存多:这是因为大多数库将deque实现为动态数组,它可以保留已删除元素的已分配空间,或者提前分配额外的容量以允许更快的插入操作

2.3.4Element access 元素访问

dc1ea37903d143e182656f90b581840d.png

Ⅰ、operator[ ]

通过下标访问deque容器中的元素,类似vector的operator[ ],返回下标为n的元素的引用,下标从0开始。operattor[ ]不会强制检查,如果访问的元素的下标超出deuqe对象的范围,则会导致未定义的错误,不同的编译器的处理方式不同!

e88837495da4400886fb2490b4ab17bc.png

正常访问: 

void Test()
{
	deque<int> deq{ 1,2,3,4,5 };
	for (size_t i = 0; i < deq.size(); ++i)
	{
		cout << deq[i] << " ";
	}
	cout << endl;
}

输出: 

cc10b17e06a14f17880e953ffc93ec40.png

越界访问:

void Test()
{
	deque<int> deq{ 1,2,3,4,5 };
	for (size_t i = 0; i < 10; ++i)
	{
		cout << deq[i] << " ";
	}
	cout << endl;
}

vs下的处理方式:

调试断言失败,程序崩溃,试图访问超出容器范围的下标会导致错误。

d1aac2e91e86490eafa4a9013c7e77ff.png

Linux下g++的处理方式:

代码编译通过,程序正常运行,但是输出结果错误

输出:

89754d2edb6b427f846898ed930c0b80.png

Ⅱ、at()

访问元素,返回对deque容器对象中下标为n的元素的引用。

该函数自动检查n是否在容器中有效元素的范围内,如果不在,则抛出out_of_range异常(即,如果n大于或等于其大小)。这与成员操作符[]相反,它不检查边界。

正常访问:

void Test()
{
	deque<int> deq{ 1,2,3,4,5 };
	for (size_t i = 0; i < deq.size(); ++i)
		cout << deq.at(i) << " ";
	cout << endl;
}

输出:

越界访问:会强制检查并且抛出异常

void Test()
{
	deque<int> deq{ 1,2,3,4,5 };
	//会抛出out_of_range异常,因为已经越界,捕获异常
	try
	{
		cout << deq.at(10) << endl;
	}
	catch (const out_of_range& e)
	{
		//打印异常信息
		cerr << "Caught out_of_range exception: " << e.what() << endl;
	}
}

vs下捕获到的异常信息: 

Linux下g++捕获到的异常信息:

在使用at函数访问deque容器中的元素时,如果出现了越界访问,但是没有捕获异常,那么就会导致程序崩溃!

Ⅲ、front()

返回对deque容器中第一个元素的引用。在空容器上调用此函数会导致未定义行为。与operator[]相同,vs下程序崩溃,Linux下g++编译通过,程序输出结果错误!

Ⅳ、back()

访问最后一个元素,返回对容器中最后一个元素的引用。在空容器上调用此函数会导致未定义行为。同front()

示例: 

void Test()
{
	deque<int> deq{ 1,2,3,4,5 };
	cout << "front():" << deq.front() << endl;
	cout << "back():" << deq.back() << endl;
}

输出:

2.3.5、Modifiers 增删查改

Ⅰ、push_back()

在末尾添加元素,在deque容器的最后一个当前元素之后,在其末尾添加一个新元素

Ⅱ、push_front()

在开头插入元素,在deque容器的开头,即当前第一个元素的正前方插入一个新元素

示例:

void Test()
{
	deque<int> deq;
	deq.push_back(100);
	deq.push_front(1);
	//对空容器插入后访问
		cout << "front():" << deq.front() << endl;
		cout << "back():" << deq.back() << endl;
}

输出:

Ⅲ、pop_back()

删除deque容器中的最后一个元素,有效地将容器大小减少一个。

Ⅳ、pop_front() 

删除deque容器中的第一个元素,有效地将其大小减少一个。

示例:

void Test()
{
	deque<int> deq{ 1,2,3,4,5 };
	cout << "初始front():" << deq.front() <<  " "<<"初始back():"<< deq.back() << endl;
	deq.pop_back();
	deq.pop_front();
	cout << endl;
	cout << "当前front():" << deq.front() <<" " << "当前back():" << deq.back() << endl;
}

输出:

Ⅴ、insert()

插入元素,通过在指定位置的元素前面插入新元素来扩展deque容器。插入的位置是一个元素的迭代器

形参决定插入多少个元素以及将元素初始化为哪些值。

返回值,指向第一个新插入元素的迭代器。

示例:

void Test()
{
	vector<int> v{ 6,6,6 };
	deque<int> deq{ 1,2,3,4,5 };
	//在position位置插入value
	deq.insert(deq.begin(), -100);
	//在position位置插入n个value
	deq.insert(deq.begin() + 2, 3, 100);
	//在position位置插入一段迭代器区间
	deq.insert(deq.end(), v.begin(), v.end());
	for (size_t i = 0; i < deq.size(); ++i)
		cout << deq[i] << " ";
	cout << endl;
}

输出:

Ⅵ、erase()

删除元素,从deque容器中移除单个元素或一系列元素。

参数,单个元素的迭代器或者一系列元素的迭代器区间【first,last】

返回值,指向被函数调用删除的最后一个元素后面元素的迭代器,如果删除了容器中的最后一个元素,则返回end迭代器。

示例:

void Test()
{
	deque<int> deq{ 1,2,3,4,5,6,7,8,9,10 };
	//移除单个元素
	deq.erase(deq.begin() + 4);
	deq.erase(deq.end() - 2);
	//移除一段迭代器区间
	deq.erase(deq.begin() + 3, deq.end() - 2);
	for (size_t i = 0; i < deq.size(); ++i)
		cout << deq[i] << " ";
	cout << endl;
}

输出:

Ⅶ、swap()

交换两个deque容器的内容

示例:

void Test()
{
	deque<int> deq1{ 1,2,3,4,5 };
	deque<int> deq2{ 6,7,8,9,10 };
	deq1.swap(deq2);
	cout << "deq1:" << " ";
	for (size_t i = 0; i < deq1.size(); ++i)
		cout << deq1[i] << " ";
	cout << endl;
	cout << "deq2:" << " ";
	for (size_t i = 0; i < deq1.size(); ++i)
		cout << deq2[i] << " ";
}

输出:

Ⅷ、clear

从deque中移除所有元素,使容器的大小为0。

示例:

void Test()
{
	deque<int> deq{1,2,3,4,5 };
	deq.clear();
	cout << "size():" << deq.size() << endl;
	if (deq.empty())
		cout << "deq容器已被清空" << endl;
}

输出:

Ⅸ、emplace系列接口

emplace系列接口涉及C++11的可变参数模板,以及右值引用的移动语义,这里我只进行简单的使用示例。具体原理请移步下面这篇博文:

深入探索C++11 第三弹:C++11完结,迈进高效编程的新纪元-CSDN博客

示例:

void Test()
{
	deque<pair<string,string>> deq;
	deq.emplace(deq.begin(), "apple", "苹果"); 
	//deq.emplace_front("apple", "苹果");
	deq.emplace(deq.end(), "banana", "香蕉"); 
	//deq.emplace_back("apple", "苹果");
	cout << deq.front().first <<" :"<<deq.front().second << endl;
	cout << deq.back().first <<" :"<< deq.back().second << endl;
}

输出:

3、deque的结构

deque作为STL标准库中stack和queue的底层容器,是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比较高。它的元素在内存中不是连续存储的,而是由多个固定大小的数组块组成,这些块在逻辑上是连续的。

3.1、deque容器的结构

deque 并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际 deque 类似于一个

动态的二维数组 ,其底层结构如下图所示:

多个缓冲区的组合:deque内部维护了一个指向这些缓冲区的指针数组(通常称为映射,map)。每个缓冲区是一段连续的内存空间,用于存储deque中的元素。当需要存储的元素数量增加时,deque会动态分配新的缓冲区,并将这些缓冲区通过指针数组组织起来

元素存储方式:元素在这些缓冲区中按照顺序存储,就像它们在一个连续的内存空间中一样。例如,如果一个deque中有 10 个元素,可能会存储在两个缓冲区中,第一个缓冲区存储前几个元素,第二个缓冲区存储剩下的元素,而从用户的角度来看,这些元素是连续排列的。

3.2 、deque的迭代器结构

双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其  整体连续  以及随机访问的假象,落在了 deque 的迭代器身上, 因此deque 的迭代器设计就比较复杂,如下图所示

 那deque是如何借助其迭代器维护其假想连续的结构呢? 通过对迭代器的特殊封装:

deque的迭代器是一个相对复杂的结构。它不仅要能够指向当前元素,还需要知道所在的缓冲区以及在该缓冲区中的位置。
deque的迭代器包含多个成员,用于记录其在deque底层结构中的位置信息。它主要包含一个指向当前元素的指针(cur)、一个指向所在缓冲区起始位置的指针(first)和一个指向所在缓冲区结束位置的指针(last),以及一个指向deque中缓冲区数组(map)的指针(node)。
当迭代器被初始化时,这些指针会根据deque的初始状态进行设置。例如,当迭代器指向deque的第一个元素时,cur指向第一个缓冲区(如果只有一个缓冲区)的第一个元素,first也指向这个缓冲区的第一个元素,last指向这个缓冲区的最后一个元素,node指向包含这个缓冲区的map中的相应位置。

3.3、迭代器的遍历

3.3.1、单个缓冲区的遍历

在单个缓冲区内的遍历
当迭代器在单个缓冲区内移动时,只要cur指针没有超出last指针的范围,就可以正常地通过递增cur指针来遍历元素。
例如,假设一个deque的某个缓冲区存储了 5 个元素,迭代器从缓冲区的第一个元素开始,每次递增cur指针,就可以访问到这个缓冲区内的后续元素,就像在普通的连续内存数组中遍历一样。其时间复杂度接近O(1),因为在单个连续的内存块内访问元素主要是通过简单的指针算术运算。

3.3.2、跨越缓冲区的遍历

跨越缓冲区的遍历
当迭代器的cur指针到达当前缓冲区的last指针位置时,就需要跨越到下一个缓冲区。此时,迭代器会通过node指针找到下一个缓冲区在map中的位置。
具体操作包括更新first、last和cur指针。first和last指针会被重新设置为指向新缓冲区的起始和结束位置,cur指针会被设置为新缓冲区的起始位置。然后,继续在新缓冲区中进行遍历。
跨越缓冲区的操作相对复杂一些,会带来一定的额外开销。不过,这种情况在整个deque的遍历过程中不是频繁发生的,除非deque中的元素分布在多个缓冲区中,且遍历过程需要频繁地在缓冲区之间切换。其时间复杂度与缓冲区的数量和元素在缓冲区之间的分布有关,但总体来说,在遍历整个deque时,由于缓冲区之间的切换操作,其随机访问性能比vector(基于单一连续内存块)要差一些。

4、relational operators  关系操作符

首先比较两个deque的第一个元素。如果第一个deque的第一个元素小于第二个deque的第一个元素,那么第一个deque小于第二个deque;如果第一个元素大于第二个元素,那么第一个deque大于第二个deque。
如果两个deque的第一个元素相等,那么继续比较第二个元素,以此类推。
如果在比较过程中,一个deque的元素已经全部比较完,而另一个deque还有剩余元素,那么元素先比较完的deque小于另一个deque。

示例:

void Test()
{
	deque<int> foo(3, 100);   // three ints with a value of 100
	deque<int> bar(2, 200);   // two ints with a value of 200
	if (foo == bar) std::cout << "foo and bar are equal\n";
	if (foo != bar) std::cout << "foo and bar are not equal\n";
	if (foo < bar) std::cout << "foo is less than bar\n";
	if (foo > bar) std::cout << "foo is greater than bar\n";
	if (foo <= bar) std::cout << "foo is less than or equal to bar\n";
	if (foo >= bar) std::cout << "foo is greater than or equal to bar\n";
}

输出: 

5、deque的迭代器失效问题

5.1、插入元素导致的迭代器失效

【C++标准:】

在deque中间插入元素当在deque中间插入元素时,会导致插入点及其之后的迭代器失效。因为插入操作可能会引起元素在缓冲区之间的重新分配,或者改变现有缓冲区中的元素排列。


在deque头部插入元素使用push_front在deque头部插入元素可能会导致所有迭代器失效。因为在头部插入元素可能会引起内部缓冲区的重新分配或者调整,改变了元素的存储布局。


在deque尾部插入元素通常情况下,使用push_back在deque尾部插入元素不会导致已有迭代器失效。但如果deque需要重新分配内存(例如达到容量上限时),所有迭代器会失效。

不同的编译器对于迭代器失效的检查不同:

【VS下的检查机制:】

Visual Studio(VS)对于代码规范性和潜在错误有着比较严格的检查机制,基于一种保守的策略来防止潜在的错误使用迭代器情况,所以VS认为只要在deque中插入元素,都会导致原来的迭代器失效。

deque中间插入元素:

void Test()
{
	deque<int> dq = { 1, 2, 3, 4, 5 };
	auto it = dq.begin();
	auto position= dq.begin() + 2;
	dq.insert(position, 10); // 在3之前插入10
	// 此时,vs认为原来所有的迭代器都失效
	//cout << *it << endl;  it已经失效,继续访问就会报错
}

deque头部插入元素:

void Test()
{
	deque<int> dq = { 1, 2, 3, 4, 5 };
	auto it = dq.begin()+3;//指向4
	dq.push_front(10); // 在1之前插入10
	// 此时,vs认为原来所有的迭代器都失效
	//cout << *it << endl;  //it已经失效,继续访问就会报错
}

在deque尾部插入元素:

void Test()
{
	deque<int> dq = { 1, 2, 3, 4, 5 };
	auto it = dq.begin()+1;//指向2
	dq.push_back(10); // 在5之后插入10,尾插10
	// 此时,vs认为原来所有的迭代器都失效
	//cout << *it << endl;  //it已经失效,继续访问就会报错
}

访问失效的迭代器导致程序崩溃

综上所述,由于deque底层缓冲区的复杂结构和迭代器的特殊封装,以及VS的严格检查机制,只要在deque的任意位置插入元素,都会导致原来所有的迭代器失效。

【Linux下g++的检查机制:】

在 Linux 下的 g++ 编译器对于迭代器失效后的访问,可能不会像某些其他编译器(如 Visual Studio)那样严格报错。当使用已经失效的deque迭代器访问元素时,这是一种未定义行为。g++ 编译器在这种情况下可能没有立即给出错误提示,而是继续执行代码,可能会出现看似 “正确” 的结果。但这并不意味着迭代器没有失效,只是 g++ 编译器没有对这种错误进行严格检查

deque的底层结构由多个缓冲区组成。在某些情况下,即使迭代器已经失效,但由于内存布局的特点和元素存储的连续性(在每个缓冲区内部),g++ 编译器可能会按照失效迭代器原来指向的内存位置继续访问,而这个位置恰好还保存着看似正确的数据。但这种情况是不可靠的,因为任何微小的改变(如重新分配缓冲区、插入更多元素等)都可能导致访问错误或程序崩溃。

deque中间插入元素:

插入位置之前的迭代器不会失效,仍然可以正常使用 

void Test()
 {
	    deque<int> dq = { 1, 2, 3, 4, 5 };
	    auto it = dq.begin() + 1;
	    auto position = dq.begin() + 3;
	    dq.insert(position, 10); // 在4之前插入10
	    // 插入位置之前的迭代器不会失效,仍然可以正常使用
	    // 输出2
		cout << *it << endl;
 }

输出:

deque头部插入元素:

此时所有的迭代器都失效了,不能正常使用,但是输出结果看起来似乎正确,实际上迭代器已经失

效,这种情况下,不要继续访问已经失效的迭代器。

void Test()
{
	deque<int> dq = { 1, 2, 3, 4, 5 };
	auto it = dq.begin() + 3;//指向4
	dq.push_front(10); // 在1之前插入10,头插10
	// 此时,原来所有的迭代器都失效
	cout << *it << endl;  //it已经失效,不要继续访问
}

输出: 

在deque的尾部插入元素:

一般不会导致已有的迭代器失效,但是如果deque需要重新分配内存的话,所有的迭代器都会失效。

void Test()
{
	deque<int> dq = { 1, 2, 3, 4, 5 };
	auto it1= dq.begin();//指向1
	auto it2= dq.begin()+1;//指向2
	auto it3 = dq.begin()+2;//指向3
	auto it4 = dq.begin()+3;//指向4
	auto it5 = dq.begin() + 4;//指向5
	dq.push_back(10); // 在5之后插入10,尾插10
	// 此时,已有的迭代器一般不会失效
	cout << *it1 << " " << *it2 << " " << *it3 << " " <<
		*it4 << " " << *it5 << endl;
}

输出: 

综上所述,Linux 下的 g++ 编译器同样存在deque迭代器失效的问题,只是在处理迭代器失效后的访问时,可能没有像其他一些编译器那样严格的报错机制,但这并不代表可以忽视迭代器失效问题。正确的做法是始终遵循 C++ 标准,在可能导致迭代器失效的操作后,正确地更新和使用迭代器。

5.2、删除操作导致的迭代器失效问题

【C++标准:】

在deque中间删除元素当从deque中间删除一个元素时,所有指向被删除元素及其之后位置的迭代器都会失效。这是因为删除元素后,deque内部的元素布局发生了变化,后续元素可能会在缓冲区之间移动或者在同一缓冲区内重新排列。

在deque头部删除元素使用pop_front删除deque头部元素会导致所有迭代器失效。因为头部元素的删除会引起内部存储结构的改变,例如缓冲区的调整等。

在deque尾部删除元素一般情况下,使用pop_back删除deque尾部元素不会导致已有迭代器失效。但如果deque在删除元素后需要重新分配内存,所有迭代器会失效。
 

【VS下的检查机制:】

对于删除元素来说,VS的检查机制也不严格,有些迭代器失效的场景也无法检测。

删除deque中间的元素,此时所有的迭代器失效,访问就会导致错误

void Test()
{
	deque<int> dq = { 1, 2, 3, 4, 5 };
	auto it = dq.begin() + 1;//指向2
	auto position = dq.begin() + 3;//指向4
	dq.erase(position); //删除deque中间的元素,迭代器失效
	cout << *it << endl;//继续访问it就会报错
}

 

删除deque的头部元素,根据C++标准,此时所有已有的迭代器失效,无法访问。但是VS并没有检测出迭代器失效的问题。因为这个是一个概率性的情况 当调用 dq.pop_front() 删除 deque 的头部元素时,deque 的容器可能会重新分配内存  但是这里是可能 如果说底层没有重新进行内存分配那么是检查不到失效的  并且 编译器其实并没有那么智能,编译器也只能在特定情况下检查到越界使用或者迭代器失效

void Test()
{
	deque<int> dq{ 1,2,3,4,5 };
	auto it = dq.begin() + 2; //3
	dq.pop_front(); //删除头部元素
	cout << *it << endl; //理论上it已经失效无法访问,但是编译器没有检查出来
}

输出:

删除尾部元素:

一般不会导致已有的迭代器失效,已有的迭代器仍然可以正常访问

void Test()
{
	deque<int> dq{ 1,2,3,4,5 };
	auto it1 = dq.begin();//指向1
	auto it2 = dq.begin() + 1;//指向2
	auto it3 = dq.begin() + 2;//指向3
	auto it4 = dq.begin() + 3;//指向4
	dq.pop_back(); //删除尾部元素
	// 此时,已有的迭代器一般不会失效
	cout << *it1 << " " << *it2 << " " << *it3 << " " <<
		*it4 << " "  << endl;
}

输出: 

【Linux下g++的检查机制:】

与插入元素相同,迭代器失效的检查不严格,也不采取措施终止程序,而是继续执行代码,可能会出现看似 “正确” 的结果。但这并不意味着迭代器没有失效,只是 g++ 编译器没有对这种错误进行严格检查。

删除中间元素:

void Test()
{
	deque<int> dq = { 1, 2, 3, 4, 5 };
	auto it = dq.begin() + 1;//指向2
	auto position = dq.begin() + 2;//指向3
	dq.erase(position); //删除deque中间的元素,删除位置之后的迭代器失效,之前的正常使用
	cout << *it << endl;//it可以正常访问
}

输出: 

删除头部元素:

void Test()
{
	deque<int> dq{ 1,2,3,4,5 };
	auto it = dq.begin() + 2; //3
	dq.pop_front(); //删除头部元素
	cout << *it << endl; //理论上it已经失效无法访问
}

理论上所有迭代器全都失效,但是仍然可以继续访问并且输出,但这并不意味着迭代器没有失效,只是 g++ 编译器没有对这种错误进行严格检查。

输出:

删除尾部元素:

一般不会导致已有的迭代器失效,已有的迭代器仍然可以正常访问

void Test()
{
	deque<int> dq{ 1,2,3,4,5 };
	auto it1 = dq.begin();//指向1
	auto it2 = dq.begin() + 1;//指向2
	auto it3 = dq.begin() + 2;//指向3
	auto it4 = dq.begin() + 3;//指向4
	dq.pop_back(); //删除尾部元素
	// 此时,已有的迭代器一般不会失效
	cout << *it1 << " " << *it2 << " " << *it3 << " " <<
		*it4 << " "  << endl;
}

输出: 

5.3、迭代器失效问题的解决

【及时更新迭代器】

插入操作后的更新,当在deque中进行插入操作时,利用insert函数的返回值来更新迭代器。insert函数会返回一个指向新插入元素的迭代器。 

void Test()
{
    std::deque<int> dq = { 1, 2, 3, 4, 5 };
    auto it = dq.begin() + 2; //3
    auto new_it = dq.insert(it, 6);
    // 此时new_it指向新插入的元素6
    // 如果想访问新插入元素之后的元素,比如原来it指向的元素(现在是新插入元素之后一个)
    if (new_it + 1 != dq.end()) 
    {
        cout << *(new_it + 1) << endl;
    }
}

输出:

删除操作后的更新,对于deque的删除操作,erase函数会返回一个迭代器,它指向被删除元素之后的元素。可以利用这个返回值来更新迭代器,以确保在删除操作后能够正确地继续访问deque中的元素。

void Test()
{
    deque<int> dq = { 1, 2, 3, 4, 5 };
    auto it = dq.begin() + 2; //3
    auto new_it = dq.erase(it);
    // 此时new_it指向原来元素4,即被删除元素3之后的元素
    cout << *new_it << endl;
}

输出:

【减少循环内的插入 / 删除操作】

尽量避免在循环中频繁地对deque进行插入或删除操作。因为这样很容易导致迭代器失效,并且难以跟踪和更新迭代器。
如果必须在循环中进行插入或删除操作,可以考虑先记录需要插入或删除的位置信息,等循环结束后再统一进行操作。


【谨慎使用迭代器范围】

当使用迭代器范围进行操作时(如在一个范围内插入或删除元素),要特别注意迭代器失效的情况。例如,在deque中,如果要删除一个范围内的元素,要考虑到删除操作后迭代器的变化以及如何正确地更新迭代器来继续后续的操作。

5.4、迭代器使用总结

如上分析迭代器失效问题,不同的编译器检查机制以及处理方式都不同,编译器也没有我们想象中那么智能,只能在特定情况下检查到越界使用或者迭代器失效这种  就是我们有时候看不到现象是很正常的,对于我们来说,我们要掌握迭代器失效的原理,以及如何避免迭代器失效的问题,严格的按照C++标准来规范的使用迭代器。

6、deque的应用场景

6.1、队列和栈的实现

队列:

deque 非常适合用于实现队列(Queue)数据结构。在队列中,元素是按照先进先出(FIFO)的原则进行操作的。push_back操作可以将元素添加到队列的尾部,而pop_front操作则可以从队列的头部移除元素。

例如,在一个任务调度系统中,任务可以被添加到队列的末尾,然后按照顺序从队列头部取出任务进行处理

void Test()
{
    deque<int> taskQueue;
    // 将任务添加到队列(模拟)
    taskQueue.push_back(1);
    taskQueue.push_back(2);
    taskQueue.push_back(3);
    // 从队列头部取出任务并处理(模拟)
    while (!taskQueue.empty()) {
        int currentTask = taskQueue.front();
        std::cout << "Processing task: " << currentTask << std::endl;
        taskQueue.pop_front();
    }
}

栈:

对于栈(Stack)数据结构,deque 也能很好地支持。栈遵循后进先出(LIFO)原则,push_back和pop_back操作可以分别用于将元素压入栈和从栈顶弹出元素。
比如在一个表达式求值的程序中,操作数和运算符可以被压入栈中,然后根据运算规则从栈顶弹出元素进行计算。

void Test()
{
    deque<int> stack;
    // 将元素压入栈
    stack.push_back(1);
    stack.push_back(2);
    stack.push_back(3);
    // 从栈顶弹出元素
    while (!stack.empty()) 
    {
        int topElement = stack.back();
        std::cout << "Popping element: " << topElement << std::endl;
        stack.pop_back();
    }
}

6.2、滑动窗口问题

在一些算法问题中,如滑动窗口算法,deque 可以用于高效地维护窗口内的元素。
例如,在一个给定的数组中,需要找到长度为k的滑动窗口内的最大值。可以使用 deque 来存储窗口内元素的索引,并且保证 deque 的头部始终是窗口内的最大值索引。

vector<int> maxSlidingWindow(const std::vector<int>& nums, int k)
{
    vector<int> result;
    deque<int> window;
    for (size_t i = 0; i < nums.size(); ++i) {
        // 当窗口头部元素不在当前窗口范围内时,弹出头部元素
        while (!window.empty() && window.front() <= i - k) {
            window.pop_front();
        }
        // 保证deque的头部始终是窗口内的最大值索引
        while (!window.empty() && nums[window.back()] <= nums[i]) {
            window.pop_back();
        }
        window.push_back(i);
        if (i >= k - 1) {
            result.push_back(nums[window.front()]);
        }
    }
    return result;
}
int main() {
    vector<int> nums = { 1, 3, -1, -3, 5, 3, 6, 7 };
    int k = 3;
    vector<int> maxValues = maxSlidingWindow(nums, k);
    for (int value : maxValues) {
        cout << value << " ";
    }
    cout << std::endl;
    return 0;
}

6.3、需要在两端频繁插入和删除元素的场景

在网络编程中,对于接收和发送网络数据包的缓冲区,deque 可以用于存储等待处理的数据包。
新收到的数据包可以使用push_back添加到缓冲区的末尾,而已经处理完的数据包可以使用pop_front从缓冲区的头部移除。这样可以方便地管理数据包的流入和流出,并且在处理过程中,如果需要在缓冲区头部或尾部插入一些控制信息或者元数据,deque 也能够很好地支持。
例如,在一个简单的网络服务器程序中,接收客户端请求并存储在 deque 中,然后从 deque 头部取出请求进行处理,处理后的响应可以添加到另一个 deque 中等待发送回客户端。

7、本文小结

deque同样是STL中十分重要的容器之一,实际应用中很多场景都能看到它的身影,深入理解其特性、操作方法、迭代器机制、内部实现以及应用场景,能够使我们在 C++ 编程中更加得心应手地应对各种数据处理和算法实现的挑战,根据实际需求灵活选择并高效运用 deque,为开发高质量、高性能的程序奠定坚实的基础。

以上关于 deque 的讲解只是我在学习与探索过程中的一些心得分享,自知难免存在局限与浅薄之处。诚如管中窥豹,仅见一斑,若有未详尽或不准确之处,还望各位大佬不吝在评论区赐教点拨,予以斧正,让我得以不断完善知识体系,在编程学习之路上更进一步,感激不尽!

在 C++ 学习的浩瀚星空中,我们都是执着的追光者,每一次成功解析一个晦涩的错误,每一回透彻理解一个精妙的算法,都是我们穿越星际迷雾的胜利。每一行精心编写的代码,都是在这片星空中留下的属于自己的光芒,这段话与诸君共勉,在C++追梦路上共同前行

创作不易,还请多多互三支持!!!关注博主,为你带来更多优质内容!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/930416.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

DevOps系统设计和技术选型

命名是一件痛苦的事情&#xff0c;除非你不想要一个好名字。 我正在做的这个管理系统叫什么合适&#xff0c;或者是什么类型的系统&#xff0c;想去想来不知所措&#xff0c;后来想想这么小的东西纠结什么&#xff0c;先从小的细节一点点来&#xff0c;能用就行&#xff0c;就用…

20241206-Windows 10下使用IDEA 2024.2.3(JDK 18.0.2.1)搭建Hadoop 3.3.6开发环境

Windows 10下使用IDEA 2024.2.3(JDK 18.0.2.1)搭建Hadoop 3.3.6开发环境 1. 配置好本地hadoop之后 2. idea 新建或导入 Maven 项目 3. 编写 pom.xml 文件: 有些版本和项目信息需要根据自己的项目进行调整 JDK 18.0.2.1 Hadoop 3.3.6 <?xml version"1.0" encod…

C#Treeview

创建一个Windows应用程序&#xff0c;在默认窗体中添加一个TreeView控件、一个ImageList控件和一个ContextMenuStrip控件&#xff0c;其中&#xff0c;TreeView控件用来显示部门结构&#xff0c;ImageList控件用来存储TreeView控件中用到的图片文件&#xff0c;ContextMenuStri…

pytorch多GPU训练教程

pytorch多GPU训练教程 文章目录 pytorch多GPU训练教程1. Torch 的两种并行化模型封装1.1 DataParallel1.2 DistributedDataParallel 2. 多GPU训练的三种架构组织方式2.2 数据不拆分&#xff0c;模型拆分&#xff08;Model Parallelism&#xff09;2.3 数据拆分&#xff0c;模型…

使用el-row和el-col混合table设计栅格化,实现表头自适应宽度,表格高度占位

演示效果&#xff1a; 如上图,由于地址信息很长,需要占多个格子,所以需要错开,若想实现这种混合效果,可以这样搭建: 页面效果&#xff1a; 代码分析: 上面使用el-row和el-col搭建表单显示 第一排三个8,第二排8和16 下面混合table实现&#xff0c;并使用border来自适应宽度…

WPS解决Word文件引入excel对象文件无法打开提示“不能启动此对象...”的问题

一、问题现象 接收到了一份 Word文件&#xff0c;里面引入了一个Excel对象文件&#xff0c;双击时候&#xff0c;wps出现卡顿&#xff0c;过一会之后弹出错误提示&#xff1a;不能启动此对象... 二、解决方法 1.点击WPS左上角图标&#xff0c;并打开右上角设置&#xff0c;萱蕚…

JAVA (Springboot) i18n国际化语言配置

JAVA i18n国际化语言配置 一、简介二、功能三、Java配置国际化步骤四、Java国际化配置工具类五、Spring Boot配置六、测试 一、简介 在Java中&#xff0c;国际化&#xff08;Internationalization&#xff0c;通常简称为i18n&#xff09;是一个过程&#xff0c;它允许应用程…

Jenkins 中自定义Build History中显示构建信息

有时候会遇到一个代码仓库下面会有多个不同的分支&#xff0c;而这写分支表示着不同的开发者在开发新的需求&#xff0c;但是这样就会出现一个问题&#xff0c;在Jenkins上进行多分支构建的时候&#xff0c;很难找到哪一个是属于自己分支构建的&#xff0c;这样的问题大家应该都…

springboot安康旅游网站的设计与实现(代码+数据库+LW)

目 录 目 录 摘 要 Abstract 第一章 绪论 1.1 研究现状 1.2 设计原则 1.3 研究内容 第二章 相关技术简介 2.1 JSP技术 2.2 Java技术 2.3 MYSQL数据库 2.4 B/S结构 2.5 Spring Boot框架 第三章 系统分析 3.1可行性分析 3.1.1技术可行性 …

asp.net core过滤器应用

筛选器类型 授权筛选器 授权过滤器是过滤器管道的第一个被执行的过滤器&#xff0c;用于系统授权。一般不会编写自定义的授权过滤器&#xff0c;而是配置授权策略或编写自定义授权策略。简单举个例子。 using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCo…

深入体验c语言中const的多种多样的用法

const是一个C语言&#xff08;ANSI C&#xff09;的关键字&#xff0c;它限定一个变量不允许被改变&#xff0c;一定程序上提高程序的安全性和可靠性。虽然这个关键字看起来简单&#xff0c;但是实际上随着它限定位置不一样&#xff0c;产生的效果也各异。 一、const作用 cons…

齐护机器人ModbusRTU RS485转TTL通信模块与ESP32 Arduino通信可Mixly的图形化编程Scratch图形化编程

齐护机器人ModbusRTU RS485-TTL通信模块 一、概念理解 Modbus协议是一种由Modicon公司&#xff08;现为施耐德电气Schneider Electric&#xff09;于1979年发表的网络通信协议&#xff0c;旨在实现可编辑逻辑控制器&#xff08;PLC&#xff09;之间的通信。 1.1 什么是Mod…

【动手学运动规划】 4.5 A*算法

我宁愿永远做我自己&#xff0c;也不愿成为别人&#xff0c;即使那个人比你更快乐。 —《成为简奥斯汀》 &#x1f3f0;代码及环境配置&#xff1a;请参考 环境配置和代码运行! 4.5.1 概述 Dijkstra算法是基于广度优先搜索策略来遍历空间内的所有节点&#xff0c;最终计算出…

一些引入依赖,提示引入方式报错的问题

背景 当我们使用gulp自动化处理文件的时候&#xff0c;难免会遇到需要按照一定条件过滤的需求&#xff0c;这里博主所遇到问题是&#xff0c;通过文件内容中是否包含 某一串字符串 决定过滤当前的文件 比如&#xff1a; 碰到文件中包含注释 * replace-note 此文件未被引用 ,那…

十二月第二周

作业题&#xff1a; 嵌套循环穷举&#xff0c;先看一道题也是今天作业题&#xff1a; 重点掌握题&#xff1a; 接下来&#xff0c;我们看一下未来要学习的内容&#xff1a;数组 数组基本用法如下&#xff1a; 扩展题&#xff1a;

PyTorch环境迁移指南

在进行深度学习研究和开发时,我们经常需要在不同计算机之间迁移PyTorch环境。无论是更换新设备还是在多台机器间协同工作,都需要确保环境配置的一致性。本文将详细介绍PyTorch环境迁移的完整流程和注意事项。 环境迁移看似简单,实则暗藏玄机。直接复制文件可能会遇到系统差异带…

QGroundControl之4-QGCCorePlugin.cc

介绍 核心控件接口 Core Plugin Interface for QGroundControl 。主要看settingsPages、analyzePages、instrumentPages 等&#xff0c;这里明显看出配置了不同类型toolbar按钮对应的页面 1.MainRootWindow.qml MainRootWindow.qml页面中使用 AppSettings.qml 2.AppSettings.…

tomcat 运行加载机制解析

tomcat 运行加载机制 从tomcat jar包的加载顺序&#xff1a; tomcat的具体运行加载 可以从 start、setclasspath、catalina文件中看出来&#xff1a; start.bat执行 去找bin目录下的catalina.bat,catalina 或去找 bin\setenv.bat以获取标准环境变量&#xff0c;然后去找bin\…

【机器学习 | 基于Lasso回归和随机森林的上海链家二手房房价预测】

文章目录 &#x1f3f3;️‍&#x1f308; 1. 导入模块&#x1f3f3;️‍&#x1f308; 2. Pandas数据处理2.1 读取数据2.2 查看数据信息2.3 去除重复数据2.4 去除缺失数据2.5 面积、价格、单价、楼层、建筑时间数据提取2.6 朝向数据处理 &#x1f3f3;️‍&#x1f308; 3. 特…

npm, yarn, pnpm之间的区别

前言 在现代化的开发中&#xff0c;一个人可能同时开发多个项目&#xff0c;安装的项目越来越多&#xff0c;所随之安装的依赖包也越来越臃肿&#xff0c;而且有时候所安装的速度也很慢&#xff0c;甚至会安装失败。 因此我们就需要去了解一下&#xff0c;我们的包管理器&#…