🎓 レベル:基礎 | 重要度:A(必須)
📎 前提:計算量とオーダー記法 | 関連:基本アルゴリズム・メモリ階層とキャッシュ
要点(BLUF)
- データ構造とは「データの持ち方」。持ち方が操作のコスト(計算量とオーダー記法)を決める。同じ「検索」でも O(1) にも O(n) にもなる。
- 配列は添字アクセスが O(1) だが挿入/削除が O(n)。連結リストは挿入/削除が O(1)(位置が分かれば)だが添字アクセスが O(n)。
- ハッシュ表は平均 O(1) で検索/挿入/削除。**木(特に平衡二分探索木)**は O(log n) で順序を保ったまま検索。用途で使い分ける。
概念 ── 持ち方がコストを決める
データを「どう並べ、どう繋ぐか」で、操作の速さが変わります。万能のデータ構造はなく、ある操作を速くすると別の操作が遅くなるのが常。だから「この処理で頻繁にやる操作は何か」を見て選びます。
仕組み ── 4つの基本構造
配列(array)
メモリ上に要素を連続して並べる。base + i × 要素サイズ で位置を即計算できるので添字アクセスが O(1)(物理メモリと論理アドレス のアドレス計算そのもの)。連続ゆえキャッシュ局所性も抜群(メモリ階層とキャッシュ)。弱点は途中への挿入/削除が O(n)(後続を全部ずらす)と、サイズ変更が苦手。
連結リスト(linked list)
各要素(ノード)が「値+次ノードへのポインタ」を持ち、メモリ上はバラバラでよい。位置が分かれば挿入/削除はポインタの繋ぎ替えだけで O(1)。弱点はi番目を得るのに先頭からn回辿る O(n)、ポインタのぶんメモリを食う、バラバラ配置でキャッシュミスが多い。
flowchart LR
subgraph A["配列(連続・添字O(1)・挿入O(n))"]
a0["要素0"]-->a1["要素1"]-->a2["要素2"]-->a3["要素3"]
end
subgraph L["連結リスト(分散・添字O(n)・挿入O(1))"]
n0["値・next"]-->n1["値・next"]-->n2["値・next"]
end
ハッシュ表(hash table)
キーをハッシュ関数で配列の添字に変換し、そこに値を置く。位置 = hash(key) mod 表サイズ。うまく散れば検索/挿入/削除が平均 O(1)。弱点は、異なるキーが同じ位置に来る衝突(チェイン法や開番地法で対処)と、最悪 O(n)、順序を保たないこと。OSのプロセステーブルやファイルのディレクトリ(ファイルシステムの構造)の高速ルックアップに使われます。
木(tree)/ 平衡二分探索木
階層構造。二分探索木は「左の子 < 親 < 右の子」を保ち、検索を木の高さぶん=バランスが取れていれば O(log n) に。平衡木(赤黒木・AVL・B木)は挿入/削除でも高さを O(log n) に保つ。ハッシュと違い順序を保てる(範囲検索・ソート順の走査ができる)。DBのインデックスやファイルシステムのメタデータ管理に使われます。
仕組み ── 操作コスト早見表
| データ構造 | 添字アクセス | 検索 | 挿入/削除 | 順序保持 |
|---|---|---|---|---|
| 配列 | O(1) | O(n)(未ソート) | O(n) | 並べれば可 |
| 連結リスト | O(n) | O(n) | O(1)(位置既知) | 可 |
| ハッシュ表 | ― | 平均O(1) | 平均O(1) | 不可 |
| 平衡二分探索木 | ― | O(log n) | O(log n) | 可 |
太字がその構造の「売り」。ハッシュは最速だが順序を捨て、木は少し遅いが順序を保つ――このトレードオフが選択の決め手です。
具体例 ── 「あるか調べる」を計測
同じ「存在確認」を、線形検索(配列)とハッシュ検索(集合)で比べます([[#対応ラボ]] の 06-02_data_structures.py、n=100万)。
実行結果(実機・値は環境で変動):
配列の添字アクセス lst[i] : 100.0 ns (O(1))
ハッシュ検索 (i in set) : 100.0 ns (平均 O(1))
線形検索 (i in list) : 6797.8 us (O(n)・末尾)
線形検索はハッシュ検索の約 67,977 倍遅い(n=1,000,000)
添字アクセスもハッシュ検索も一定時間(ns)。一方、配列を線形に探す検索はマイクロ秒オーダで、約7万倍遅い。「リストに in で含まれるか調べる」コードが遅いなら、set/dict に変えるだけで劇的に速くなる――その理由がこれです。
仕組みの直観 ── なぜこの設計か
- 配列が添字O(1)な理由:連続配置だからアドレスを計算1発で出せる(物理メモリと論理アドレス)。代償が「連続を保つための挿入コスト」。
- ハッシュがO(1)な理由:キーを直接位置に写す(探さない・計算する)。代償が衝突処理と順序の喪失、メモリの余裕。空間で時間を買う(計算量とオーダー記法)。
- 木がO(log n)な理由:各ステップで候補を半減できる(二分探索の構造化版)。順序を保つ代わりにハッシュよりは遅い。範囲検索が要るならこちら。
- 局所性が効く理由:計算量が同じでも、配列(連続)は連結リスト(分散)よりキャッシュヒット(メモリ階層とキャッシュ)が多く、実速度で勝つことが多い。理論と実機の橋。
⚠️ よくある誤解・落とし穴
- 「連結リストは配列より速い」→ 挿入/削除は速いが、走査・ランダムアクセス・キャッシュ効率で配列に負けることが多い。実務では配列系が既定。
- 「ハッシュは常に O(1)」→ 平均。悪いハッシュ関数や衝突多発で最悪 O(n)。順序も保てない。
- 「木は二分木なら O(log n)」→ 平衡していれば。偏ると(ソート済み挿入など)連結リスト化して O(n)。だから平衡木を使う。
- 「計算量さえ良ければ実装は何でも」→ 局所性・定数倍で実速度は変わる。プロファイルで確かめる(性能の評価とアムダールの法則)。
対応ラボ
cs-foundations-study/labs/06-02_data_structures.py(実行して添字/ハッシュ/線形の速度差を確認済み)。
- 確認できること:同じ「検索」がデータ構造でオーダーが変わること
関連
- コストの言語は 計算量とオーダー記法
- これらを使う探索・整列は 基本アルゴリズム
- 実速度を左右する局所性は メモリ階層とキャッシュ