详解STL中stack_queue为什么选择deque作为默认容器
目录
C语言中的栈和队列
vector和list的区别
deque的底层实现逻辑
1.集vector和list之长
2.双端操作优于vector和list
deque简直是stack_queue的天选容器
为何deque无法完美取代vector和list
相比于vector
1.内存连续性:
2.扩容代价:
3.内存碎片:
相比于list
1.中间操作效率:
2.迭代器稳定性:
不存在“完美容器”,只有“场景适配”
C语言中的栈和队列
当我们对初阶数据结构有所了解之后,就知道栈的底层通常是用顺序表实现,也就是C++的vector,而队列则通常用链表实现,也就是C++的list,详细可以看看博主栈和队列的篇章链接
https://blog.csdn.net/cyzzzuzm/article/details/147747706?fromshare=blogdetail&sharetype=blogdetail&sharerId=147747706&sharerefer=PC&sharesource=cyzzzuzm&sharefrom=from_link
那为什么C++的STL中的栈和队列会选择deque作为默认容器,它相对于顺序表链表的优点又在哪里,它可以完全取代顺序表链表吗?接下来重点讲解这些。
vector和list的区别
vector优点:
1.下标随机访问快,尾插尾删效率高
2.CPU高速缓存命中率高*
vector缺点:
1.头部或中间插入删除效率低下
2.插入时空间要不断扩容,扩容有一定性能损耗*,倍数级扩容可能存在一定的空间浪费
list优点:
1.任意位置的插入删除效率都是O(1)
2.按需申请释放内存,不会存在空间大量浪费的问题
list缺点:
1.不支持下标随机访问
2.CPU高速缓存命中率低*
由此可见,vector和list其实是两个极端,也是功能的极致,一方的有点是另一方致命的缺点,那有没有什么容器能够集两者之长,优缺点折中呢?那就是我们接下来要说的deque容器。
deque的底层实现逻辑
1.集vector和list之长
deque本质上来讲是一个指针数组,数组里面的每一个指针都指向一个buffer数组的位置,也就是说,数组里面连续存放着多个分散在各个空间的buffer数组的地址。它拥有vector的优点吗?当然有,首先是下标访问效率,虽然它的底层多个buffer数组的分布是散乱的,但是遍历完第一个buffer数组之后,指针数组的访问指针就会移动到下一个位置,精准找到第二个buffer数组,然后继续遍历,实现无缝衔接遍历,相当于指针数组将多个buffer数组串联在一起成为一个超级长的数组,大多数情况下,deque的下标访问效率与vector基本一致。前面提到的,它其实就是相当于被拼接起来的一个巨长无比的连续的数组空间,因此CPU高速缓存命中率也很高。但是它的中间插入删除的效率并不高,通常是O(n)级的,这一点和vector类似。list的优点它也并非没有,按需申请释放内存,不会存在空间大量浪费的问题,这一优点类似list,当buffer数组满了之后,会在申请一个内存块,新的指针会挂在指针数组里面,每次申请的内存块大小都是固定的,因此不会有太大的浪费。
2.双端操作优于vector和list
双端操作,也就是头尾的插入和删除,deque的指针数组会在两端预留位置,要双端删除直接删掉双端的buffer数组中的数据即可,效率为O(1),要插入数据,也是插入在双端指向的buffer数组中,如果一个buffer数组满了,比如头部插入,会在开辟一个内存块,它的指针继续存放在指针数组的前端,然后把要插入数据放在新开辟的内存块里,完全不需要移动数据,效率同样O(1),零数据移动,完美降维打击vector,这是优于vector的点,而list还得申请结点(带两个指针的额外内存),内存碎片多,开销更大,而deque一次可以分配多个元素,做到“批量分配”,内存利用率更高,这是优于list的点。最坏的情况,哪怕指针数组也满了,此时可能就要异地扩容,但最多也就是把指针再拷贝到新数组里而已,而且指针占用空间极小,拷贝成本几乎可以忽略,效率极高。综上,双端操作上deque是更优于vector和list的。
deque简直是stack_queue的天选容器
对初阶数据结构有了一定了解之后,我们都知道栈的出入数据是后进先出的(LIFO),从栈顶入数据,同样也从栈顶出数据;而队列,出入数据的原则是先进先出(FIFO),从一头进再从另一头出。我们可以发现,栈和队列无论是出数据还是入数据都是从两端操作的,那么双端操作,我们是不是拥有了可以完美替代vector和list的容器?那就是deque双端队列,因此用它作为栈和队列的默认容器简直再合适不过了。
为何deque无法完美取代vector和list
首先我们知道deque兼顾两者的优点,虽全能,却也全不精,没有二者的优点那么极致
相比于vector
1.内存连续性:
vector的元素在内存中是完全连续的,而deque是由多个分散的“内存块”组成,这会造成哪些劣势呢,首先vector的随机访问是真正的O(1),直接通过首地址+偏移量计算,deque虽然也支持随机访问,但是底层实现上需要先计算元素在哪个内存块,再找块内的偏移量,实际效率略低(尤其是数据量巨大时),且vector的CPU缓存命中率更高(连续内存适合CPU预读),遍历速度通常比deque快。
2.扩容代价:
vector虽然扩容时需要整体迁移数据,代价略高,但是一旦扩容完成,后续尾插的效率极高,deque虽然不需要整体迁移数据,但是每次新增内存块时,需要维护指针数组(比如需要扩容指针数组),且小块内存的管理成本更高。
3.内存碎片:
deque有多个内存块,会导致产生更多的内存碎片,而vector是一大块连续的内存,利用率更高。
相比于list
1.中间操作效率:
list的中间插入删除操作只需要改变前后结点的指针,效率很高,纯O(1)操作,而deque的插入删除操作需要移动插入点之后的所有数据,类似于vector,时间复杂度是O(n),而且由于deque内存块分散,移动数据时需要跨越多个内存块,效率甚至比vector更低。
2.迭代器稳定性:
list的迭代器在插入删除操作后,除了被删除的迭代器,始终有效;而deque在头尾插入删除时,迭代器可能会失效(如触发指针数组扩容),中间操作则会导致大量迭代器失效,使用更麻烦。
不存在“完美容器”,只有“场景适配”
vector:适合频繁随机访问、尾插尾删为主的场景,如储存大量数据并遍历。
list:适合频繁中间插入删除的场景。
deque:适合双端频繁操作,但很少中间修改的场景,如实现栈和队列的底层容器。