C++ Learning

第15回 STL コンテナ選び方ガイド ― どれを使えばいい?

STL には十数種類のコンテナがあり、vector / array / deque / list / set / map / priority_queue ... と並ぶと最初は圧倒されます。実は「何をしたいか」から選ぶだけでほぼ決まり、迷ったら std::vectorで 8 割の場面は足ります。本ページではコンテナの全体像と用途別フローチャート、比較表を一望できる形で整理し、それぞれの詳細ページへの入口にします。

このページで押さえること
✅ 最低限ここだけ覚える
  • 迷ったら std::vector
  • サイズ固定なら std::array
  • キー → 値なら std::map / std::unordered_map
  • 重複排除なら std::set / std::unordered_set
⭐ 余裕があれば読む
  • 両端追加 std::deque / キュー系アダプタ
  • 中間挿入・要素の安定参照が要件なら std::list
  • 常に最大値が欲しい std::priority_queue
  • ハッシュ版(unordered_*)の速度/順序のトレードオフ

1. STL コンテナの全体像

STL のコンテナは大きく 3 系統+アダプタに分けられます。まずはどの系統に属するかで当たりをつけます。

① 連続/列の系統(シーケンスコンテナ)

順番通りに値を詰める系。C の配列の進化版と思えば OK。

② キー付き/集合の系統(連想コンテナ)

キーでの検索・集合演算・重複排除など。内部はバランス木。

③ ハッシュ版(非順序連想コンテナ)

順序はないがアクセスが速い。平均 O(1)。キーが順序付けできない型でも使える。

④ コンテナアダプタ

上記コンテナを裏に敷いて、使える操作を絞ったラッパー。インターフェースがシンプルになる。

仲間分けの覚え方: 順番に並ぶ=シーケンス、キーで引く=連想、高速化優先のハッシュ版=unordered、使える操作を絞ったラッパー=アダプタ。この 4 分類で 12 個ほどのコンテナが頭に入ります。

2. 選び方フローチャート

上から順に質問に答えていくと、使うべきコンテナが 1 つに絞れます。

Q1: キーと値のペアで引きたい?
→ YES:順序が必要なら std::map、速度重視なら std::unordered_map
→ NO:Q2 へ
Q2: 同じ値を持たない集合(重複排除)?
→ YES:順序が必要なら std::set、速度重視なら std::unordered_set
→ NO:Q3 へ
Q3: 常に「最大値(または最小値)だけ」が欲しい?
→ YESstd::priority_queue
→ NO:Q4 へ
Q4: キュー/スタックとして使える操作を限定したい?
→ YES:FIFO なら std::queue、LIFO なら std::stack
→ NO:Q5 へ
Q5: 先頭(front)と末尾(back)の両方に追加する?
→ YESstd::deque
→ NO:Q6 へ
Q6: サイズがコンパイル時に決まっている?
→ YESstd::array
→ NOstd::vector(これで OK)
std::list が出てこないのは? 現代のマシンではメモリの連続性(キャッシュヒット)が圧倒的に効くため、中間挿入が多くても vector のほうが速いケースがほとんどです。list は「巨大な要素を要素ごと別領域に置きたい」「反復子/参照が絶対に無効化されないことが重要」など特殊事情のときだけ検討します。

3. 比較表(特徴・計算量)

主要コンテナの代表的な操作の計算量をまとめました。O(1) は「要素数に関係なく一定時間」、O(log n) は「要素数の対数に比例」、O(n) は「要素数に比例」。

コンテナ 系統 末尾追加 先頭追加 ランダム
アクセス v[i]
検索 順序
std::vector Sequence O(1) 償却 O(n) O(1) O(n) 挿入順
std::array Sequence ―(固定長) O(1) O(n) 挿入順
std::deque Sequence O(1) 償却 O(1) 償却 O(1) O(n) 挿入順
std::list Sequence O(1) O(1) ―(不可) O(n) 挿入順
std::set Associative O(log n) O(log n) ―(不可) O(log n) ソート
std::unordered_set Unordered O(1) 平均 O(1) 平均 ―(不可) O(1) 平均 なし
std::map Associative O(log n) O(log n) O(log n)
(キー経由)
O(log n) キーでソート
std::unordered_map Unordered O(1) 平均 O(1) 平均 O(1) 平均
(キー経由)
O(1) 平均 なし
std::priority_queue Adapter O(log n)
(push)
―(top のみ) ヒープ
表の読み方のコツ: O(1) 償却 は「ときどき再確保で重くなるけど平均は O(1)」、O(1) 平均 は「ハッシュの衝突次第で最悪 O(n) になるが普段は O(1)」の意味。実戦ではどちらも O(1) と考えて OK

4. 迷ったら vector で OK の理由

「用途ごとに最適なコンテナが分かれている」と聞くと難しそうですが、現代のマシンでは std::vector がほとんどの場面で最速です。理由は 3 つ。

  1. メモリが連続している ― CPU のキャッシュに丸ごと乗る。list のような細切れメモリより圧倒的に速い
  2. 末尾追加が O(1) 償却 ― 容量を 2 倍ずつ拡張する仕組みで、push_back のコストは平均的に小さい
  3. インデックス v[i] が O(1) ― ランダムアクセスが要る場面では他コンテナに勝つ

逆に言うと、vector を捨ててまで他コンテナに乗り換えるべきなのは、計算量クラスが変わるとき。例:

「list のほうが中間挿入が速い」は半分嘘: 計算量としては list の中間挿入が O(1) ですが、挿入位置を見つけるのに O(n) かかるため、結局 O(n)。さらに list はキャッシュ効率が悪いので、実測では vector のほうが速いことが多いです。入門段階では list は「こんなのもある」程度で十分。

5. 個別コンテナへの入口

このあとの各ページで、それぞれのコンテナを個別に掘り下げます。順に読んでも、必要なところだけつまみ読みしても OK。