C++ Learning

第19回 std::deque ― 両端に追加できる配列

std::deque(double-ended queue)は、先頭にも末尾にも O(1) で追加/削除できる動的配列です。std::vector と違って push_front が速いのが特徴で、キュー(先入れ先出し)のような使い方や履歴バッファで効きます。本章では vector との違いと使い分けを整理します。

このページで押さえること
✅ 最低限ここだけ覚える
  • push_front / push_back が両方 O(1) 償却
  • インデックスアクセス dq[i] も使える
  • pop_front / pop_back で両端を消せる
  • ほとんどの場面は vector で足り、両端操作が必要なときに deque
⭐ 余裕があれば読む
  • 内部はブロック(チャンク)の配列
  • vector と違いメモリは連続ではない
  • 再確保でポインタが無効化されにくい
  • キャッシュ効率は vector より少し悪い

1. まず触ってみる ― 両端から追加

push_frontpush_back を押して、両端にどう要素が入るかを見てください。インデックスは 0 が常に先頭(front)、size-1 が末尾(back)を指します。

▶ std::deque<int> のインタラクティブ操作

2. 基本操作と vector との違い

deque 基本C++
#include <deque> std::deque<int> dq; dq.push_back(10); // 末尾に追加 dq.push_front(5); // 先頭に追加 (vector にはない) dq.push_back(20); // dq = {5, 10, 20} dq.pop_front(); // 先頭削除 dq.pop_back(); // 末尾削除 dq[0]; // vector と同じインデックスアクセス
vector と比べると違い
// vector: 先頭追加は全要素シフトで O(n) std::vector<int> v; v.insert(v.begin(), 5); // 遅い O(n) // v に push_front はない // deque: 両端とも O(1) 償却 std::deque<int> dq; dq.push_front(5); // 高速 O(1) 償却

vector と共通のメソッド

deque だけの追加メソッド

使い勝手は vector と 8 割同じ: vector のコードを std::vectorstd::deque に置き換えるだけで大抵動きます。追加で push_front / pop_front が使える分だけ柔軟、というのが第一印象で合っています。

3. 内部構造(なぜ両端 O(1) ?)

メモリ上の deque は、小さなブロック(チャンク)の配列として管理されます。vector のように単一の連続領域ではありません。

deque の内部イメージ(ブロックサイズ 4 の例)
map (中央管理):  [ブロック0][ブロック1][ブロック2][ブロック3]
                      ↓          ↓          ↓          ↓
ブロック0 (空き 2):   [ . ][ . ][3][5]
ブロック1:            [8][10][12][20]
ブロック2:            [25][30][33][40]
ブロック3 (空き 2):   [50][60][ . ][ . ]
vector にない利点: 要素が連続領域に置かれない代わりに、拡張しても既存要素のメモリ位置が動かない(ブロック単位で確保するため)。イテレータや要素へのポインタが無効化されにくいのも deque の特徴。
キャッシュ効率は少し劣る: 連続領域の vector に比べ、ブロックを跨ぐたびにキャッシュミスが起きやすい。要素数が多くランダムアクセスが中心の用途では、vector のほうが速いことがほとんど。deque は「両端操作が必要」という明確な理由があるときに選ぶ

4. 使い所 ― vector との使い分け

deque が効くケース:
  • キュー(FIFO)として使う:末尾に追加、先頭から取り出す処理(BFS、ログ処理など)
  • 履歴バッファ:新しいものを先頭に追加、古いものを末尾から消す
  • 末尾の要素へのポインタが生き続けてほしい場面(vector だと再確保で無効化する可能性あり)
vector のほうがいいケース:
  • 末尾追加と v[i] だけで足りる(多数派)
  • 要素数が少ない or キャッシュヒット率を稼ぎたい
  • data()T* を取り出して C API に渡したい(deque は連続していないので不可)
std::queue / std::stack との関係: 「FIFO にしたい」なら std::queue(内部は deque)、「LIFO にしたい」なら std::stack(内部は deque)がより簡潔です。アダプタは第 22 回で扱います。

確認クイズ

2 問。

Q1. 「ログを末尾に追加し続け、一番古いログ(先頭)から順に処理する」を最も自然に書けるのは?

std::vector(push_back + erase(begin()))
std::list
std::deque(または std::queue アダプタ)
std::set
「末尾に追加 + 先頭から取り出す」= FIFO キュー。deque(または deque を内部に持つ std::queue)が最適。vector は先頭削除が O(n) で遅いです。

Q2. deque について正しい説明は?

内部メモリは単一の連続領域である
インデックス dq[i] で要素にアクセスできる
push_front は O(n) かかる
data()T* を取り出せる
deque はブロック(チャンク)配列なのでメモリは連続していない。そのため data() もありません。ただしインデックスアクセス dq[i] は使えるし、push_frontO(1) 償却です。