STL标准库之deque
- 双端队列deque的介绍
- 双端队列的底层结构
- 双端队列的迭代器
- 双端队列的缺陷
- 为何将双端队列作为栈和队列的底层默认容器
双端队列deque的介绍
双端队列和我们常见的队列或者优先级队列不同,双端队列是一种双开口的连续空间的数据结构。双开口意味着它可以从两端进行插入和删除,同时还能够保证具有O(1)的时间复杂度。比vector的头插效率高,不需要搬移元素,比list空间利用率高,开辟的是一段段连续空间。
双端队列以其独有的空间特点,使得具备有上述优势
那么就有一个问题,它是如何做到双端进出还能达到O(1)的时间复杂度呢?
实际上,双端队列的底层并不是上图中所画的样子,它是由一段段连续的小空间拼接而成,类似于一个动态的二维数组。
双端队列的底层结构
实际上,双端队列的底层和二维数组有点相似,当开辟一小段空间后,进行头插和尾插,当头插时发现头部位置的空间已满,则会在当前空间之前再开辟一段空间,在新开辟空间的最后进行插入,如果尾插时发现尾部空间已满,则在当前位置的下面再开辟一段空间,在新空间的头部进行插入。新开辟的空间都有各自的入口地址,map就是在记录当前每一小段空间的入口地址。记录的方式是从中心向外记录,也就是先放入到map中最中心的位置,如果开辟的空间在前面就放到中心位置的前面,在后面就放到中心位置的后面。
而如果当前map已经满了,意味着需要进行扩容了,这时候的扩容就无需将所有的元素都重新拷贝一遍,而是直接创建一个新的map,将旧map中的值拷贝到新map,在将旧map释放即可。
可能有人会问这样的拷贝代价大吗?
其实可以说很小,因为map中的元素个数实际上就是小段空间的个数,小段空间的个数本身不会非常大。
这样就解释了为何双端队列能够实现头插尾插头删尾删的时间复杂度都是O(1),并且还能保证是连续空间。由于连续空间,使得缓存利用率也较高,而比list的空间利用率也要高(连续空间,无需使用指针记录下一个节点的位置)
双端队列的迭代器
由于双端队列特殊的结构,我们对其迭代器进行设计时也显得较为复杂。由于需要记录当前位置是否在当前空间头部,以及是否在当前空间的尾部,还需要记录当前位置在整个map中的位置。
可以看到,双端队列的迭代器是由四个指针组成的,其中有指向当前位置的指针,指向当前空间中头部位置的指针,指向尾部元素的指针,指向map中当前空间node节点的元素。由这四个指针共同维护着当前空间中某一个元素的位置,以防止其越界。
双端队列的缺陷
与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。
与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作为stack和queue的底层数据结构。
为何将双端队列作为栈和队列的底层默认容器
1.stack和queue不需要遍历,只需要在一端或者两端进行操作。
2.在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长时,deque不仅效率高,而且内存使用率高。
因此,使用deque可以完美的利用其有点,而避免其缺陷。