C++ STL Deque 高频面试题与答案
目录
Q1: 什么是 C++ STL 中的 deque?
Q2: deque 的内部数据结构是怎样的?
Q3: deque 主要操作的时间复杂度是多少?
Q4: deque 和 vector 有什么主要区别?
Q5: deque 和 list 有什么主要区别?
Q6: deque 的迭代器失效规则是怎样的?
Q7: 什么时候应该使用 deque?
deque
(double-ended queue,双端队列)是 C++ STL 中一个非常有用的序列容器。它允许在序列的两端(前端和后端)进行快速的插入和删除操作。
Q1: 什么是 C++ STL 中的 deque
?
A1: deque
是一个序列容器,它支持在容器的两端(前端和后端)进行高效的插入和删除操作。它也支持通过索引([]
或 .at()
)进行快速的随机访问。
它的行为就像一个 vector
和 list
的混合体,提供了类似 vector
的随机访问能力,又具备了类似 list
的两端高效插入/删除能力。
Q2: deque
的内部数据结构是怎样的?
A2: deque
的内部实现通常比 vector
复杂。它不是一块连续的内存。
相反,deque
内部通常由一个“中控器”(或称为“map”,但这里的 map 不是 std::map
)来管理。这个中控器是一个指针数组(或指针的指针),它指向多个固定大小的内存块(chunks/blocks)。
-
中控器(Map): 是一个连续的内存块,存储指向各个数据块的指针。
-
数据块(Chunks): 是实际存储元素的连续内存块。
当在 deque
的前端或后端插入元素时:
-
如果对应方向的数据块未满,元素被直接放入该块。
-
如果该数据块已满,
deque
会分配一个新的数据块,并将新元素放入,同时在中控器(map)中添加指向这个新数据块的指针。 -
如果中控器(map)本身也满了,
deque
会分配一个新的、更大的中控器,将旧中控器的内容拷贝过去,然后释放旧中控器。
这种分块的结构使得它可以在两端高效地增长,而不需要像 vector
那样移动所有元素。
Q3: deque
主要操作的时间复杂度是多少?
A3:
操作 | 时间复杂度 | 备注 |
---|---|---|
随机访问 ( | $O(1)$ | 几乎是 $O(1)$。严格来说,它需要一次中控器索引和一次数据块内偏移,但都是常数时间。 |
前端插入/删除 ( | $O(1)$ (均摊) | 均摊复杂度。在大多数情况下是 $O(1)$,但如果需要分配新数据块或扩展中控器,则会产生额外开销。 |
后端插入/删除 ( | $O(1)$ (均摊) | 同上。 |
中间插入/删除 | $O(N)$ | $N$ 是 |
Q4: deque
和 vector
有什么主要区别?
A4: 这是最常被问到的问题之一。
特性 |
|
|
---|---|---|
内存布局 | 连续内存 (所有元素在一整块内存中) | 分块的连续内存 (通过中控器管理多个数据块) |
前端插入/删除 | $O(N)$ (效率极低,需移动所有元素) | $O(1)$ (均摊,非常高效) |
后端插入/删除 | $O(1)$ (均摊) | $O(1)$ (均摊) |
中间插入/删除 | $O(N)$ | $O(N)$ (通常比 |
随机访问 | $O(1)$ (最快,一次指针偏移) | $O(1)$ (也很快,但比 |
内存重新分配 | 当容量不足时,重新分配一块更大的内存,并移动所有元素。 | 在两端添加时,只需分配新的数据块,无需移动已有元素。仅当中控器满时才需重新分配中控器。 |
迭代器 | 连续内存,是普通的指针。 | 非普通指针,是一个特殊的类,内部维护指向中控器、数据块和当前元素的指针。 |
总结:
-
如果你需要频繁在前端插入/删除,请使用
deque
。 -
如果你只需要在后端操作,并且需要最快的随机访问和绝对连续的内存(例如与 C API 交互),请使用
vector
。 -
如果你在乎内存占用的连续性,
vector
更好;deque
会产生内存碎片(多个小块)。
Q5: deque
和 list
有什么主要区别?
A5:
特性 |
|
|
---|---|---|
内存布局 | 非连续内存 (每个节点单独分配) | 分块的连续内存 |
随机访问 | $O(N)$ (不支持 | $O(1)$ (支持 |
前端插入/删除 | $O(1)$ (常数时间) | $O(1)$ (均摊常数时间) |
后端插入/删除 | $O(1)$ (常数时间) | $O(1)$ (均摊常数时间) |
中间插入/删除 | $O(1)$ (如果已有迭代器指向该位置) | $O(N)$ (效率很低) |
迭代器失效 | 非常稳定。插入操作不会使迭代器失效;删除操作仅使指向被删除元素的迭代器失效。 | 不稳定。见 Q6。 |
内存开销 | 高。每个元素都有额外的指针开销 (前驱和后继)。 | 中等。有中控器和数据块管理的开销,但分摊到每个元素上通常比 |
总结:
-
如果你需要高效的随机访问,请在
deque
和vector
中选择。 -
如果你需要频繁在中间插入/删除元素,并且希望操作后迭代器保持有效,请使用
list
。 -
如果你的需求集中在两端操作,并且也需要随机访问,
deque
是完美的选择。
Q6: deque
的迭代器失效规则是怎样的?
A6: deque
的迭代器失效规则比较复杂,介于 vector
和 list
之间:
-
在两端插入 (
push_front
,push_back
):-
迭代器:所有已存在的迭代器都失效。(因为中控器(map)可能会被重新分配和拷贝,导致迭代器内部指向中控器的指针失效)。
-
引用和指针:不会失效。(因为数据块本身没有被移动)。
-
-
在两端删除 (
pop_front
,pop_back
):-
迭代器:仅指向被删除元素的迭代器失效。
-
引用和指针:仅指向被删除元素的引用和指针失效。
-
-
在中间插入:
-
所有迭代器、引用和指针都失效。
-
-
在中间删除:
-
所有迭代器、引用和指针都失效。
-
注意: deque
在两端插入时迭代器会失效,这是它与 vector
(后端插入时,若未扩容则迭代器不失效)的一个重要区别,也是面试中容易出错的地方。
Q7: 什么时候应该使用 deque
?
A7: 当你的应用场景满足以下一个或多个条件时,应优先考虑 deque
:
-
需要频繁在容器的前端和后端进行插入和删除操作。(例如:实现一个队列或双端队列)。
-
需要支持高效的随机访问(
[]
操作)。 (这是它优于list
的地方)。 -
不希望在容器增长时(尤其是前端增长时)移动所有元素。(这是它优于
vector
的地方)。 -
可以接受非连续的内存布局。
经典用例:
-
实现一个队列(Queue):
std::queue
默认就是使用deque
作为其底层容器的。 -
实现一个滑动窗口(Sliding Window)算法:例如“求窗口中的最大值”,需要在窗口滑动时(一端出、一端进)高效操作,同时可能需要访问窗口内的元素。
-
任务调度队列。