- Published on
データ構造完全整理2025:ArrayからTrie、B-Treeまで — 面接に出る全データ構造
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 1. Big-O記法(きほう)
- 2. 線形(せんけい)データ構造(こうぞう)
- 3. ハッシュデータ構造
- 4. ツリーデータ構造
- 5. Heap(ヒープ)
- 6. Trie(トライ)
- 7. グラフ(Graph)
- 8. 高度(こうど)なデータ構造
- 9. データ構造の時間計算量比較表(ひかくひょう)
- 10. 問題別(もんだいべつ)データ構造選択(せんたく)ガイド
- 11. 面接(めんせつ)質問(しつもん)15選(せん)
- 12. クイズ5選(せん)
- 13. 参考資料(さんこうしりょう)
1. Big-O記法(きほう)
1.1 時間計算量(じかんけいさんりょう)の概要(がいよう)
アルゴリズムの効率(こうりつ)を入力(にゅうりょく)サイズ(n)の関数(かんすう)で表現(ひょうげん)します。
O(1) — 定数時間 例: 配列インデックスアクセス
O(log n) — 対数時間 例: 二分探索
O(n) — 線形時間 例: 配列走査
O(n log n) — 線形対数 例: マージソート
O(n^2) — 二次時間 例: バブルソート
O(2^n) — 指数時間 例: フィボナッチ再帰
O(n!) — 階乗 例: 順列生成
1.2 増加率(ぞうかりつ)の比較(ひかく)
n O(1) O(log n) O(n) O(n log n) O(n^2) O(2^n)
10 1 3 10 33 100 1,024
100 1 7 100 664 10,000 1.27e30
1000 1 10 1000 9,966 1,000,000 ---
10000 1 13 10000 132,877 1億 ---
1.3 償却分析(しょうきゃくぶんせき)(Amortized Analysis)
個々(ここ)の操作(そうさ)は高(たか)コストになり得(え)ますが、連続(れんぞく)した操作の**平均(へいきん)**コストが低(ひく)い場合(ばあい)です。
動的配列のappend:
通常: O(1) — 空きスロットに追加
時々: O(n) — 配列が満杯になると2倍のサイズにコピー
n回のappend総コスト: n + n/2 + n/4 + ... ≈ 2n
償却コスト: O(2n/n) = O(1)
2. 線形(せんけい)データ構造(こうぞう)
2.1 Array(配列(はいれつ))
連続(れんぞく)したメモリ空間(くうかん)に同(おな)じ型(かた)の要素(ようそ)を格納(かくのう)します。
インデックス: 0 1 2 3 4
[10] [20] [30] [40] [50]
^ ^
arr[0] arr[4]
| 操作 | 時間計算量 |
|---|---|
| アクセス(インデックス) | O(1) |
| 探索 | O(n) |
| 挿入(末尾) | O(1) 償却 |
| 挿入(中間) | O(n) |
| 削除(末尾) | O(1) |
| 削除(中間) | O(n) |
# 動的配列の実装
class DynamicArray:
def __init__(self):
self.size = 0
self.capacity = 1
self.data = [None] * self.capacity
def append(self, value):
if self.size == self.capacity:
self._resize(2 * self.capacity)
self.data[self.size] = value
self.size += 1
def _resize(self, new_capacity):
new_data = [None] * new_capacity
for i in range(self.size):
new_data[i] = self.data[i]
self.data = new_data
self.capacity = new_capacity
def get(self, index):
if 0 <= index < self.size:
return self.data[index]
raise IndexError("Index out of range")
2.2 Linked List(連結(れんけつ)リスト)
各(かく)ノードがデータと次(つぎ)のノードへのポインタを持(も)ちます。
単方向連結リスト (Singly):
[10|->] -> [20|->] -> [30|->] -> [40|->] -> None
双方向連結リスト (Doubly):
None <- [<-|10|->] <-> [<-|20|->] <-> [<-|30|->] -> None
循環連結リスト (Circular):
[10|->] -> [20|->] -> [30|->] -> [10|->] (先頭に戻る)
| 操作 | 時間計算量 |
|---|---|
| アクセス(インデックス) | O(n) |
| 探索 | O(n) |
| 挿入(先頭) | O(1) |
| 挿入(末尾、tailポインタ) | O(1) |
| 削除(ノード既知) | O(1) |
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class LinkedList:
def __init__(self):
self.head = None
def prepend(self, val):
new_node = ListNode(val, self.head)
self.head = new_node
def delete(self, val):
dummy = ListNode(0, self.head)
prev, curr = dummy, self.head
while curr:
if curr.val == val:
prev.next = curr.next
break
prev, curr = curr, curr.next
self.head = dummy.next
def reverse(self):
prev, curr = None, self.head
while curr:
nxt = curr.next
curr.next = prev
prev = curr
curr = nxt
self.head = prev
Array vs Linked List:
| 基準 | Array | Linked List |
|---|---|---|
| アクセス | O(1) | O(n) |
| 挿入/削除(先頭) | O(n) | O(1) |
| キャッシュ効率 | 良い(連続メモリ) | 悪い(分散メモリ) |
| メモリオーバーヘッド | 低い | ポインタ分の追加 |
2.3 Stack(スタック)
LIFO(Last In, First Out)構造(こうぞう)。
push(10) -> push(20) -> push(30) -> pop()
| | | | | 30 | | 20 |
| | | 20 | | 20 | | | <- 30を返す
| 10 | | 10 | | 10 | | 10 |
+----+ +----+ +----+ +----+
- 活用(かつよう): 関数呼び出しスタック、括弧マッチング、undo/redo、DFS
# スタック — Pythonリストで実装
class Stack:
def __init__(self):
self.items = []
def push(self, item):
self.items.append(item)
def pop(self):
if self.is_empty():
raise IndexError("Stack is empty")
return self.items.pop()
def peek(self):
return self.items[-1] if self.items else None
def is_empty(self):
return len(self.items) == 0
2.4 Queue(キュー)
FIFO(First In, First Out)構造。
enqueue(10) -> enqueue(20) -> enqueue(30) -> dequeue()
Front -> Front ->
[10] [10][20] [10][20][30] [20][30] <- 10を返す
- 活用: BFS、タスクスケジューリング、メッセージキュー
2.5 Deque(デック)
両端(りょうたん)から挿入(そうにゅう)/削除(さくじょ)が可能(かのう)なキュー。
先頭から挿入/削除 <- [10][20][30] -> 末尾から挿入/削除
- Pythonの
collections.dequeは双方向連結リストベース
3. ハッシュデータ構造
3.1 Hash Table(ハッシュテーブル)
キーをハッシュ関数(かんすう)で変換(へんかん)し、バケットに格納(かくのう)します。
key: "apple" -> hash("apple") -> 3
key: "banana" -> hash("banana") -> 7
key: "cherry" -> hash("cherry") -> 3 <- 衝突!
バケット:
[0] ->
[1] ->
[2] ->
[3] -> ("apple", 100) -> ("cherry", 300) <- チェイニング
[4] ->
[5] ->
[6] ->
[7] -> ("banana", 200)
3.2 衝突(しょうとつ)解決(かいけつ)戦略(せんりゃく)
チェイニング(Chaining): 同(おな)じバケットに連結リストで格納
[3] -> ("apple", 100) -> ("cherry", 300) -> None
オープンアドレッシング(Open Addressing): 他(ほか)の空(あ)きスロットを探(さが)して格納
線形探査(Linear Probing):
hash("cherry") = 3 -> 使用中 -> 4を確認 -> 空き -> 格納
二次探査(Quadratic Probing):
hash("cherry") = 3 -> 3+1^2=4 -> 3+2^2=7 -> ...
二重ハッシュ(Double Hashing):
hash("cherry") = 3 -> 3 + hash2("cherry") -> ...
3.3 負荷率(ふかりつ)(Load Factor)とリハッシュ
Load Factor = 格納済み要素数 / バケット数
LF > 0.75 -> リハッシュ(バケット数を2倍に拡張、全要素を再配置)
class HashMap:
def __init__(self, capacity=16):
self.capacity = capacity
self.size = 0
self.buckets = [[] for _ in range(capacity)]
self.load_factor_threshold = 0.75
def _hash(self, key):
return hash(key) % self.capacity
def put(self, key, value):
if self.size / self.capacity >= self.load_factor_threshold:
self._rehash()
idx = self._hash(key)
for i, (k, v) in enumerate(self.buckets[idx]):
if k == key:
self.buckets[idx][i] = (key, value)
return
self.buckets[idx].append((key, value))
self.size += 1
def get(self, key):
idx = self._hash(key)
for k, v in self.buckets[idx]:
if k == key:
return v
raise KeyError(key)
def _rehash(self):
old_buckets = self.buckets
self.capacity *= 2
self.buckets = [[] for _ in range(self.capacity)]
self.size = 0
for bucket in old_buckets:
for key, value in bucket:
self.put(key, value)
| 操作 | 平均 | 最悪 |
|---|---|---|
| 挿入 | O(1) | O(n) |
| 検索 | O(1) | O(n) |
| 削除 | O(1) | O(n) |
4. ツリーデータ構造
4.1 Binary Search Tree(二分探索木(にぶんたんさくき))
左(ひだり)の子(こ) < 親(おや) < 右(みぎ)の子(こ)というルールに従(したが)います。
8
/ \
3 10
/ \ \
1 6 14
/ \ /
4 7 13
class TreeNode:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
class BST:
def __init__(self):
self.root = None
def insert(self, val):
self.root = self._insert(self.root, val)
def _insert(self, node, val):
if not node:
return TreeNode(val)
if val < node.val:
node.left = self._insert(node.left, val)
elif val > node.val:
node.right = self._insert(node.right, val)
return node
def search(self, val):
return self._search(self.root, val)
def _search(self, node, val):
if not node or node.val == val:
return node
if val < node.val:
return self._search(node.left, val)
return self._search(node.right, val)
def inorder(self, node, result=None):
if result is None:
result = []
if node:
self.inorder(node.left, result)
result.append(node.val)
self.inorder(node.right, result)
return result
| 操作 | 平均 | 最悪(偏り) |
|---|---|---|
| 探索 | O(log n) | O(n) |
| 挿入 | O(log n) | O(n) |
| 削除 | O(log n) | O(n) |
4.2 AVL Tree(自己均衡(じこきんこう)二分探索木)
全(すべ)てのノードの左右(さゆう)サブツリーの高(たか)さの差(さ)が最大(さいだい)1であるBSTです。
不均衡発生時に回転(Rotation)で均衡を復元:
LL回転(Right Rotation): RR回転(Left Rotation):
z z
/ \
y -> y y -> y
/ / \ \ / \
x x z x z x
LR回転: RL回転:
z z
/ \
x -> y x -> y
\ / \ / / \
y x z y z x
- 全操作 O(log n) を保証(ほしょう)
- 挿入/削除のたびに回転が必要な場合があり、若干(じゃっかん)のオーバーヘッド
4.3 Red-Black Tree(赤黒木(あかくろき))
5つのルールを満(み)たす自己均衡BSTです。
ルール:
1. 全ノードは赤または黒
2. ルートは黒
3. 全リーフ(NIL)は黒
4. 赤ノードの子は全て黒(連続する赤は不可)
5. 任意ノードから全リーフまでの黒ノード数は同一
(B:8)
/ \
(R:3) (R:10)
/ \ \
(B:1)(B:6) (B:14)
/ \ /
(R:4)(R:7)(R:13)
- JavaのTreeMap、C++のstd::mapで使用
- AVLより挿入/削除が高速(回転回数が少ない)
- AVLより探索はやや遅い(均衡度が低い)
4.4 B-Tree / B+Tree
ディスクベース格納(かくのう)に最適化(さいてきか)された自己均衡多分岐探索木(たぶんきたんさくき)です。
B-Tree(次数3):
[10, 20]
/ | \
[3,7] [12,15] [25,30,40]
B+Tree(次数3):
[10, 20]
/ | \
[3,7] [10,15] [20,25,30]
-> -> -> -> -> <- リーフノードが連結リストで接続
B-Tree vs B+Tree:
| 特性 | B-Tree | B+Tree |
|---|---|---|
| データ位置 | 全ノード | リーフノードのみ |
| リーフ接続 | なし | 連結リスト |
| 範囲クエリ | 非効率 | 効率的 |
| 使用例 | 一般インデックス | DBインデックス(MySQL InnoDB) |
4.5 Segment Tree
区間(くかん)クエリ(合計(ごうけい)、最小値(さいしょうち)、最大値(さいだいち)など)をO(log n)で処理(しょり)します。
配列: [1, 3, 5, 7, 9, 11]
区間合計 Segment Tree:
[36] (0-5)
/ \
[9] [27] (0-2), (3-5)
/ \ / \
[4] [5] [16] [11] (0-1),(2),(3-4),(5)
/ \ / \
[1] [3] [7] [9]
class SegmentTree:
def __init__(self, arr):
self.n = len(arr)
self.tree = [0] * (4 * self.n)
self.build(arr, 1, 0, self.n - 1)
def build(self, arr, node, start, end):
if start == end:
self.tree[node] = arr[start]
else:
mid = (start + end) // 2
self.build(arr, 2 * node, start, mid)
self.build(arr, 2 * node + 1, mid + 1, end)
self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]
def query(self, node, start, end, l, r):
if r < start or end < l:
return 0
if l <= start and end <= r:
return self.tree[node]
mid = (start + end) // 2
left = self.query(2 * node, start, mid, l, r)
right = self.query(2 * node + 1, mid + 1, end, l, r)
return left + right
5. Heap(ヒープ)
5.1 Min Heap / Max Heap
完全二分木(かんぜんにぶんき)で、親(おや)が子(こ)より小(ちい)さい(Min)または大(おお)きい(Max)構造です。
Min Heap: Max Heap:
1 9
/ \ / \
3 5 7 8
/ \ / \ / \ / \
7 8 9 10 3 5 4 6
配列(はいれつ)で表現:
インデックス: 0 1 2 3 4 5 6
Min: [ 1, 3, 5, 7, 8, 9, 10]
親: (i-1) // 2
左の子: 2*i + 1
右の子: 2*i + 2
5.2 Heap操作(そうさ)
class MinHeap:
def __init__(self):
self.heap = []
def push(self, val):
self.heap.append(val)
self._sift_up(len(self.heap) - 1)
def pop(self):
if not self.heap:
raise IndexError("Heap is empty")
self._swap(0, len(self.heap) - 1)
val = self.heap.pop()
if self.heap:
self._sift_down(0)
return val
def _sift_up(self, i):
parent = (i - 1) // 2
while i > 0 and self.heap[i] < self.heap[parent]:
self._swap(i, parent)
i = parent
parent = (i - 1) // 2
def _sift_down(self, i):
n = len(self.heap)
while True:
smallest = i
left = 2 * i + 1
right = 2 * i + 2
if left < n and self.heap[left] < self.heap[smallest]:
smallest = left
if right < n and self.heap[right] < self.heap[smallest]:
smallest = right
if smallest == i:
break
self._swap(i, smallest)
i = smallest
def _swap(self, i, j):
self.heap[i], self.heap[j] = self.heap[j], self.heap[i]
| 操作 | 時間計算量 |
|---|---|
| push | O(log n) |
| pop(最小値) | O(log n) |
| peek | O(1) |
| heapify(配列 → ヒープ) | O(n) |
5.3 Heap Sort
def heap_sort(arr):
import heapq
heapq.heapify(arr)
return [heapq.heappop(arr) for _ in range(len(arr))]
- 時間計算量: O(n log n)
- 空間計算量: O(1)(インプレースソート可能)
- 活用: 優先度キュー、Top-K問題、ダイクストラアルゴリズム
6. Trie(トライ)
6.1 Trieの構造
文字列(もじれつ)検索(けんさく)に最適化(さいてきか)されたツリー型(がた)データ構造です。
単語: "cat", "car", "card", "dog", "do"
Root
+-- c
| +-- a
| +-- t (*)
| +-- r (*)
| +-- d (*)
+-- d
+-- o (*)
+-- g (*)
(*) = 単語の終端
6.2 Trieの実装(じっそう)
class TrieNode:
def __init__(self):
self.children = {}
self.is_end = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end = True
def search(self, word):
node = self._find_node(word)
return node is not None and node.is_end
def starts_with(self, prefix):
return self._find_node(prefix) is not None
def _find_node(self, prefix):
node = self.root
for char in prefix:
if char not in node.children:
return None
node = node.children[char]
return node
def autocomplete(self, prefix):
"""prefixで始まる全ての単語を返す"""
node = self._find_node(prefix)
if not node:
return []
results = []
self._dfs(node, prefix, results)
return results
def _dfs(self, node, path, results):
if node.is_end:
results.append(path)
for char, child in node.children.items():
self._dfs(child, path + char, results)
| 操作 | 時間計算量(m = 文字列長) |
|---|---|
| 挿入 | O(m) |
| 検索 | O(m) |
| プレフィックス検索 | O(m) |
- 活用: オートコンプリート、スペルチェック、IPルーティング(Longest Prefix Match)、辞書検索
7. グラフ(Graph)
7.1 グラフの表現(ひょうげん)
グラフ:
A --- B
| / |
| / |
C --- D
隣接リスト(Adjacency List):
A: [B, C]
B: [A, C, D]
C: [A, B, D]
D: [B, C]
隣接行列(Adjacency Matrix):
A B C D
A [0, 1, 1, 0]
B [1, 0, 1, 1]
C [1, 1, 0, 1]
D [0, 1, 1, 0]
| 比較 | 隣接リスト | 隣接行列 |
|---|---|---|
| 空間 | O(V + E) | O(V^2) |
| 辺の存在確認 | O(degree) | O(1) |
| 全隣接ノード走査 | O(degree) | O(V) |
| 適用 | 疎グラフ | 密グラフ |
7.2 BFS(幅優先探索(はばゆうせんたんさく))
from collections import deque
def bfs(graph, start):
visited = set([start])
queue = deque([start])
order = []
while queue:
node = queue.popleft()
order.append(node)
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
return order
BFS順序(Aから開始):
Level 0: A
Level 1: B, C
Level 2: D
キュー: [A] -> [B, C] -> [C, D] -> [D] -> []
7.3 DFS(深さ優先探索(ふかさゆうせんたんさく))
def dfs(graph, start, visited=None):
if visited is None:
visited = set()
visited.add(start)
result = [start]
for neighbor in graph[start]:
if neighbor not in visited:
result.extend(dfs(graph, neighbor, visited))
return result
7.4 ダイクストラアルゴリズム(Dijkstra)
重(おも)み付(つ)きグラフで最短経路(さいたんけいろ)を求(もと)めます。
import heapq
def dijkstra(graph, start):
distances = {node: float('inf') for node in graph}
distances[start] = 0
pq = [(0, start)] # (距離, ノード)
while pq:
dist, node = heapq.heappop(pq)
if dist > distances[node]:
continue
for neighbor, weight in graph[node]:
new_dist = dist + weight
if new_dist < distances[neighbor]:
distances[neighbor] = new_dist
heapq.heappush(pq, (new_dist, neighbor))
return distances
- 時間計算量: O((V + E) log V)(最小ヒープ使用時)
- 負(ふ)の重みがない場合のみ使用可能
7.5 トポロジカルソート(Topological Sort)
DAG(有向非巡回グラフ)で先行関係(せんこうかんけい)を保(たも)つ順序付(じゅんじょづ)けです。
from collections import deque
def topological_sort(graph, in_degree):
queue = deque([v for v in graph if in_degree[v] == 0])
result = []
while queue:
node = queue.popleft()
result.append(node)
for neighbor in graph[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
return result if len(result) == len(graph) else [] # サイクル検出
- 活用: ビルド依存関係、タスクスケジューリング、コンパイル順序
7.6 ベルマン-フォードアルゴリズム(Bellman-Ford)
負(ふ)の重(おも)みがあるグラフでも最短経路を求めます。
def bellman_ford(edges, num_vertices, start):
distances = [float('inf')] * num_vertices
distances[start] = 0
for _ in range(num_vertices - 1):
for u, v, w in edges:
if distances[u] + w < distances[v]:
distances[v] = distances[u] + w
# 負のサイクル検出
for u, v, w in edges:
if distances[u] + w < distances[v]:
return None # 負のサイクルが存在
return distances
- 時間計算量: O(V * E)
8. 高度(こうど)なデータ構造
8.1 Union-Find(素集合(そしゅうごう))
初期状態: 各ノードが自分自身を代表
{0} {1} {2} {3} {4}
union(0, 1): {0, 1} {2} {3} {4}
union(2, 3): {0, 1} {2, 3} {4}
union(0, 3): {0, 1, 2, 3} {4}
find(1) == find(3)? -> Yes(同じ集合)
find(1) == find(4)? -> No(異なる集合)
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
# 経路圧縮(Path Compression)
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
# ランクによる統合(Union by Rank)
px, py = self.find(x), self.find(y)
if px == py:
return False
if self.rank[px] < self.rank[py]:
px, py = py, px
self.parent[py] = px
if self.rank[px] == self.rank[py]:
self.rank[px] += 1
return True
- 経路圧縮 + ランクによる統合: ほぼO(1)(逆アッカーマン関数)
- 活用: クラスカルMST、連結成分判定、サイクル検出
8.2 Skip List
ソート済(ず)み連結リストに複数(ふくすう)レベルのインデックスを追加(ついか)した確率的(かくりつてき)データ構造です。
Level 3: HEAD -------------------- 50 ------- NIL
Level 2: HEAD ---- 20 ----------- 50 --- 70 -- NIL
Level 1: HEAD -- 10 -- 20 -- 30 -- 50 -- 70 -- 80 -- NIL
Level 0: HEAD -- 10 -- 20 -- 30 -- 40 -- 50 -- 60 -- 70 -- 80 -- NIL
| 操作 | 平均 | 最悪 |
|---|---|---|
| 探索 | O(log n) | O(n) |
| 挿入 | O(log n) | O(n) |
| 削除 | O(log n) | O(n) |
- RedisのSorted Setで使用
- Red-Black Treeより実装が簡単
8.3 Bloom Filter
要素(ようそ)が集合(しゅうごう)に属(ぞく)するかどうかを確率的(かくりつてき)に判定(はんてい)するデータ構造です。
k=3 ハッシュ関数、m=10 ビット配列
insert("apple"):
h1("apple")=2, h2("apple")=5, h3("apple")=8
ビット配列: [0,0,1,0,0,1,0,0,1,0]
query("apple"): 位置2,5,8が全て1 -> 「おそらく存在する」
query("grape"): h1=1, h2=5, h3=7 -> 位置1が0 -> 「確実に存在しない」
- 偽陽性(False Positive): 「ある」と判定したが実際にはない場合がある
- 偽陰性(False Negative): なし(「ない」と判定したら確実にない)
- 活用: キャッシュフィルター、スパムフィルター、DBの存在確認
8.4 LRU Cache
最(もっと)も長(なが)く使用(しよう)されていない項目(こうもく)を削除(さくじょ)するキャッシュです。
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.cache = OrderedDict()
def get(self, key):
if key not in self.cache:
return -1
self.cache.move_to_end(key)
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False)
- HashMap + 双方向連結リストでO(1)のget/putを実現
- 活用: ブラウザキャッシュ、データベースバッファ、CDN
9. データ構造の時間計算量比較表(ひかくひょう)
| データ構造 | アクセス | 探索 | 挿入 | 削除 | 空間 |
|---|---|---|---|---|---|
| Array | O(1) | O(n) | O(n) | O(n) | O(n) |
| Dynamic Array | O(1) | O(n) | O(1)* | O(n) | O(n) |
| Linked List | O(n) | O(n) | O(1) | O(1) | O(n) |
| Stack | O(n) | O(n) | O(1) | O(1) | O(n) |
| Queue | O(n) | O(n) | O(1) | O(1) | O(n) |
| Hash Table | - | O(1)* | O(1)* | O(1)* | O(n) |
| BST | - | O(log n)* | O(log n)* | O(log n)* | O(n) |
| AVL Tree | - | O(log n) | O(log n) | O(log n) | O(n) |
| Red-Black Tree | - | O(log n) | O(log n) | O(log n) | O(n) |
| B-Tree | - | O(log n) | O(log n) | O(log n) | O(n) |
| Heap | - | O(n) | O(log n) | O(log n) | O(n) |
| Trie | - | O(m) | O(m) | O(m) | O(n*m) |
| Skip List | - | O(log n)* | O(log n)* | O(log n)* | O(n log n) |
* = 平均時間。最悪の場合は異なる可能性あり。m = 文字列長。
10. 問題別(もんだいべつ)データ構造選択(せんたく)ガイド
10.1 意思決定木(いしけっていき)
高速な挿入/削除が重要? -> ハッシュテーブルまたは連結リスト
ソート順序が必要? -> BST系(AVL、Red-Black)
最小値/最大値を高速に? -> ヒープ
文字列プレフィックス検索? -> トライ
範囲クエリ? -> セグメントツリーまたはB-Tree
集合所属の高速判定? -> HashSetまたはBloom Filter
連結成分の管理? -> Union-Find
最短経路? -> グラフ + Dijkstra/BFS
FIFO? -> キュー
LIFO? -> スタック
10.2 実践活用(じっせんかつよう)マップ
| 問題の種類 | 推奨データ構造 | 理由 |
|---|---|---|
| キャッシュ実装 | HashMap + DLL(LRU) | O(1) get/put |
| オートコンプリート | Trie | プレフィックス検索 O(m) |
| リアルタイムランキング | Skip Listまたは均衡BST | ソート + 高速更新 |
| タスクスケジューラ | Priority Queue(Heap) | 優先度ベースの抽出 |
| ソーシャルネットワーク | Graph(隣接リスト) | 関係性の探索 |
| DBインデックス | B+Tree | 範囲クエリ + ディスク最適化 |
| 重複判定 | HashSetまたはBloom Filter | O(1) 判定 |
| 区間合計/最小値 | Segment Tree | O(log n) クエリ |
| ネットワーク接続性 | Union-Find | ほぼO(1) の統合/検索 |
| 括弧マッチング | Stack | LIFOパターン |
| 幅優先探索 | Queue + Graph | レベル順走査 |
11. 面接(めんせつ)質問(しつもん)15選(せん)
Q1. 配列と連結リストの違いを説明し、それぞれいつ使うか述べてください。
配列は連続メモリ割り当てによりインデックスアクセスがO(1)ですが、中間の挿入/削除はO(n)です。連結リストはポインタで接続されたノードにより挿入/削除がO(1)ですが、インデックスアクセスはO(n)です。配列はランダムアクセスが頻繁でサイズが予測可能な場合に、連結リストは頻繁な挿入/削除と動的サイズが必要な場合に使用します。
Q2. ハッシュテーブルの衝突解決方法を説明してください。
チェイニング(Chaining)は同じバケットに連結リストで複数要素を格納します。実装が簡単で、負荷率が高くても性能劣化が緩やかです。オープンアドレッシング(Open Addressing)は衝突時に他の空きスロットを探します。線形探査、二次探査、二重ハッシュなどがあります。キャッシュ効率は良いですが、負荷率が高いと性能が急激に低下します。
Q3. BSTでの削除操作を説明してください。
3つのケースがあります:(1)リーフノード: 単純に削除。(2)子が1つ: 子で置換。(3)子が2つ: 中間順後続ノード(右サブツリーの最小値)または中間順先行ノード(左サブツリーの最大値)で置換し、そのノードを削除します。
Q4. HeapとBSTの違いを説明してください。
Heapは親が子より大きい(Max)または小さい(Min)完全二分木で、最小値/最大値の取得がO(1)、挿入/削除がO(log n)です。BSTは左の子 < 親 < 右の子というルールで、ソート順の走査が可能で特定値の検索がO(log n)です。Heapは優先度キューに、BSTはソート済みデータの管理に適しています。
Q5. Trieの時間/空間計算量とHashMapベースの実装の長所と短所を説明してください。
Trieは文字列長mに対して挿入/検索がO(m)です。空間は最悪O(n*m)ですが、共通プレフィックスを共有するため実際にはそれより少なくなります。HashMapベースの実装(各ノードのchildrenをHashMapで管理)はアルファベットサイズに柔軟でメモリ効率が良いですが、配列ベースより定数時間が大きくなります。
Q6. BFSとDFSの違いとそれぞれの活用事例を述べてください。
BFSはキューを使用してレベル順に探索し、重みなしグラフでの最短経路に適しています。DFSはスタック(または再帰)を使用して深さ優先で探索し、経路探索、サイクル検出、トポロジカルソートに適しています。どちらもO(V+E)の時間、O(V)の空間を使用します。
Q7. Red-Black TreeとAVL Treeの違いを説明してください。
どちらも自己均衡BSTです。AVLはより厳密な均衡(高さの差が1以下)により探索が速いですが、挿入/削除時に回転が多くなります。Red-Blackはより緩い均衡により挿入/削除が速いですが、探索はやや遅くなります。読み取り中心ならAVL、書き込み中心ならRed-Blackが有利です。
Q8. LRU CacheをO(1)で実装する方法を説明してください。
HashMapと双方向連結リストを組み合わせます。HashMapはキーからノード参照を格納し、双方向連結リストは使用順序を管理します。getはHashMapでO(1)検索後、ノードをリストの末尾に移動。putは新しいノードを末尾に追加し、容量超過時はリストの先頭ノードを削除します。
Q9. Union-Findでの経路圧縮とランクによる統合を説明してください。
経路圧縮(Path Compression)はfind時に経路上の全ノードをルートに直接接続し、以降のfindをほぼO(1)にします。ランクによる統合(Union by Rank)は常に小さいツリーを大きいツリーの下に接続し、ツリーの高さを最小化します。両方を併用するとほぼO(alpha(n))で、事実上の定数時間になります。
Q10. B-Treeがデータベースインデックスに適している理由を説明してください。
B-Treeは次数が高いためツリーの高さが低くなります。ディスクI/Oはブロック単位で行われるため、1ノードに複数キーを格納するB-TreeはI/O回数を最小化します。B+Treeは全データがリーフにあり、リーフが連結リストで接続されているため、範囲クエリが効率的です。
Q11. Bloom Filterとは何で、どこで使いますか?
Bloom Filterは要素の集合所属を確率的に判定するデータ構造です。偽陽性(「ある」と判定したが実際にはない)は可能ですが、偽陰性(「ない」と判定したが実際にはある)は不可能です。空間効率が良く、キャッシュフィルター、スパムURL検査、DBの不要なディスクアクセス防止などに使用されます。
Q12. ダイクストラとベルマン-フォードアルゴリズムの違いを説明してください。
ダイクストラは負の重みがないグラフで最短経路を求め、優先度キューを使用してO((V+E)log V)です。ベルマン-フォードは負の重みも処理可能で負のサイクルも検出しますが、O(V*E)で遅くなります。負の重みがなければダイクストラ、あればベルマン-フォードを使用します。
Q13. StackでQueueを実装する方法を説明してください。
2つのスタックを使用します。pushスタックにenqueueし、dequeue時にpopスタックが空であればpushスタックの全要素をpopスタックに移します。これにより償却O(1)でenqueue/dequeueが可能になります。
Q14. ハッシュテーブルのLoad Factorが性能に与える影響を説明してください。
Load Factor(負荷率)は格納済み要素数をバケット数で割った値です。負荷率が高くなると衝突確率が増加し、チェイニングでは連結リストが長くなり、オープンアドレッシングではクラスタリングが悪化します。一般的に0.75を閾値とし、超過するとリハッシュ(バケット数の拡張)を実行します。
Q15. グラフでサイクルを検出する方法を説明してください。
無向グラフ: Union-Findで辺追加時に同じ集合ならサイクル、またはDFSで親以外の訪問済みノードに遭遇したらサイクル。有向グラフ: DFSで現在の経路(再帰スタック)にあるノードを再度訪問したらサイクル(バックエッジ検出)。トポロジカルソートで全ノードを訪問できなければサイクルが存在します。
12. クイズ5選(せん)
クイズ1. サイズnのソート済み配列での二分探索の最大比較回数は?
floor(log2(n)) + 1回(かい)です。二分探索は毎回探索範囲を半分に縮小するためO(log n)です。例えばn=1000の場合、最大10回の比較で見つけることができます。
クイズ2. Min Heapで最大値を見つける時間計算量は?
**O(n)**です。Min Heapでは最大値は必ずリーフノードにあり、リーフノードは約n/2個あるため全て確認する必要があります。Heapは最小値のみO(1)でアクセス可能です。
クイズ3. ハッシュテーブルの全キーをソート順で出力するには?
**O(n log n)**が必要です。ハッシュテーブルはソート順を保持しないため、全キーを取り出してからソートする必要があります。ソート順が頻繁に必要な場合はTreeMap(均衡BSTベース)を使用するのが良いです。
クイズ4. n個のノードを持つ完全二分木の高さは?
**floor(log2(n))**です。完全二分木のレベルkには最大2^k個のノードがあり、n個のノードを収めるには約log2(n)レベルが必要です。
クイズ5. Trieで「abc」を削除する際、「ab」も単語である場合はどうなりますか?
「abc」の終端マーカー(is_end)をfalseに変更するだけです。ノード「c」に子がなければ「c」ノードを削除します。ノード「b」は「ab」の終端なので削除しません。削除は、子がなく単語の終端でもないノードをリーフから再帰的に除去します。
13. 参考資料(さんこうしりょう)
書籍(しょせき)
- Thomas H. Cormen et al., "Introduction to Algorithms (CLRS)" (4th ed., 2022)
- Robert Sedgewick, Kevin Wayne, "Algorithms" (4th ed., 2011)
- Steven S. Skiena, "The Algorithm Design Manual" (3rd ed., 2020)
オンライン学習(がくしゅう)
- LeetCode: https://leetcode.com/
- NeetCodeロードマップ: https://neetcode.io/
- Visualgo — データ構造可視化: https://visualgo.net/
- Big-O Cheat Sheet: https://www.bigocheatsheet.com/
講義(こうぎ)
- MIT 6.006 Introduction to Algorithms (OpenCourseWare)
- Stanford CS 161 Design and Analysis of Algorithms
- UC Berkeley CS 61B Data Structures
コーディング面接準備(じゅんび)
- Gayle Laakmann McDowell, "Cracking the Coding Interview" (6th ed.)
- Alex Xu, "System Design Interview" Vol. 1 & 2
- Aditya Bhargava, "Grokking Algorithms" (2nd ed., 2024)
Web資料(しりょう)
- GeeksforGeeks Data Structures: https://www.geeksforgeeks.org/data-structures/
- CP-Algorithms: https://cp-algorithms.com/
- Tech Interview Handbook: https://www.techinterviewhandbook.org/