データ構造
はじめに
プログラム = データ構造 + アルゴリズム。 ここまでCPUが命令を実行する仕組みやOSがリソースを管理する方法を学びました。しかしプログラムが処理する中心的な対象はデータです——ユーザー情報、商品一覧、ソーシャルグラフ……これらのデータをメモリ上でどう整理するかが、プログラムの速度を直接左右します。「なぜあるプログラムは数万件のデータを高速に処理できるのに、別のプログラムは数百件で動かなくなるのか?」——その答えは多くの場合、データ構造の選択にあります。
この記事で学べること
この章を学び終えると、次の力が身につきます:
- 直感的な判断力:要件を見ただけで、どのデータ構造を使うべきか頭に浮かぶ
- パフォーマンス分析の視点:ボトルネックがデータ構造の選択ミスなのか、アルゴリズムの効率の問題なのかを判断できる
- トレードオフ思考:「空間と時間のトレードオフ」を理解し、完璧なデータ構造は存在しないことを知る
- コードリーディング力:HashMap、Stack、Queueといった用語に戸惑わなくなる
- 後続学習の基礎:データベースインデックス、キャッシュシステム、検索エンジンなどの技術の土台を築く
| 章 | 内容 | コアコンセプト |
|---|---|---|
| 第1章 | 全体像 | 4大データ構造、分類基準 |
| 第2章 | 線形構造 | 配列、連結リスト、スタック、キュー |
| 第3章 | ハッシュテーブル | ハッシュ関数、衝突処理、O(1) 検索 |
| 第4章 | ツリー構造 | 二分木、ファイルシステムツリー、DOMツリー |
| 第5章 | グラフ構造 | 有向グラフ、無向グラフ、探索アルゴリズム |
| 第6章 | パフォーマンス比較 | 時間計算量、空間計算量 |
| 第7章 | 選定ガイド | シナリオ分析、意思決定フロー |
1. 全体像:データ構造とは何か?
本の整理方法を想像してみてください:
- 床に積み上げる:目的の本を探すには一冊ずつ確認——これが最も原始的な保存方法
- 番号順に本棚に並べる:直接その位置に行って取れる——これが配列
- カテゴリごとに棚を分ける:まず棚を特定してから本を探す——これがハッシュテーブル
- タイトル順に多段ラックに並べる:毎回半分ずつ除外——これがツリー
整理の仕方が違うだけで、本を探す効率は天と地ほどの差があります。データ構造とはデータの「整理方法」——データをどう保存し、どう検索し、どう変更するかを決めるものです。
すべてのデータ構造は4つの大きなカテゴリに分類できます:
| 種類 | データの関係 | 代表例 | 日常での例え |
|---|---|---|---|
| 線形構造 | 1対1、一列に並ぶ | 配列、連結リスト、スタック、キュー | 電車の車両、行列 |
| ハッシュ構造 | キー→値のマッピング | ハッシュテーブル、辞書、セット | 図書館の索引カード |
| ツリー構造 | 1対多、階層関係 | 二分木、B木、ヒープ | 家系図、フォルダ |
| グラフ構造 | 多対多、ネットワーク関係 | 有向グラフ、無向グラフ | 地下鉄路線図、ソーシャルネットワーク |
なぜこんなに多くの種類を学ぶのか?
万能なデータ構造は存在しないからです。各構造は「検索速度」「挿入速度」「メモリ使用量」の間でトレードオフを行っています。家具を運ぶのにバッグを使わないのと同じで、手紙を届けるのにトラックを使わないのと同じ——適切な道具を選べば、半分の労力で倍の成果が得られます。
2. 線形構造:最も基本的な整理方法
線形構造は最も直感的なデータ整理方法です——データが次々と並び、電車の車両のようです。しかし「どうつなぐか」と「どの端から操作するか」の違いによって、4つのバリエーションが生まれ、それぞれに得意技があります。
| Data structure | Access | Insert | Delete | Feature |
|---|---|---|---|---|
| 📊 Array | O(1) fast | O(n) slow | O(n) slow | Fixed size |
| 🔗 Linked list | O(n) slow | O(1) fast | O(1) fast | Variable size |
| 🥞 Stack | O(n) | O(1) fast | O(1) fast | One-end operations |
| 🚶 Queue | O(n) | O(1) fast | O(1) fast | Two-end operations |
2.1 配列 vs 連結リスト:まったく異なる2つの保存方式
配列と連結リストは最も基本的な2つの線形構造です。その核心的な違いはメモリレイアウトにあります:
| 比較項目 | 配列 | 連結リスト |
|---|---|---|
| メモリレイアウト | 連続したひと塊 | 散在し、ポインタでつなぐ |
| n番目へのアクセス | アドレスを直接計算、O(1) | 先頭から順に探す、O(n) |
| 中間への挿入 | 後続要素をすべて移動、O(n) | 2つのポインタを書き換えるだけ、O(1) |
| サイズ | 作成時に固定 | いつでも拡張可能 |
| 日常での例え | 番号付きロッカー | 宝探しゲームの手がかりの連鎖 |
配列と連結リスト、どちらを使うべきか?
- データ量が既知で、位置指定アクセスが頻繁 → 配列(例:成績表、ピクセル行列)
- データ量が未知で、挿入・削除が頻繁 → 連結リスト(例:プレイリスト、UNDO履歴)
- 迷ったら? → まずは配列。ほとんどのケースで、配列のキャッシュ親和性によるパフォーマンス上の利点が大きい
2.2 スタックとキュー:「ルール」を追加した線形構造
スタックとキューは本質的には配列か連結リストですが、操作方法に制限を加えたものです。機能は減ったように見えますが、その制限こそが明確な用途を生み出します:
| 構造 | ルール | 操作 | 例え | あなたのコードのどこにあるか? |
|---|---|---|---|---|
| スタック | 後入れ先出し (LIFO) | push / pop | 積み重ねた皿 | 関数コールスタック、ブラウザの戻る、Ctrl+Z のUNDO |
| キュー | 先入れ先出し (FIFO) | enqueue / dequeue | チケット購入の列 | タスクスケジューリング、メッセージキュー、印刷キュー |
「制限」があるのはむしろ良いこと?
「皿を置く」「皿を取る」の2操作しかないスタックを想像してください——順番を間違えることは絶対にありません。制限は確定性をもたらし、確定性は信頼性をもたらします。 関数コールスタックは「後入れ先出し」によって、最後に呼ばれた関数が最初に戻ることを保証しています。途中の関数に任意にアクセスできたら、プログラムは混乱してしまうでしょう。
3. ハッシュテーブル:最速の検索
線形構造の検索はどれも十分に速くありません——配列は走査に O(n) かかり、ソート済みで二分探索を使っても O(log n) です。O(1) で直接見つけられる構造はないのでしょうか?あります、それがハッシュテーブルです。
3.1 ハッシュテーブルの核心的な考え方
ハッシュテーブルの原理はとてもシンプルです:
- キーを渡す(例:"apple")
- ハッシュ関数がキーを数値に変換(例:
hash("apple") = 3) - 配列の3番目の位置に直接アクセス——走査不要、一発で到達
これは図書館の索引システムのようなものです:本棚を一列ずつ探す必要はなく、索引カードを見れば本の位置に直接たどり着けます。
3.2 ハッシュ衝突:2つのキーが衝突したら?
異なる2つのキーが同じインデックスを生成することがあります——これをハッシュ衝突と呼びます。2冊の本の索引番号が同じで、同じ位置を指しているようなものです。
| 解決方法 | 原理 | 例え |
|---|---|---|
| チェイン法 | 同じ位置に連結リストで複数の値を格納 | 同じロッカーに複数の本を入れる |
| オープンアドレス法 | 衝突したら後ろの空き位置を探す | ロッカーがいっぱいなら隣のロッカーに入れる |
3.3 ハッシュテーブルのパフォーマンス
| 操作 | 平均ケース | 最悪ケース(すべて衝突) |
|---|---|---|
| 検索 | O(1) | O(n) |
| 挿入 | O(1) | O(n) |
| 削除 | O(1) | O(n) |
いつ劣化するのか?
すべてのキーが同じインデックスにマッピングされると、ハッシュテーブルは連結リストに劣化し、すべての操作が O(n) になります。回避方法:優れたハッシュ関数の選択 + 動的リサイズ(負荷率がしきい値を超えたら拡張)。
ハッシュテーブルはあなたのコードのあらゆる場所に
- JavaScript の
{}オブジェクトとMap→ ハッシュテーブル - Python の
dict→ ハッシュテーブル - Java の
HashMap→ ハッシュテーブル - データベースのインデックス → 内部でもハッシュを使用
user["name"] や map.get("key") を書くたびに、その裏ではハッシュテーブルが働いています。
4. ツリー構造:階層関係の表現
ハッシュテーブルは検索が速いですが、データは順序付けられていません。高速な検索とデータの順序維持を両立したいなら、ツリー構造が必要です。
ツリーの核心的な特徴:各ノードは複数の「子」を持てますが、「親」は1つだけです(ルートノードを除く)。この1対多の階層関係は、現実のあらゆる場所で見られます。
4.1 二分探索木:順序付きのツリー
二分探索木にはシンプルながら強力なルールがあります:左小右大。
- 左部分木のすべての値 < ルートノード
- 右部分木のすべての値 > ルートノード
検索時、毎回の比較で半分のノードを除外でき、時間計算量は O(log n) です。数当てゲームのように——「50より大きい?小さい?」「大きい。」「75より大きい?小さい?」——毎回半分ずつ除外します。
4.2 平衡木:劣化を防ぐ
二分探索木には問題があります:データが順番に挿入されると(1, 2, 3, 4, 5)、木は一本の鎖に劣化し、検索が O(n) に戻ってしまいます。平衡木は構造を自動調整することでこの問題を回避します:
| 種類 | 平衡戦略 | 特徴 | 典型的な用途 |
|---|---|---|---|
| AVL木 | 厳密な平衡(高さの差 ≤ 1) | 検索が最速、挿入・削除はやや遅い | 検索が頻繁なシーン |
| 赤黒木 | 近似的な平衡 | 総合性能が良好 | Java TreeMap、Linuxカーネル |
| B木 | 多分岐平衡、1ノードに複数の値を格納 | ディスクI/Oを削減 | データベースインデックス |
ツリーはあなたのコードのどこにあるか?
- ファイルシステム:フォルダのネストはツリー構造
- HTML DOM:
<html>→<body>→<div>→<p>は一本のツリー - データベースインデックス:B+木により、数百万件のデータ検索がわずか3〜4回のディスク読み取りで完了
- JSON/XML:ネストされたデータ形式は本質的にツリー
5. グラフ構造:複雑な関係のネットワーク
ツリーは「1対多」の階層関係しか表現できません。しかし現実には「多対多」の関係が多くあります——あなたの友達にも友達がいて、都市間には複数の道路があります。このような任意のノード間に接続があり得る構造が、グラフです。
5.1 グラフの3つの形態
| 種類 | 特徴 | 例え | 典型的な用途 |
|---|---|---|---|
| 無向グラフ | 辺に方向がない、A→B と B→A は同じ | LINEの友達(相互) | ソーシャルネットワーク、通信ネットワーク |
| 有向グラフ | 辺に方向がある、A→B と B→A は別 | Twitterのフォロー(一方向) | Webリンク、依存関係 |
| 重み付きグラフ | 辺に重みがある(距離、コストなど) | 都市間の道路(距離付き) | 地図ナビゲーション、最短経路 |
5.2 グラフの探索
グラフの探索は線形構造より複雑です。循環(A→B→C→A)があり得るため、「訪問済み」ノードを記録する必要があります:
| 探索方法 | 戦略 | 例え | 適用シーン |
|---|---|---|---|
| BFS(幅優先探索) | まずすべての隣接ノードを訪問し、次に隣接ノードの隣接ノードへ | 水の波紋の広がり | 最短経路、階層走査 |
| DFS(深さ優先探索) | 一本道を突き当たりまで進み、行き止まりなら戻る | 迷路探索 | 経路探索、連結性判定 |
グラフの現実での応用
- 地図ナビゲーション:都市がノード、道路が辺、ナビゲーションはグラフ上の最短経路探索
- ソーシャルネットワーク:ユーザーがノード、フォロー/友達が辺、「知り合いかも」はグラフアルゴリズムによる推薦
- パッケージマネージャ:npm/pip の依存関係は有向グラフ、
npm installはグラフのトポロジカルソート
6. パフォーマンス比較:全データ構造を一枚の表で
ここまで多くのデータ構造を学びましたが、それらのパフォーマンスは実際どれくらい違うのでしょうか?以下のインタラクティブな比較で直感を養えます:
| Operation | Array | Linked list | Hash table | Tree |
|---|---|---|---|---|
| Access | O(1) | O(n) | O(1) | O(log n) |
| Search | O(n) | O(n) | O(1) | O(log n) |
| Insert | O(n) | O(1) | O(1) | O(log n) |
| Delete | O(n) | O(1) | O(1) | O(log n) |
主要パフォーマンス比較表:
| データ構造 | アクセス | 検索 | 挿入 | 削除 | 空間 |
|---|---|---|---|---|---|
| 配列 | O(1) | O(n) | O(n) | O(n) | O(n) |
| 連結リスト | O(n) | O(n) | O(1) | O(1) | O(n) |
| スタック/キュー | O(n) | O(n) | O(1) | O(1) | O(n) |
| ハッシュテーブル | — | O(1) | O(1) | O(1) | O(n) |
| 二分探索木 | — | O(log n) | O(log n) | O(log n) | O(n) |
| グラフ | — | O(V+E) | O(1) | O(E) | O(V+E) |
この表の読み方
- O(1):データ量に関わらず操作時間は一定——最速
- O(log n):データ量が2倍になっても、時間は1ステップ増えるだけ——かなり速い
- O(n):データ量が2倍になると、時間も2倍——普通
- O(V+E):ノード数と辺数に依存——グラフ特有の表現
注意:これらはすべて平均ケースです。最悪ケースでは、ハッシュテーブルは O(n) に劣化し、二分探索木も O(n) に劣化します。
7. 選定ガイド:どのデータ構造を使うべきか?
多くのデータ構造を学びましたが、実際の要件に直面したとき、どう選べばよいでしょうか?鍵は要件から出発し、自分にいくつかの質問をすることです:
- 最も頻繁な操作は何か? 検索?挿入?削除?走査?
- データ間の関係は? 1対1?1対多?多対多?
- データ量はどの程度か? 数十件と数百万件では最適な選択がまったく異なる場合がある
- 順序付けが必要か? 特定の順序でデータを走査する必要があるか
| Scenario need | Recommended structure | Time complexity |
|---|---|---|
| Random access | Array | O(1) |
| Fast lookup | Hash table | O(1) |
| Ordered lookup | Binary search tree | O(log n) |
| Frequent insertion/deletion | Linked list | O(1) |
| Undo operations | Stack | O(1) |
| Task scheduling | Queue | O(1) |
クイック意思決定フロー:
| あなたの要件 | 推奨構造 | 理由 |
|---|---|---|
| 位置指定で高速アクセス | 配列 | O(1) のランダムアクセス |
| 中間での頻繁な挿入・削除 | 連結リスト | O(1) の挿入・削除、要素の移動不要 |
| 後入れ先出し(UNDO、再帰) | スタック | LIFOセマンティクスが自然に適合 |
| 先入れ先出し(タスクキュー) | キュー | FIFOセマンティクスが自然に適合 |
| キーによる高速検索 | ハッシュテーブル | O(1) の平均検索 |
| 順序付きデータ + 高速検索 | 二分探索木 | O(log n) 検索かつ順序維持 |
| 複雑な多対多の関係 | グラフ | 任意のノード間の接続を表現可能 |
実践開発における経験則
- 80%のシーンは配列とハッシュテーブルで十分
- 順序が必要ならツリーを検討
- 関係が複雑ならグラフを検討
- 迷ったら? 最もシンプルなものから始め、パフォーマンス問題が出てから切り替える。早すぎる最適化は諸悪の根源
まとめ
データ構造はプログラムの骨格です。配列は番号付きロッカーのように、位置で取り出すのが最速;連結リストは宝探しの手がかりの連鎖のように、挿入・削除が最も柔軟;ハッシュテーブルは図書館の索引のように、名前で探すのが最速;ツリーは家系図のように、階層関係を表現しつつ順序を維持;グラフは地下鉄路線図のように、任意の複雑なネットワーク関係を表現します。最善のデータ構造はなく、最適なデータ構造があるだけ——鍵は各構造の利点とコストを理解し、実際の要件に基づいてトレードオフを行うことです。
さらに学ぶ
| テーマ | おすすめリソース |
|---|---|
| データ構造の可視化 | VisuAlgo - 様々なデータ構造とアルゴリズムのアニメーション解説 |
| アルゴリズムとデータ構造 | 『アルゴリズム図鑑』- Aditya Bhargava、図解が豊富で入門に最適 |
| 深く理解する | 『データ構造とアルゴリズム解析』- Mark Allen Weiss |
| 演習問題 | LeetCode - データ構造別に分類された練習問題 |
次のステップ
データ構造の核心的な知識を身につけました。次に学べる内容: