Algorithms and Data Structures

アルゴリズムとデータ構造 完全ガイド

1. イントロダクション

1.1 アルゴリズムとは何か

アルゴリズムとは、ある問題を解決するための手順を明確に定義した有限の命令列である。日常生活においても、料理のレシピや道順の説明など、アルゴリズム的な思考は随所に見られる。コンピュータサイエンスにおいては、データの処理・検索・ソート・最適化など、あらゆる計算処理の基盤となる概念である。

良いアルゴリズムの条件は以下の通りである。

  • 正確性(Correctness): すべての入力に対して正しい出力を返す
  • 効率性(Efficiency): 時間的・空間的リソースを最小限に抑える
  • 有限性(Finiteness): 有限のステップで必ず終了する
  • 明確性(Definiteness): 各ステップが曖昧さなく定義されている

1.2 データ構造とは何か

データ構造とは、データを効率的に格納・管理・アクセスするための体系的な方法である。アルゴリズムの効率はデータ構造の選択に大きく依存する。適切なデータ構造を選択することで、アルゴリズムの時間計算量を劇的に改善できる場合がある。

データ構造は大きく以下のように分類される。

分類
線形データ構造配列、連結リスト、スタック、キュー
非線形データ構造ツリー、グラフ
ハッシュベースハッシュテーブル、ハッシュマップ
特殊構造トライ木、セグメント木、Union-Find

1.3 計算量(Big-O記法)

アルゴリズムの効率を評価する上で最も重要な概念が計算量分析である。Big-O記法は、入力サイズ n に対するアルゴリズムの実行時間や使用メモリの増加率の上限を表す。

1.3.1 時間計算量の代表的なオーダー

記法名称
O(1)定数時間配列のインデックスアクセス
O(log n)対数時間二分探索
O(n)線形時間線形探索
O(n log n)準線形時間マージソート、クイックソート(平均)
O(n^2)二乗時間バブルソート、選択ソート
O(n^3)三乗時間行列の乗算(ナイーブ)
O(2^n)指数時間部分集合の列挙
O(n!)階乗時間順列の全列挙

1.3.2 Big-O記法の数学的定義

f(n) = O(g(n)) とは、ある正の定数 c と n₀ が存在して、すべての n ≧ n₀ に対して f(n) ≦ c・g(n) が成り立つことを意味する。

直感的には、「入力サイズが十分に大きいとき、f(n) の増加率は g(n) の定数倍以下である」ということである。

1.3.3 計算量の計算例

# O(1) - 定数時間
def get_first(arr):
    return arr[0]

# O(n) - 線形時間
def find_max(arr):
    max_val = arr[0]
    for x in arr:       # n回のループ
        if x > max_val:
            max_val = x
    return max_val

# O(n^2) - 二乗時間
def has_duplicate(arr):
    n = len(arr)
    for i in range(n):        # n回
        for j in range(i+1, n):  # 最大n-1回
            if arr[i] == arr[j]:
                return True
    return False

# O(log n) - 対数時間
def binary_search(arr, target):
    lo, hi = 0, len(arr) - 1
    while lo <= hi:          # 毎回半分に絞る → log n 回
        mid = (lo + hi) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            lo = mid + 1
        else:
            hi = mid - 1
    return -1

1.3.4 空間計算量

時間計算量と同様に、アルゴリズムが使用するメモリ量も重要な指標である。

# 空間計算量 O(1) - 入力以外に追加メモリを使わない
def reverse_in_place(arr):
    left, right = 0, len(arr) - 1
    while left < right:
        arr[left], arr[right] = arr[right], arr[left]
        left += 1
        right -= 1

# 空間計算量 O(n) - 入力と同じサイズのメモリが必要
def reverse_copy(arr):
    return arr[::-1]

1.3.5 最良・平均・最悪計算量

アルゴリズムの計算量は入力によって異なる場合がある。

ケース説明例(クイックソート)
最良ケース(Best Case)最も有利な入力O(n log n)
平均ケース(Average Case)典型的な入力O(n log n)
最悪ケース(Worst Case)最も不利な入力O(n^2)

1.4 なぜアルゴリズムとデータ構造が重要か

パフォーマンスへの直接的影響

100万件のデータからある要素を検索するケースを考える。

  • 線形探索 O(n): 最悪100万回の比較
  • 二分探索 O(log n): 最大約20回の比較

この差は、システムの応答速度やスケーラビリティに直結する。

ソフトウェアエンジニアとしての必須知識

  1. 技術面接: FAANG(Google, Amazon, Meta等)をはじめとする多くの企業で、アルゴリズムとデータ構造の知識は面接の重要項目である
  2. システム設計: 適切なデータ構造の選択は、大規模システムの設計において不可欠である
  3. 問題解決能力: アルゴリズム的思考は、複雑な問題を分解して解決する能力を養う
  4. 競技プログラミング: AtCoder、Codeforces、LeetCode等の競技プログラミングでは必須のスキルである

1.5 本記事の構成

本記事では、以下の内容を体系的に解説する。

  1. 基本データ構造: 配列、連結リスト、スタック、キュー、デック
  2. ツリー構造: 二分木から高度な平衡木、セグメント木まで
  3. ハッシュテーブルとグラフ: ハッシュの仕組みとグラフアルゴリズム
  4. ソートアルゴリズム: 基本的なソートから高効率なソートまで
  5. 探索と動的計画法: 探索手法とDPによる最適化
  6. 高度なアルゴリズム: 貪欲法、分割統治、文字列アルゴリズム等
  7. 実践ガイド: 競技プログラミングや実務での活用法

各トピックについて、理論的な解説に加えて、Python・Java・TypeScriptでの具体的な実装例を提示する。

2. 基本データ構造

2.1 配列(Array)

配列は、同じ型の要素をメモリ上に連続して格納するデータ構造である。インデックスを用いたランダムアクセスが O(1) で可能な点が最大の特徴である。

計算量

操作静的配列動的配列
インデックスアクセスO(1)O(1)
末尾への追加N/A償却 O(1)
先頭への挿入O(n)O(n)
任意位置への挿入O(n)O(n)
末尾からの削除N/AO(1)
任意位置の削除O(n)O(n)
探索(未ソート)O(n)O(n)

Python実装例

# Pythonのリストは動的配列として実装されている
# 基本操作
arr = [1, 2, 3, 4, 5]

# インデックスアクセス O(1)
print(arr[2])  # 3

# 末尾への追加 償却O(1)
arr.append(6)  # [1, 2, 3, 4, 5, 6]

# 任意位置への挿入 O(n)
arr.insert(2, 10)  # [1, 2, 10, 3, 4, 5, 6]

# 削除 O(n)
arr.pop(2)  # [1, 2, 3, 4, 5, 6]

# 動的配列の自作実装
class DynamicArray:
    def __init__(self):
        self._capacity = 4
        self._size = 0
        self._data = [None] * self._capacity

    def __len__(self):
        return self._size

    def __getitem__(self, index):
        if index < 0 or index >= self._size:
            raise IndexError("Index out of range")
        return self._data[index]

    def append(self, value):
        if self._size == self._capacity:
            self._resize(self._capacity * 2)
        self._data[self._size] = value
        self._size += 1

    def pop(self):
        if self._size == 0:
            raise IndexError("Pop from empty array")
        self._size -= 1
        val = self._data[self._size]
        self._data[self._size] = None
        if self._size > 0 and self._size <= self._capacity // 4:
            self._resize(self._capacity // 2)
        return val

    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

Java実装例

import java.util.ArrayList;
import java.util.Arrays;

public class ArrayExample {
    // 静的配列
    public static void staticArrayExample() {
        int[] arr = new int[]{1, 2, 3, 4, 5};
        System.out.println(arr[2]); // 3
    }

    // 動的配列 (ArrayList)
    public static void dynamicArrayExample() {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.get(1);     // O(1)
        list.add(1, 10); // O(n) - インデックス1に挿入
        list.remove(1);  // O(n)
    }

    // 動的配列の自作実装
    static class DynamicArray<T> {
        private Object[] data;
        private int size;
        private int capacity;

        public DynamicArray() {
            this.capacity = 4;
            this.size = 0;
            this.data = new Object[capacity];
        }

        @SuppressWarnings("unchecked")
        public T get(int index) {
            if (index < 0 || index >= size)
                throw new IndexOutOfBoundsException();
            return (T) data[index];
        }

        public void add(T value) {
            if (size == capacity) resize(capacity * 2);
            data[size++] = value;
        }

        @SuppressWarnings("unchecked")
        public T removeLast() {
            if (size == 0) throw new RuntimeException("Empty");
            T val = (T) data[--size];
            data[size] = null;
            if (size > 0 && size <= capacity / 4) resize(capacity / 2);
            return val;
        }

        private void resize(int newCapacity) {
            data = Arrays.copyOf(data, newCapacity);
            capacity = newCapacity;
        }

        public int size() { return size; }
    }
}

TypeScript実装例

// TypeScriptの配列は動的配列
const arr: number[] = [1, 2, 3, 4, 5];

// インデックスアクセス O(1)
console.log(arr[2]); // 3

// 末尾への追加 償却O(1)
arr.push(6);

// 先頭への挿入 O(n)
arr.unshift(0);

// 動的配列の自作実装
class DynamicArray<T> {
    private data: (T | undefined)[];
    private _size: number;
    private capacity: number;

    constructor() {
        this.capacity = 4;
        this._size = 0;
        this.data = new Array(this.capacity);
    }

    get(index: number): T {
        if (index < 0 || index >= this._size) {
            throw new Error("Index out of range");
        }
        return this.data[index] as T;
    }

    push(value: T): void {
        if (this._size === this.capacity) {
            this.resize(this.capacity * 2);
        }
        this.data[this._size++] = value;
    }

    pop(): T {
        if (this._size === 0) throw new Error("Empty");
        const val = this.data[--this._size] as T;
        this.data[this._size] = undefined;
        if (this._size > 0 && this._size <= this.capacity / 4) {
            this.resize(this.capacity / 2);
        }
        return val;
    }

    get size(): number { return this._size; }

    private resize(newCapacity: number): void {
        const newData = new Array(newCapacity);
        for (let i = 0; i < this._size; i++) {
            newData[i] = this.data[i];
        }
        this.data = newData;
        this.capacity = newCapacity;
    }
}

2.2 連結リスト(Linked List)

連結リストは、各要素(ノード)がデータと次のノードへの参照(ポインタ)を持つデータ構造である。メモリ上で連続している必要がないため、挿入・削除が効率的に行える。

種類

  • 単方向連結リスト(Singly Linked List): 各ノードが次のノードへの参照のみを持つ
  • 双方向連結リスト(Doubly Linked List): 各ノードが前後のノードへの参照を持つ
  • 循環連結リスト(Circular Linked List): 末尾のノードが先頭を指す

計算量

操作単方向双方向
先頭への挿入O(1)O(1)
末尾への挿入O(n) / O(1)*O(1)
任意位置への挿入O(n)O(n)
先頭の削除O(1)O(1)
末尾の削除O(n)O(1)
探索O(n)O(n)
インデックスアクセスO(n)O(n)

*末尾ポインタを保持している場合

Python実装例(双方向連結リスト)

class Node:
    def __init__(self, data):
        self.data = data
        self.prev = None
        self.next = None

class DoublyLinkedList:
    def __init__(self):
        # センチネルノードを使用
        self.head = Node(None)  # ダミーヘッド
        self.tail = Node(None)  # ダミーテール
        self.head.next = self.tail
        self.tail.prev = self.head
        self._size = 0

    def __len__(self):
        return self._size

    def is_empty(self):
        return self._size == 0

    def push_front(self, data):
        """先頭に挿入 O(1)"""
        new_node = Node(data)
        new_node.next = self.head.next
        new_node.prev = self.head
        self.head.next.prev = new_node
        self.head.next = new_node
        self._size += 1

    def push_back(self, data):
        """末尾に挿入 O(1)"""
        new_node = Node(data)
        new_node.prev = self.tail.prev
        new_node.next = self.tail
        self.tail.prev.next = new_node
        self.tail.prev = new_node
        self._size += 1

    def pop_front(self):
        """先頭を削除 O(1)"""
        if self.is_empty():
            raise IndexError("List is empty")
        node = self.head.next
        self.head.next = node.next
        node.next.prev = self.head
        self._size -= 1
        return node.data

    def pop_back(self):
        """末尾を削除 O(1)"""
        if self.is_empty():
            raise IndexError("List is empty")
        node = self.tail.prev
        self.tail.prev = node.prev
        node.prev.next = self.tail
        self._size -= 1
        return node.data

    def find(self, data):
        """探索 O(n)"""
        current = self.head.next
        while current != self.tail:
            if current.data == data:
                return current
            current = current.next
        return None

    def remove(self, node):
        """ノードの削除 O(1) ※ノード参照がある場合"""
        node.prev.next = node.next
        node.next.prev = node.prev
        self._size -= 1
        return node.data

    def to_list(self):
        result = []
        current = self.head.next
        while current != self.tail:
            result.append(current.data)
            current = current.next
        return result

# 使用例
dll = DoublyLinkedList()
dll.push_back(1)
dll.push_back(2)
dll.push_back(3)
dll.push_front(0)
print(dll.to_list())  # [0, 1, 2, 3]
dll.pop_front()        # 0を削除
dll.pop_back()         # 3を削除
print(dll.to_list())  # [1, 2]

Java実装例(単方向連結リスト)

public class SinglyLinkedList<T> {
    private static class Node<T> {
        T data;
        Node<T> next;
        Node(T data) { this.data = data; this.next = null; }
    }

    private Node<T> head;
    private int size;

    public SinglyLinkedList() {
        head = null;
        size = 0;
    }

    public void pushFront(T data) {
        Node<T> newNode = new Node<>(data);
        newNode.next = head;
        head = newNode;
        size++;
    }

    public T popFront() {
        if (head == null) throw new RuntimeException("Empty list");
        T data = head.data;
        head = head.next;
        size--;
        return data;
    }

    public void pushBack(T data) {
        Node<T> newNode = new Node<>(data);
        if (head == null) {
            head = newNode;
        } else {
            Node<T> current = head;
            while (current.next != null) current = current.next;
            current.next = newNode;
        }
        size++;
    }

    public boolean contains(T data) {
        Node<T> current = head;
        while (current != null) {
            if (current.data.equals(data)) return true;
            current = current.next;
        }
        return false;
    }

    public void reverse() {
        Node<T> prev = null;
        Node<T> current = head;
        while (current != null) {
            Node<T> next = current.next;
            current.next = prev;
            prev = current;
            current = next;
        }
        head = prev;
    }

    public int size() { return size; }
}

TypeScript実装例(双方向連結リスト)

class ListNode<T> {
    data: T;
    prev: ListNode<T> | null = null;
    next: ListNode<T> | null = null;
    constructor(data: T) { this.data = data; }
}

class DoublyLinkedList<T> {
    private head: ListNode<T>;
    private tail: ListNode<T>;
    private _size = 0;

    constructor() {
        this.head = new ListNode<T>(null as any); // sentinel
        this.tail = new ListNode<T>(null as any); // sentinel
        this.head.next = this.tail;
        this.tail.prev = this.head;
    }

    get size(): number { return this._size; }

    pushFront(data: T): void {
        const node = new ListNode(data);
        node.next = this.head.next;
        node.prev = this.head;
        this.head.next!.prev = node;
        this.head.next = node;
        this._size++;
    }

    pushBack(data: T): void {
        const node = new ListNode(data);
        node.prev = this.tail.prev;
        node.next = this.tail;
        this.tail.prev!.next = node;
        this.tail.prev = node;
        this._size++;
    }

    popFront(): T {
        if (this._size === 0) throw new Error("Empty");
        const node = this.head.next!;
        this.head.next = node.next;
        node.next!.prev = this.head;
        this._size--;
        return node.data;
    }

    popBack(): T {
        if (this._size === 0) throw new Error("Empty");
        const node = this.tail.prev!;
        this.tail.prev = node.prev;
        node.prev!.next = this.tail;
        this._size--;
        return node.data;
    }

    toArray(): T[] {
        const result: T[] = [];
        let current = this.head.next;
        while (current !== this.tail) {
            result.push(current!.data);
            current = current!.next;
        }
        return result;
    }
}

2.3 スタック(Stack)

スタックは、LIFO(Last In, First Out: 後入れ先出し) の原則に従うデータ構造である。最後に追加された要素が最初に取り出される。

主な操作と計算量

操作計算量説明
pushO(1)要素を上に積む
popO(1)上の要素を取り出す
peek/topO(1)上の要素を参照(取り出さない)
isEmptyO(1)空かどうかの判定

典型的な活用場面

  • 関数呼び出しスタック
  • 括弧の対応チェック
  • ブラウザの「戻る」機能
  • 深さ優先探索(DFS)
  • 逆ポーランド記法の計算

Python実装例

class Stack:
    def __init__(self):
        self._data = []

    def push(self, value):
        self._data.append(value)

    def pop(self):
        if self.is_empty():
            raise IndexError("Stack is empty")
        return self._data.pop()

    def peek(self):
        if self.is_empty():
            raise IndexError("Stack is empty")
        return self._data[-1]

    def is_empty(self):
        return len(self._data) == 0

    def __len__(self):
        return len(self._data)


# 活用例: 括弧の対応チェック
def is_valid_parentheses(s: str) -> bool:
    stack = Stack()
    mapping = {')': '(', '}': '{', ']': '['}
    for char in s:
        if char in '({[':
            stack.push(char)
        elif char in ')}]':
            if stack.is_empty() or stack.pop() != mapping[char]:
                return False
    return stack.is_empty()

print(is_valid_parentheses("({[]})"))   # True
print(is_valid_parentheses("({[})"))    # False


# 活用例: 逆ポーランド記法の計算
def eval_rpn(tokens: list) -> int:
    stack = Stack()
    ops = {
        '+': lambda a, b: a + b,
        '-': lambda a, b: a - b,
        '*': lambda a, b: a * b,
        '/': lambda a, b: int(a / b),
    }
    for token in tokens:
        if token in ops:
            b = stack.pop()
            a = stack.pop()
            stack.push(ops[token](a, b))
        else:
            stack.push(int(token))
    return stack.pop()

print(eval_rpn(["2", "1", "+", "3", "*"]))  # 9 = (2+1)*3

Java実装例

import java.util.ArrayDeque;
import java.util.Deque;

public class StackExample {
    // Javaでは Deque をスタックとして使うのが推奨
    public static boolean isValidParentheses(String s) {
        Deque<Character> stack = new ArrayDeque<>();
        for (char c : s.toCharArray()) {
            if (c == '(' || c == '{' || c == '[') {
                stack.push(c);
            } else {
                if (stack.isEmpty()) return false;
                char top = stack.pop();
                if ((c == ')' && top != '(') ||
                    (c == '}' && top != '{') ||
                    (c == ']' && top != '[')) {
                    return false;
                }
            }
        }
        return stack.isEmpty();
    }

    // 配列ベースのスタック実装
    static class ArrayStack<T> {
        private Object[] data;
        private int top;
        private int capacity;

        public ArrayStack(int capacity) {
            this.capacity = capacity;
            this.data = new Object[capacity];
            this.top = -1;
        }

        public void push(T value) {
            if (top == capacity - 1) throw new RuntimeException("Stack overflow");
            data[++top] = value;
        }

        @SuppressWarnings("unchecked")
        public T pop() {
            if (top == -1) throw new RuntimeException("Stack underflow");
            T val = (T) data[top];
            data[top--] = null;
            return val;
        }

        @SuppressWarnings("unchecked")
        public T peek() {
            if (top == -1) throw new RuntimeException("Stack is empty");
            return (T) data[top];
        }

        public boolean isEmpty() { return top == -1; }
        public int size() { return top + 1; }
    }
}

TypeScript実装例

class Stack<T> {
    private data: T[] = [];

    push(value: T): void { this.data.push(value); }
    pop(): T {
        if (this.isEmpty()) throw new Error("Stack is empty");
        return this.data.pop()!;
    }
    peek(): T {
        if (this.isEmpty()) throw new Error("Stack is empty");
        return this.data[this.data.length - 1];
    }
    isEmpty(): boolean { return this.data.length === 0; }
    get size(): number { return this.data.length; }
}

// 活用例: 最小値スタック
class MinStack {
    private stack: number[] = [];
    private minStack: number[] = [];

    push(val: number): void {
        this.stack.push(val);
        const currentMin = this.minStack.length === 0
            ? val
            : Math.min(val, this.minStack[this.minStack.length - 1]);
        this.minStack.push(currentMin);
    }

    pop(): void {
        this.stack.pop();
        this.minStack.pop();
    }

    top(): number { return this.stack[this.stack.length - 1]; }
    getMin(): number { return this.minStack[this.minStack.length - 1]; }
}

2.4 キュー(Queue)

キューは、FIFO(First In, First Out: 先入れ先出し) の原則に従うデータ構造である。最初に追加された要素が最初に取り出される。

主な操作と計算量

操作計算量説明
enqueueO(1)末尾に要素を追加
dequeueO(1)先頭の要素を取り出す
peek/frontO(1)先頭の要素を参照
isEmptyO(1)空かどうかの判定

Python実装例

from collections import deque

# dequeを使用したキュー(推奨)
class Queue:
    def __init__(self):
        self._data = deque()

    def enqueue(self, value):
        self._data.append(value)

    def dequeue(self):
        if self.is_empty():
            raise IndexError("Queue is empty")
        return self._data.popleft()

    def peek(self):
        if self.is_empty():
            raise IndexError("Queue is empty")
        return self._data[0]

    def is_empty(self):
        return len(self._data) == 0

    def __len__(self):
        return len(self._data)


# 循環バッファによるキュー実装
class CircularQueue:
    def __init__(self, capacity):
        self._data = [None] * capacity
        self._capacity = capacity
        self._front = 0
        self._rear = 0
        self._size = 0

    def enqueue(self, value):
        if self._size == self._capacity:
            raise OverflowError("Queue is full")
        self._data[self._rear] = value
        self._rear = (self._rear + 1) % self._capacity
        self._size += 1

    def dequeue(self):
        if self._size == 0:
            raise IndexError("Queue is empty")
        val = self._data[self._front]
        self._data[self._front] = None
        self._front = (self._front + 1) % self._capacity
        self._size -= 1
        return val

    def peek(self):
        if self._size == 0:
            raise IndexError("Queue is empty")
        return self._data[self._front]

    def is_empty(self):
        return self._size == 0

    def is_full(self):
        return self._size == self._capacity


# 優先度付きキュー
import heapq

class PriorityQueue:
    def __init__(self):
        self._heap = []

    def push(self, priority, item):
        heapq.heappush(self._heap, (priority, item))

    def pop(self):
        if not self._heap:
            raise IndexError("Queue is empty")
        return heapq.heappop(self._heap)

    def peek(self):
        if not self._heap:
            raise IndexError("Queue is empty")
        return self._heap[0]

    def is_empty(self):
        return len(self._heap) == 0

Java実装例

import java.util.LinkedList;
import java.util.PriorityQueue;
import java.util.Queue;

public class QueueExample {
    public static void basicQueue() {
        Queue<Integer> queue = new LinkedList<>();
        queue.offer(1);  // enqueue
        queue.offer(2);
        queue.offer(3);
        queue.peek();    // 1 (先頭を参照)
        queue.poll();    // 1 (先頭を取り出し)
    }

    // 循環キューの実装
    static class CircularQueue<T> {
        private Object[] data;
        private int front, rear, size, capacity;

        public CircularQueue(int capacity) {
            this.capacity = capacity;
            this.data = new Object[capacity];
            this.front = 0;
            this.rear = 0;
            this.size = 0;
        }

        public void enqueue(T value) {
            if (size == capacity) throw new RuntimeException("Full");
            data[rear] = value;
            rear = (rear + 1) % capacity;
            size++;
        }

        @SuppressWarnings("unchecked")
        public T dequeue() {
            if (size == 0) throw new RuntimeException("Empty");
            T val = (T) data[front];
            data[front] = null;
            front = (front + 1) % capacity;
            size--;
            return val;
        }

        public boolean isEmpty() { return size == 0; }
        public boolean isFull() { return size == capacity; }
    }

    // 優先度付きキュー
    public static void priorityQueueExample() {
        PriorityQueue<int[]> pq = new PriorityQueue<>(
            (a, b) -> a[0] - b[0]  // 優先度の低い順
        );
        pq.offer(new int[]{3, 100});
        pq.offer(new int[]{1, 200});
        pq.offer(new int[]{2, 300});
        // poll() は優先度1(最小)から取り出される
    }
}

TypeScript実装例

class Queue<T> {
    private data: Map<number, T> = new Map();
    private head = 0;
    private tail = 0;

    enqueue(value: T): void {
        this.data.set(this.tail, value);
        this.tail++;
    }

    dequeue(): T {
        if (this.isEmpty()) throw new Error("Queue is empty");
        const val = this.data.get(this.head)!;
        this.data.delete(this.head);
        this.head++;
        return val;
    }

    peek(): T {
        if (this.isEmpty()) throw new Error("Queue is empty");
        return this.data.get(this.head)!;
    }

    isEmpty(): boolean { return this.head === this.tail; }
    get size(): number { return this.tail - this.head; }
}

2.5 デック(Deque / Double-Ended Queue)

デック(両端キュー)は、先頭と末尾の両方から要素の追加・削除が O(1) で行えるデータ構造である。スタックとキューの両方の機能を兼ね備える。

計算量

操作計算量
先頭への追加 (pushFront)O(1)
末尾への追加 (pushBack)O(1)
先頭の削除 (popFront)O(1)
末尾の削除 (popBack)O(1)
先頭の参照O(1)
末尾の参照O(1)

Python実装例

from collections import deque

# Pythonのdequeは双方向連結リストで実装
dq = deque()

dq.append(1)        # 末尾に追加
dq.append(2)
dq.appendleft(0)    # 先頭に追加
print(list(dq))     # [0, 1, 2]

dq.pop()             # 末尾から削除 → 2
dq.popleft()         # 先頭から削除 → 0

# 活用例: スライディングウィンドウ最大値
def max_sliding_window(nums: list, k: int) -> list:
    """
    サイズkのスライディングウィンドウの最大値をO(n)で求める
    """
    dq = deque()  # インデックスを格納(単調減少デック)
    result = []
    for i, num in enumerate(nums):
        # ウィンドウ外の要素を削除
        while dq and dq[0] < i - k + 1:
            dq.popleft()
        # 現在の要素より小さい要素をすべて削除
        while dq and nums[dq[-1]] <= num:
            dq.pop()
        dq.append(i)
        # ウィンドウが完成したら結果に追加
        if i >= k - 1:
            result.append(nums[dq[0]])
    return result

print(max_sliding_window([1, 3, -1, -3, 5, 3, 6, 7], 3))
# [3, 3, 5, 5, 6, 7]

Java実装例

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.ArrayList;
import java.util.List;

public class DequeExample {
    // スライディングウィンドウ最大値
    public static int[] maxSlidingWindow(int[] nums, int k) {
        Deque<Integer> dq = new ArrayDeque<>();
        int[] result = new int[nums.length - k + 1];
        int idx = 0;

        for (int i = 0; i < nums.length; i++) {
            while (!dq.isEmpty() && dq.peekFirst() < i - k + 1) {
                dq.pollFirst();
            }
            while (!dq.isEmpty() && nums[dq.peekLast()] <= nums[i]) {
                dq.pollLast();
            }
            dq.offerLast(i);
            if (i >= k - 1) {
                result[idx++] = nums[dq.peekFirst()];
            }
        }
        return result;
    }
}

TypeScript実装例

class Deque<T> {
    private data: Map<number, T> = new Map();
    private head = 0;
    private tail = 0;

    pushFront(value: T): void {
        this.head--;
        this.data.set(this.head, value);
    }

    pushBack(value: T): void {
        this.data.set(this.tail, value);
        this.tail++;
    }

    popFront(): T {
        if (this.isEmpty()) throw new Error("Deque is empty");
        const val = this.data.get(this.head)!;
        this.data.delete(this.head);
        this.head++;
        return val;
    }

    popBack(): T {
        if (this.isEmpty()) throw new Error("Deque is empty");
        this.tail--;
        const val = this.data.get(this.tail)!;
        this.data.delete(this.tail);
        return val;
    }

    peekFront(): T {
        if (this.isEmpty()) throw new Error("Deque is empty");
        return this.data.get(this.head)!;
    }

    peekBack(): T {
        if (this.isEmpty()) throw new Error("Deque is empty");
        return this.data.get(this.tail - 1)!;
    }

    isEmpty(): boolean { return this.head === this.tail; }
    get size(): number { return this.tail - this.head; }
}

// スライディングウィンドウ最大値
function maxSlidingWindow(nums: number[], k: number): number[] {
    const dq: number[] = []; // インデックスの単調減少デック
    const result: number[] = [];

    for (let i = 0; i < nums.length; i++) {
        while (dq.length > 0 && dq[0] < i - k + 1) dq.shift();
        while (dq.length > 0 && nums[dq[dq.length - 1]] <= nums[i]) dq.pop();
        dq.push(i);
        if (i >= k - 1) result.push(nums[dq[0]]);
    }
    return result;
}

2.6 基本データ構造の比較まとめ

データ構造主な用途アクセス挿入削除探索
配列ランダムアクセスO(1)O(n)O(n)O(n)
連結リスト頻繁な挿入/削除O(n)O(1)*O(1)*O(n)
スタックLIFO処理O(1)topO(1)O(1)O(n)
キューFIFO処理O(1)frontO(1)O(1)O(n)
デック両端操作O(1)両端O(1)O(1)O(n)

*ノードの参照がある場合

3. ツリー構造

3.1 ツリーの基本概念

ツリー(木構造)は、ノードとエッジで構成される非線形の階層的データ構造である。一つのルート(根)ノードから始まり、各ノードは0個以上の子ノードを持つ。

用語定義

用語説明
ルート(Root)ツリーの最上位ノード
葉(Leaf)子を持たないノード
深さ(Depth)ルートからそのノードまでのエッジ数
高さ(Height)そのノードから最も深い葉までのエッジ数
部分木(Subtree)あるノードを根とするツリー
次数(Degree)ノードが持つ子の数

3.2 二分木(Binary Tree)

各ノードが最大2つの子(左の子、右の子)を持つツリー。

種類

  • 完全二分木(Complete Binary Tree): 最下段を除きすべてのレベルが満たされ、最下段は左詰め
  • 満二分木(Full Binary Tree): すべてのノードが0個または2個の子を持つ
  • 完璧二分木(Perfect Binary Tree): すべての内部ノードが2個の子を持ち、すべての葉が同じ深さ

走査(Traversal)

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

# 前順走査(Preorder): 根 → 左 → 右
def preorder(node):
    if node is None:
        return []
    return [node.val] + preorder(node.left) + preorder(node.right)

# 中順走査(Inorder): 左 → 根 → 右
def inorder(node):
    if node is None:
        return []
    return inorder(node.left) + [node.val] + inorder(node.right)

# 後順走査(Postorder): 左 → 右 → 根
def postorder(node):
    if node is None:
        return []
    return postorder(node.left) + postorder(node.right) + [node.val]

# レベル順走査(Level-order / BFS)
from collections import deque

def level_order(root):
    if root is None:
        return []
    result = []
    queue = deque([root])
    while queue:
        level = []
        for _ in range(len(queue)):
            node = queue.popleft()
            level.append(node.val)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        result.append(level)
    return result

# 反復的な中順走査(スタックを使用)
def inorder_iterative(root):
    result = []
    stack = []
    current = root
    while current or stack:
        while current:
            stack.append(current)
            current = current.left
        current = stack.pop()
        result.append(current.val)
        current = current.right
    return result

Java実装例

import java.util.*;

public class BinaryTree {
    static class TreeNode {
        int val;
        TreeNode left, right;
        TreeNode(int val) { this.val = val; }
    }

    // 中順走査(反復)
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        Deque<TreeNode> stack = new ArrayDeque<>();
        TreeNode current = root;
        while (current != null || !stack.isEmpty()) {
            while (current != null) {
                stack.push(current);
                current = current.left;
            }
            current = stack.pop();
            result.add(current.val);
            current = current.right;
        }
        return result;
    }

    // レベル順走査
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> result = new ArrayList<>();
        if (root == null) return result;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            int size = queue.size();
            List<Integer> level = new ArrayList<>();
            for (int i = 0; i < size; i++) {
                TreeNode node = queue.poll();
                level.add(node.val);
                if (node.left != null) queue.offer(node.left);
                if (node.right != null) queue.offer(node.right);
            }
            result.add(level);
        }
        return result;
    }
}

3.3 二分探索木(Binary Search Tree: BST)

二分探索木は、以下の性質を持つ二分木である。

  • 左部分木のすべてのノードの値 < 親ノードの値
  • 右部分木のすべてのノードの値 > 親ノードの値

計算量

操作平均最悪(偏った木)
探索O(log n)O(n)
挿入O(log n)O(n)
削除O(log n)O(n)

Python実装例

class BSTNode:
    def __init__(self, key):
        self.key = key
        self.left = None
        self.right = None

class BST:
    def __init__(self):
        self.root = None

    def insert(self, key):
        self.root = self._insert(self.root, key)

    def _insert(self, node, key):
        if node is None:
            return BSTNode(key)
        if key < node.key:
            node.left = self._insert(node.left, key)
        elif key > node.key:
            node.right = self._insert(node.right, key)
        return node

    def search(self, key):
        return self._search(self.root, key)

    def _search(self, node, key):
        if node is None or node.key == key:
            return node
        if key < node.key:
            return self._search(node.left, key)
        return self._search(node.right, key)

    def delete(self, key):
        self.root = self._delete(self.root, key)

    def _delete(self, node, key):
        if node is None:
            return None
        if key < node.key:
            node.left = self._delete(node.left, key)
        elif key > node.key:
            node.right = self._delete(node.right, key)
        else:
            # ケース1: 葉ノードまたは子が1つ
            if node.left is None:
                return node.right
            if node.right is None:
                return node.left
            # ケース2: 子が2つ → 右部分木の最小値で置換
            successor = self._find_min(node.right)
            node.key = successor.key
            node.right = self._delete(node.right, successor.key)
        return node

    def _find_min(self, node):
        while node.left:
            node = node.left
        return node

    def inorder(self):
        result = []
        self._inorder(self.root, result)
        return result

    def _inorder(self, node, result):
        if node:
            self._inorder(node.left, result)
            result.append(node.key)
            self._inorder(node.right, result)

# 使用例
bst = BST()
for key in [5, 3, 7, 1, 4, 6, 8]:
    bst.insert(key)
print(bst.inorder())        # [1, 3, 4, 5, 6, 7, 8]
print(bst.search(4).key)    # 4
bst.delete(3)
print(bst.inorder())        # [1, 4, 5, 6, 7, 8]

3.4 AVL木(AVL Tree)

AVL木は、各ノードにおいて左右の部分木の高さの差(バランスファクター)が -1, 0, 1 のいずれかであることを保証する自己平衡二分探索木である。

回転操作

バランスが崩れた場合、以下の回転操作で修正する。

  1. 右回転(LL回転): 左の子が重い場合
  2. 左回転(RR回転): 右の子が重い場合
  3. 左右回転(LR回転): 左の子の右が重い場合
  4. 右左回転(RL回転): 右の子の左が重い場合

Python実装例

class AVLNode:
    def __init__(self, key):
        self.key = key
        self.left = None
        self.right = None
        self.height = 1

class AVLTree:
    def _height(self, node):
        return node.height if node else 0

    def _balance_factor(self, node):
        return self._height(node.left) - self._height(node.right) if node else 0

    def _update_height(self, node):
        node.height = 1 + max(self._height(node.left), self._height(node.right))

    def _rotate_right(self, y):
        """右回転"""
        x = y.left
        t = x.right
        x.right = y
        y.left = t
        self._update_height(y)
        self._update_height(x)
        return x

    def _rotate_left(self, x):
        """左回転"""
        y = x.right
        t = y.left
        y.left = x
        x.right = t
        self._update_height(x)
        self._update_height(y)
        return y

    def _balance(self, node):
        self._update_height(node)
        bf = self._balance_factor(node)

        # 左が重い
        if bf > 1:
            if self._balance_factor(node.left) < 0:
                node.left = self._rotate_left(node.left)  # LR
            return self._rotate_right(node)  # LL

        # 右が重い
        if bf < -1:
            if self._balance_factor(node.right) > 0:
                node.right = self._rotate_right(node.right)  # RL
            return self._rotate_left(node)  # RR

        return node

    def insert(self, root, key):
        if root is None:
            return AVLNode(key)
        if key < root.key:
            root.left = self.insert(root.left, key)
        elif key > root.key:
            root.right = self.insert(root.right, key)
        else:
            return root
        return self._balance(root)

    def delete(self, root, key):
        if root is None:
            return None
        if key < root.key:
            root.left = self.delete(root.left, key)
        elif key > root.key:
            root.right = self.delete(root.right, key)
        else:
            if root.left is None:
                return root.right
            if root.right is None:
                return root.left
            # 右部分木の最小値で置換
            min_node = root.right
            while min_node.left:
                min_node = min_node.left
            root.key = min_node.key
            root.right = self.delete(root.right, min_node.key)
        return self._balance(root)

# 使用例
avl = AVLTree()
root = None
for key in [10, 20, 30, 40, 50, 25]:
    root = avl.insert(root, key)
# 木は自動的にバランスされる

Java実装例

public class AVLTree {
    static class Node {
        int key, height;
        Node left, right;
        Node(int key) {
            this.key = key;
            this.height = 1;
        }
    }

    private int height(Node n) { return n == null ? 0 : n.height; }
    private int balanceFactor(Node n) { return n == null ? 0 : height(n.left) - height(n.right); }

    private void updateHeight(Node n) {
        n.height = 1 + Math.max(height(n.left), height(n.right));
    }

    private Node rotateRight(Node y) {
        Node x = y.left;
        Node t = x.right;
        x.right = y;
        y.left = t;
        updateHeight(y);
        updateHeight(x);
        return x;
    }

    private Node rotateLeft(Node x) {
        Node y = x.right;
        Node t = y.left;
        y.left = x;
        x.right = t;
        updateHeight(x);
        updateHeight(y);
        return y;
    }

    private Node balance(Node node) {
        updateHeight(node);
        int bf = balanceFactor(node);
        if (bf > 1) {
            if (balanceFactor(node.left) < 0) node.left = rotateLeft(node.left);
            return rotateRight(node);
        }
        if (bf < -1) {
            if (balanceFactor(node.right) > 0) node.right = rotateRight(node.right);
            return rotateLeft(node);
        }
        return node;
    }

    public Node insert(Node node, int key) {
        if (node == null) return new Node(key);
        if (key < node.key) node.left = insert(node.left, key);
        else if (key > node.key) node.right = insert(node.right, key);
        else return node;
        return balance(node);
    }
}

3.5 赤黒木(Red-Black Tree)

赤黒木は、以下の性質を持つ自己平衡二分探索木である。

  1. 各ノードは赤か黒のいずれか
  2. ルートは黒
  3. すべての葉(NIL)は黒
  4. 赤ノードの子は両方とも黒(赤が連続しない)
  5. 任意のノードからその子孫の葉までの各パスに含まれる黒ノードの数は同じ

AVL木と比較して、赤黒木は挿入・削除時の回転回数が少ないため、頻繁に更新が行われる場面で有利である。Java の TreeMap、C++ の std::map は赤黒木で実装されている。

計算量

操作計算量
探索O(log n)
挿入O(log n)
削除O(log n)

Python実装例(簡略版)

class RBColor:
    RED = True
    BLACK = False

class RBNode:
    def __init__(self, key, color=RBColor.RED):
        self.key = key
        self.color = color
        self.left = None
        self.right = None
        self.parent = None

class RedBlackTree:
    def __init__(self):
        self.NIL = RBNode(None, RBColor.BLACK)
        self.root = self.NIL

    def _rotate_left(self, x):
        y = x.right
        x.right = y.left
        if y.left != self.NIL:
            y.left.parent = x
        y.parent = x.parent
        if x.parent is None:
            self.root = y
        elif x == x.parent.left:
            x.parent.left = y
        else:
            x.parent.right = y
        y.left = x
        x.parent = y

    def _rotate_right(self, y):
        x = y.left
        y.left = x.right
        if x.right != self.NIL:
            x.right.parent = y
        x.parent = y.parent
        if y.parent is None:
            self.root = x
        elif y == y.parent.right:
            y.parent.right = x
        else:
            y.parent.left = x
        x.right = y
        y.parent = x

    def insert(self, key):
        node = RBNode(key)
        node.left = self.NIL
        node.right = self.NIL
        parent = None
        current = self.root
        while current != self.NIL:
            parent = current
            if key < current.key:
                current = current.left
            else:
                current = current.right
        node.parent = parent
        if parent is None:
            self.root = node
        elif key < parent.key:
            parent.left = node
        else:
            parent.right = node
        self._fix_insert(node)

    def _fix_insert(self, z):
        while z.parent and z.parent.color == RBColor.RED:
            if z.parent == z.parent.parent.left:
                uncle = z.parent.parent.right
                if uncle.color == RBColor.RED:
                    # Case 1: 叔父が赤
                    z.parent.color = RBColor.BLACK
                    uncle.color = RBColor.BLACK
                    z.parent.parent.color = RBColor.RED
                    z = z.parent.parent
                else:
                    if z == z.parent.right:
                        # Case 2: 叔父が黒、zが右の子
                        z = z.parent
                        self._rotate_left(z)
                    # Case 3: 叔父が黒、zが左の子
                    z.parent.color = RBColor.BLACK
                    z.parent.parent.color = RBColor.RED
                    self._rotate_right(z.parent.parent)
            else:
                # 左右対称
                uncle = z.parent.parent.left
                if uncle.color == RBColor.RED:
                    z.parent.color = RBColor.BLACK
                    uncle.color = RBColor.BLACK
                    z.parent.parent.color = RBColor.RED
                    z = z.parent.parent
                else:
                    if z == z.parent.left:
                        z = z.parent
                        self._rotate_right(z)
                    z.parent.color = RBColor.BLACK
                    z.parent.parent.color = RBColor.RED
                    self._rotate_left(z.parent.parent)
        self.root.color = RBColor.BLACK

3.6 B木(B-Tree)

B木は、データベースやファイルシステムで広く使われる自己平衡探索木である。各ノードが複数のキーと子を持てるため、ディスクアクセスを最小化できる。

性質(次数 t のB木)

  • 各ノードは最大 2t-1 個のキーを持つ
  • 各ノードは最大 2t 個の子を持つ
  • ルート以外の各ノードは最低 t-1 個のキーを持つ
  • すべての葉は同じ深さ

Python実装例(簡略版)

class BTreeNode:
    def __init__(self, t, leaf=True):
        self.t = t          # 最小次数
        self.keys = []       # キーのリスト
        self.children = []   # 子ノードのリスト
        self.leaf = leaf

class BTree:
    def __init__(self, t):
        self.t = t
        self.root = BTreeNode(t)

    def search(self, node, key):
        i = 0
        while i < len(node.keys) and key > node.keys[i]:
            i += 1
        if i < len(node.keys) and key == node.keys[i]:
            return (node, i)
        if node.leaf:
            return None
        return self.search(node.children[i], key)

    def insert(self, key):
        root = self.root
        if len(root.keys) == 2 * self.t - 1:
            new_root = BTreeNode(self.t, leaf=False)
            new_root.children.append(self.root)
            self._split_child(new_root, 0)
            self.root = new_root
        self._insert_non_full(self.root, key)

    def _split_child(self, parent, i):
        t = self.t
        child = parent.children[i]
        new_node = BTreeNode(t, child.leaf)
        parent.keys.insert(i, child.keys[t - 1])
        parent.children.insert(i + 1, new_node)
        new_node.keys = child.keys[t:]
        child.keys = child.keys[:t - 1]
        if not child.leaf:
            new_node.children = child.children[t:]
            child.children = child.children[:t]

    def _insert_non_full(self, node, key):
        i = len(node.keys) - 1
        if node.leaf:
            node.keys.append(None)
            while i >= 0 and key < node.keys[i]:
                node.keys[i + 1] = node.keys[i]
                i -= 1
            node.keys[i + 1] = key
        else:
            while i >= 0 and key < node.keys[i]:
                i -= 1
            i += 1
            if len(node.children[i].keys) == 2 * self.t - 1:
                self._split_child(node, i)
                if key > node.keys[i]:
                    i += 1
            self._insert_non_full(node.children[i], key)

3.7 トライ木(Trie / Prefix Tree)

トライ木は、文字列の集合を効率的に格納・検索するためのツリー構造である。各エッジが文字に対応し、ルートから葉までのパスが一つの文字列を表す。

計算量(文字列の長さを m とする)

操作計算量
挿入O(m)
探索O(m)
前方一致探索O(m + k) ※k は結果数

Python実装例

class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_end = False
        self.count = 0  # この接頭辞を持つ文字列の数

class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word: str):
        node = self.root
        for char in word:
            if char not in node.children:
                node.children[char] = TrieNode()
            node = node.children[char]
            node.count += 1
        node.is_end = True

    def search(self, word: str) -> bool:
        node = self._find_node(word)
        return node is not None and node.is_end

    def starts_with(self, prefix: str) -> bool:
        return self._find_node(prefix) is not None

    def count_prefix(self, prefix: str) -> int:
        node = self._find_node(prefix)
        return node.count if node else 0

    def _find_node(self, prefix: str):
        node = self.root
        for char in prefix:
            if char not in node.children:
                return None
            node = node.children[char]
        return node

    def get_all_words(self, prefix: str = "") -> list:
        """前方一致するすべての文字列を返す"""
        node = self._find_node(prefix)
        if node is None:
            return []
        result = []
        self._collect_words(node, prefix, result)
        return result

    def _collect_words(self, node, prefix, result):
        if node.is_end:
            result.append(prefix)
        for char, child in node.children.items():
            self._collect_words(child, prefix + char, result)

# 使用例
trie = Trie()
for word in ["apple", "app", "application", "banana", "band"]:
    trie.insert(word)

print(trie.search("app"))         # True
print(trie.search("ap"))          # False
print(trie.starts_with("app"))    # True
print(trie.count_prefix("app"))   # 3
print(trie.get_all_words("app"))  # ['apple', 'app', 'application']

TypeScript実装例

class TrieNode {
    children: Map<string, TrieNode> = new Map();
    isEnd = false;
}

class Trie {
    private root = new TrieNode();

    insert(word: string): void {
        let node = this.root;
        for (const ch of word) {
            if (!node.children.has(ch)) {
                node.children.set(ch, new TrieNode());
            }
            node = node.children.get(ch)!;
        }
        node.isEnd = true;
    }

    search(word: string): boolean {
        const node = this.findNode(word);
        return node !== null && node.isEnd;
    }

    startsWith(prefix: string): boolean {
        return this.findNode(prefix) !== null;
    }

    private findNode(prefix: string): TrieNode | null {
        let node = this.root;
        for (const ch of prefix) {
            if (!node.children.has(ch)) return null;
            node = node.children.get(ch)!;
        }
        return node;
    }
}

3.8 セグメント木(Segment Tree)

セグメント木は、配列の区間に対するクエリ(区間の合計、最小値、最大値など)と要素の更新を効率的に行うデータ構造である。

計算量

操作計算量
構築O(n)
区間クエリO(log n)
要素の更新O(log n)

Python実装例

class SegmentTree:
    def __init__(self, data):
        self.n = len(data)
        self.tree = [0] * (4 * self.n)
        self._build(data, 1, 0, self.n - 1)

    def _build(self, data, node, start, end):
        if start == end:
            self.tree[node] = data[start]
        else:
            mid = (start + end) // 2
            self._build(data, 2 * node, start, mid)
            self._build(data, 2 * node + 1, mid + 1, end)
            self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]

    def update(self, index, value):
        """index番目の要素をvalueに更新"""
        self._update(1, 0, self.n - 1, index, value)

    def _update(self, node, start, end, index, value):
        if start == end:
            self.tree[node] = value
        else:
            mid = (start + end) // 2
            if index <= mid:
                self._update(2 * node, start, mid, index, value)
            else:
                self._update(2 * node + 1, mid + 1, end, index, value)
            self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]

    def query(self, left, right):
        """[left, right] の区間合計を返す"""
        return self._query(1, 0, self.n - 1, left, right)

    def _query(self, node, start, end, left, right):
        if right < start or end < left:
            return 0  # 区間外
        if left <= start and end <= right:
            return self.tree[node]  # 完全に含まれる
        mid = (start + end) // 2
        return (self._query(2 * node, start, mid, left, right) +
                self._query(2 * node + 1, mid + 1, end, left, right))

# 遅延伝播付きセグメント木(区間更新対応)
class LazySegTree:
    def __init__(self, data):
        self.n = len(data)
        self.tree = [0] * (4 * self.n)
        self.lazy = [0] * (4 * self.n)
        self._build(data, 1, 0, self.n - 1)

    def _build(self, data, node, start, end):
        if start == end:
            self.tree[node] = data[start]
        else:
            mid = (start + end) // 2
            self._build(data, 2 * node, start, mid)
            self._build(data, 2 * node + 1, mid + 1, end)
            self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]

    def _push_down(self, node, start, end):
        if self.lazy[node] != 0:
            mid = (start + end) // 2
            self._apply(2 * node, start, mid, self.lazy[node])
            self._apply(2 * node + 1, mid + 1, end, self.lazy[node])
            self.lazy[node] = 0

    def _apply(self, node, start, end, val):
        self.tree[node] += val * (end - start + 1)
        self.lazy[node] += val

    def range_update(self, left, right, val):
        """[left, right] の全要素にvalを加算"""
        self._range_update(1, 0, self.n - 1, left, right, val)

    def _range_update(self, node, start, end, left, right, val):
        if right < start or end < left:
            return
        if left <= start and end <= right:
            self._apply(node, start, end, val)
            return
        self._push_down(node, start, end)
        mid = (start + end) // 2
        self._range_update(2 * node, start, mid, left, right, val)
        self._range_update(2 * node + 1, mid + 1, end, left, right, val)
        self.tree[node] = self.tree[2 * node] + self.tree[2 * node + 1]

    def query(self, left, right):
        return self._query(1, 0, self.n - 1, left, right)

    def _query(self, node, start, end, left, right):
        if right < start or end < left:
            return 0
        if left <= start and end <= right:
            return self.tree[node]
        self._push_down(node, start, end)
        mid = (start + end) // 2
        return (self._query(2 * node, start, mid, left, right) +
                self._query(2 * node + 1, mid + 1, end, left, right))

# 使用例
data = [1, 3, 5, 7, 9, 11]
seg = SegmentTree(data)
print(seg.query(1, 3))   # 3 + 5 + 7 = 15
seg.update(2, 10)         # 5 → 10 に更新
print(seg.query(1, 3))   # 3 + 10 + 7 = 20

3.9 ツリー構造の比較まとめ

構造探索挿入削除特徴
二分探索木O(log n)~O(n)O(log n)~O(n)O(log n)~O(n)単純だが偏りうる
AVL木O(log n)O(log n)O(log n)厳密にバランス、探索が高速
赤黒木O(log n)O(log n)O(log n)挿入/削除の回転が少ない
B木O(log n)O(log n)O(log n)ディスクアクセス最適化
トライ木O(m)O(m)O(m)文字列操作に特化
セグメント木O(log n)O(log n)-区間クエリに特化

4. ハッシュテーブルとグラフ

4.1 ハッシュテーブル(Hash Table)

ハッシュテーブルは、キーと値のペアを格納するデータ構造で、ハッシュ関数を用いてキーからインデックスを計算することで、平均 O(1) での検索・挿入・削除を実現する。

計算量

操作平均最悪
挿入O(1)O(n)
探索O(1)O(n)
削除O(1)O(n)

最悪ケースは、すべてのキーが同じハッシュ値に衝突した場合に発生する。

4.1.1 ハッシュ関数

良いハッシュ関数の条件:

  1. 決定性: 同じ入力に対して常に同じ出力
  2. 一様分布: 出力がバケット全体に均等に分散
  3. 高速: 計算が高速であること
# 簡単なハッシュ関数の例
def simple_hash(key: str, size: int) -> int:
    """文字列キーに対する単純なハッシュ関数"""
    h = 0
    for char in key:
        h = (h * 31 + ord(char)) % size
    return h

# Python の組み込みハッシュ
print(hash("hello"))    # 整数値が返る
print(hash(42))
print(hash((1, 2, 3)))  # タプルはハッシュ可能

4.1.2 衝突解決法

チェイニング(Chaining): 各バケットに連結リストを格納する方法。

class HashTableChaining:
    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):
        index = self._hash(key)
        bucket = self.buckets[index]
        for i, (k, v) in enumerate(bucket):
            if k == key:
                bucket[i] = (key, value)
                return
        bucket.append((key, value))
        self.size += 1
        if self.size / self.capacity > self.load_factor_threshold:
            self._resize()

    def get(self, key):
        index = self._hash(key)
        for k, v in self.buckets[index]:
            if k == key:
                return v
        raise KeyError(key)

    def remove(self, key):
        index = self._hash(key)
        bucket = self.buckets[index]
        for i, (k, v) in enumerate(bucket):
            if k == key:
                bucket.pop(i)
                self.size -= 1
                return v
        raise KeyError(key)

    def _resize(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)

    def __contains__(self, key):
        index = self._hash(key)
        return any(k == key for k, _ in self.buckets[index])

オープンアドレス法(Open Addressing): 衝突時に別のバケットを探す方法。

class HashTableOpenAddressing:
    EMPTY = object()
    DELETED = object()

    def __init__(self, capacity=16):
        self.capacity = capacity
        self.size = 0
        self.keys = [self.EMPTY] * capacity
        self.values = [None] * capacity

    def _hash(self, key, attempt):
        # 線形探索法
        return (hash(key) + attempt) % self.capacity

    def put(self, key, value):
        if self.size >= self.capacity * 0.7:
            self._resize()
        for i in range(self.capacity):
            index = self._hash(key, i)
            if self.keys[index] is self.EMPTY or self.keys[index] is self.DELETED:
                self.keys[index] = key
                self.values[index] = value
                self.size += 1
                return
            if self.keys[index] == key:
                self.values[index] = value
                return
        raise RuntimeError("Hash table is full")

    def get(self, key):
        for i in range(self.capacity):
            index = self._hash(key, i)
            if self.keys[index] is self.EMPTY:
                raise KeyError(key)
            if self.keys[index] == key:
                return self.values[index]
        raise KeyError(key)

    def remove(self, key):
        for i in range(self.capacity):
            index = self._hash(key, i)
            if self.keys[index] is self.EMPTY:
                raise KeyError(key)
            if self.keys[index] == key:
                self.keys[index] = self.DELETED
                val = self.values[index]
                self.values[index] = None
                self.size -= 1
                return val
        raise KeyError(key)

    def _resize(self):
        old_keys = self.keys
        old_values = self.values
        self.capacity *= 2
        self.keys = [self.EMPTY] * self.capacity
        self.values = [None] * self.capacity
        self.size = 0
        for i in range(len(old_keys)):
            if old_keys[i] is not self.EMPTY and old_keys[i] is not self.DELETED:
                self.put(old_keys[i], old_values[i])

Java実装例

import java.util.*;

public class HashTableExample {
    // JavaのHashMapの使用例
    public static void hashMapExample() {
        Map<String, Integer> map = new HashMap<>();
        map.put("apple", 1);
        map.put("banana", 2);
        map.put("cherry", 3);

        System.out.println(map.get("banana"));    // 2
        System.out.println(map.containsKey("apple")); // true
        map.remove("cherry");

        // 走査
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }

    // 簡易チェイニングハッシュテーブル
    static class SimpleHashMap<K, V> {
        private static final int DEFAULT_CAPACITY = 16;
        private List<Map.Entry<K, V>>[] buckets;
        private int size;

        @SuppressWarnings("unchecked")
        public SimpleHashMap() {
            buckets = new LinkedList[DEFAULT_CAPACITY];
            for (int i = 0; i < DEFAULT_CAPACITY; i++) {
                buckets[i] = new LinkedList<>();
            }
        }

        private int getBucketIndex(K key) {
            return Math.abs(key.hashCode()) % buckets.length;
        }

        public void put(K key, V value) {
            int index = getBucketIndex(key);
            for (Map.Entry<K, V> entry : buckets[index]) {
                if (entry.getKey().equals(key)) {
                    entry.setValue(value);
                    return;
                }
            }
            buckets[index].add(new AbstractMap.SimpleEntry<>(key, value));
            size++;
        }

        public V get(K key) {
            int index = getBucketIndex(key);
            for (Map.Entry<K, V> entry : buckets[index]) {
                if (entry.getKey().equals(key)) return entry.getValue();
            }
            return null;
        }

        public int size() { return size; }
    }
}

TypeScript実装例

class HashMap<K, V> {
    private buckets: [K, V][][];
    private _size = 0;
    private capacity: number;

    constructor(capacity = 16) {
        this.capacity = capacity;
        this.buckets = Array.from({ length: capacity }, () => []);
    }

    private hash(key: K): number {
        const str = String(key);
        let h = 0;
        for (let i = 0; i < str.length; i++) {
            h = (h * 31 + str.charCodeAt(i)) | 0;
        }
        return Math.abs(h) % this.capacity;
    }

    set(key: K, value: V): void {
        const index = this.hash(key);
        const bucket = this.buckets[index];
        for (let i = 0; i < bucket.length; i++) {
            if (bucket[i][0] === key) {
                bucket[i][1] = value;
                return;
            }
        }
        bucket.push([key, value]);
        this._size++;
    }

    get(key: K): V | undefined {
        const index = this.hash(key);
        for (const [k, v] of this.buckets[index]) {
            if (k === key) return v;
        }
        return undefined;
    }

    has(key: K): boolean {
        return this.get(key) !== undefined;
    }

    delete(key: K): boolean {
        const index = this.hash(key);
        const bucket = this.buckets[index];
        for (let i = 0; i < bucket.length; i++) {
            if (bucket[i][0] === key) {
                bucket.splice(i, 1);
                this._size--;
                return true;
            }
        }
        return false;
    }

    get size(): number { return this._size; }
}

4.2 グラフ(Graph)

グラフは、頂点(ノード)の集合と、頂点間を結ぶ辺(エッジ)の集合で構成されるデータ構造である。

4.2.1 グラフの種類

種類説明
無向グラフ辺に方向がない
有向グラフ辺に方向がある
重み付きグラフ辺に重み(コスト)がある
連結グラフすべての頂点間にパスが存在
DAG有向非巡回グラフ

4.2.2 グラフの表現方法

# 隣接行列(Adjacency Matrix)
# メモリ: O(V^2), 辺の存在確認: O(1)
class GraphMatrix:
    def __init__(self, num_vertices):
        self.V = num_vertices
        self.matrix = [[0] * num_vertices for _ in range(num_vertices)]

    def add_edge(self, u, v, weight=1):
        self.matrix[u][v] = weight
        self.matrix[v][u] = weight  # 無向グラフの場合

    def has_edge(self, u, v):
        return self.matrix[u][v] != 0

    def neighbors(self, v):
        return [i for i in range(self.V) if self.matrix[v][i] != 0]


# 隣接リスト(Adjacency List)
# メモリ: O(V+E), 辺の存在確認: O(degree)
class GraphList:
    def __init__(self, num_vertices):
        self.V = num_vertices
        self.adj = [[] for _ in range(num_vertices)]

    def add_edge(self, u, v, weight=1):
        self.adj[u].append((v, weight))
        self.adj[v].append((u, weight))  # 無向グラフの場合

    def neighbors(self, v):
        return self.adj[v]

4.2.3 幅優先探索(BFS)

from collections import deque

def bfs(graph, start):
    """BFS: O(V + E)"""
    visited = set()
    queue = deque([start])
    visited.add(start)
    order = []

    while queue:
        vertex = queue.popleft()
        order.append(vertex)
        for neighbor, _ in graph.neighbors(vertex):
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)
    return order

# BFSで最短経路(重みなしグラフ)
def bfs_shortest_path(graph, start, end):
    visited = {start}
    queue = deque([(start, [start])])
    while queue:
        vertex, path = queue.popleft()
        if vertex == end:
            return path
        for neighbor, _ in graph.neighbors(vertex):
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append((neighbor, path + [neighbor]))
    return None  # パスが存在しない

Java BFS実装

import java.util.*;

public class GraphBFS {
    private List<List<int[]>> adj;
    private int V;

    public GraphBFS(int V) {
        this.V = V;
        adj = new ArrayList<>();
        for (int i = 0; i < V; i++) adj.add(new ArrayList<>());
    }

    public void addEdge(int u, int v, int w) {
        adj.get(u).add(new int[]{v, w});
        adj.get(v).add(new int[]{u, w});
    }

    public List<Integer> bfs(int start) {
        boolean[] visited = new boolean[V];
        Queue<Integer> queue = new LinkedList<>();
        List<Integer> order = new ArrayList<>();
        visited[start] = true;
        queue.offer(start);
        while (!queue.isEmpty()) {
            int v = queue.poll();
            order.add(v);
            for (int[] edge : adj.get(v)) {
                if (!visited[edge[0]]) {
                    visited[edge[0]] = true;
                    queue.offer(edge[0]);
                }
            }
        }
        return order;
    }
}

4.2.4 深さ優先探索(DFS)

def dfs_recursive(graph, start, visited=None):
    """再帰的DFS: O(V + E)"""
    if visited is None:
        visited = set()
    visited.add(start)
    order = [start]
    for neighbor, _ in graph.neighbors(start):
        if neighbor not in visited:
            order.extend(dfs_recursive(graph, neighbor, visited))
    return order

def dfs_iterative(graph, start):
    """反復的DFS(スタック使用)"""
    visited = set()
    stack = [start]
    order = []
    while stack:
        vertex = stack.pop()
        if vertex in visited:
            continue
        visited.add(vertex)
        order.append(vertex)
        for neighbor, _ in graph.neighbors(vertex):
            if neighbor not in visited:
                stack.append(neighbor)
    return order

# DFSの応用: 閉路検出(有向グラフ)
def has_cycle_directed(graph):
    WHITE, GRAY, BLACK = 0, 1, 2
    color = [WHITE] * graph.V

    def dfs(v):
        color[v] = GRAY
        for neighbor, _ in graph.neighbors(v):
            if color[neighbor] == GRAY:
                return True  # 閉路発見
            if color[neighbor] == WHITE and dfs(neighbor):
                return True
        color[v] = BLACK
        return False

    for v in range(graph.V):
        if color[v] == WHITE:
            if dfs(v):
                return True
    return False

4.2.5 ダイクストラ法(Dijkstra's Algorithm)

重み付きグラフ(非負の重み)での単一始点最短経路を求めるアルゴリズム。

import heapq

def dijkstra(graph, start):
    """
    ダイクストラ法: O((V + E) log V) (優先度付きキュー使用時)
    """
    dist = [float('inf')] * graph.V
    dist[start] = 0
    prev = [-1] * graph.V
    pq = [(0, start)]  # (距離, 頂点)

    while pq:
        d, u = heapq.heappop(pq)
        if d > dist[u]:
            continue  # 既により短い経路が見つかっている
        for v, weight in graph.neighbors(u):
            new_dist = dist[u] + weight
            if new_dist < dist[v]:
                dist[v] = new_dist
                prev[v] = u
                heapq.heappush(pq, (new_dist, v))

    return dist, prev

def reconstruct_path(prev, start, end):
    """最短経路の復元"""
    path = []
    current = end
    while current != -1:
        path.append(current)
        current = prev[current]
    path.reverse()
    return path if path[0] == start else []

# 使用例
g = GraphList(6)
g.add_edge(0, 1, 4)
g.add_edge(0, 2, 2)
g.add_edge(1, 3, 5)
g.add_edge(2, 1, 1)
g.add_edge(2, 3, 8)
g.add_edge(3, 4, 2)
g.add_edge(4, 5, 6)

dist, prev = dijkstra(g, 0)
print(f"0から各頂点への最短距離: {dist}")
print(f"0から4への最短経路: {reconstruct_path(prev, 0, 4)}")

Java ダイクストラ実装

import java.util.*;

public class Dijkstra {
    public static int[] dijkstra(List<List<int[]>> adj, int V, int start) {
        int[] dist = new int[V];
        Arrays.fill(dist, Integer.MAX_VALUE);
        dist[start] = 0;
        // {距離, 頂点}
        PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
        pq.offer(new int[]{0, start});

        while (!pq.isEmpty()) {
            int[] curr = pq.poll();
            int d = curr[0], u = curr[1];
            if (d > dist[u]) continue;
            for (int[] edge : adj.get(u)) {
                int v = edge[0], w = edge[1];
                if (dist[u] + w < dist[v]) {
                    dist[v] = dist[u] + w;
                    pq.offer(new int[]{dist[v], v});
                }
            }
        }
        return dist;
    }
}

4.2.6 ベルマンフォード法(Bellman-Ford Algorithm)

負の重みを含むグラフでの単一始点最短経路を求める。負の閉路の検出も可能。

def bellman_ford(graph, start):
    """
    ベルマンフォード法: O(V * E)
    負の重みに対応、負の閉路を検出可能
    """
    dist = [float('inf')] * graph.V
    dist[start] = 0
    prev = [-1] * graph.V

    # V-1 回の緩和
    for _ in range(graph.V - 1):
        updated = False
        for u in range(graph.V):
            if dist[u] == float('inf'):
                continue
            for v, weight in graph.neighbors(u):
                if dist[u] + weight < dist[v]:
                    dist[v] = dist[u] + weight
                    prev[v] = u
                    updated = True
        if not updated:
            break  # 更新がなければ早期終了

    # 負の閉路の検出
    for u in range(graph.V):
        if dist[u] == float('inf'):
            continue
        for v, weight in graph.neighbors(u):
            if dist[u] + weight < dist[v]:
                raise ValueError("負の閉路が存在します")

    return dist, prev

TypeScript ベルマンフォード実装

interface Edge {
    from: number;
    to: number;
    weight: number;
}

function bellmanFord(
    V: number, edges: Edge[], start: number
): { dist: number[]; hasNegativeCycle: boolean } {
    const dist = new Array(V).fill(Infinity);
    dist[start] = 0;

    for (let i = 0; i < V - 1; i++) {
        let updated = false;
        for (const { from, to, weight } of edges) {
            if (dist[from] !== Infinity && dist[from] + weight < dist[to]) {
                dist[to] = dist[from] + weight;
                updated = true;
            }
        }
        if (!updated) break;
    }

    // 負の閉路検出
    for (const { from, to, weight } of edges) {
        if (dist[from] !== Infinity && dist[from] + weight < dist[to]) {
            return { dist, hasNegativeCycle: true };
        }
    }
    return { dist, hasNegativeCycle: false };
}

4.2.7 最小全域木

クラスカル法(Kruskal's Algorithm)

class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        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

def kruskal(num_vertices, edges):
    """
    クラスカル法: O(E log E)
    edges: [(weight, u, v), ...]
    """
    edges.sort()  # 重みでソート
    uf = UnionFind(num_vertices)
    mst = []
    total_weight = 0

    for weight, u, v in edges:
        if uf.union(u, v):
            mst.append((u, v, weight))
            total_weight += weight
            if len(mst) == num_vertices - 1:
                break

    return mst, total_weight

# 使用例
edges = [(4, 0, 1), (2, 0, 2), (1, 1, 2), (5, 1, 3), (8, 2, 3), (2, 3, 4)]
mst, total = kruskal(5, edges)
print(f"MST: {mst}, 総重み: {total}")

プリム法(Prim's Algorithm)

import heapq

def prim(graph, start=0):
    """
    プリム法: O((V + E) log V)
    """
    visited = set()
    mst = []
    total_weight = 0
    pq = [(0, start, -1)]  # (重み, 頂点, 接続元)

    while pq and len(visited) < graph.V:
        weight, u, parent = heapq.heappop(pq)
        if u in visited:
            continue
        visited.add(u)
        if parent != -1:
            mst.append((parent, u, weight))
            total_weight += weight
        for v, w in graph.neighbors(u):
            if v not in visited:
                heapq.heappush(pq, (w, v, u))

    return mst, total_weight

4.3 最短経路アルゴリズムの比較

アルゴリズム計算量負の重み負の閉路検出用途
ダイクストラO((V+E) log V)不可不可非負の重みの最短経路
ベルマンフォードO(VE)可能可能負の重みを含む最短経路
ワーシャルフロイドO(V^3)可能可能全点対最短経路

ワーシャルフロイド法

def floyd_warshall(graph_matrix):
    """
    ワーシャルフロイド法: O(V^3)
    全頂点間の最短距離を求める
    """
    V = len(graph_matrix)
    dist = [row[:] for row in graph_matrix]

    for k in range(V):
        for i in range(V):
            for j in range(V):
                if dist[i][k] + dist[k][j] < dist[i][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]
    return dist

# 使用例
INF = float('inf')
graph = [
    [0,   3,   INF, 5],
    [2,   0,   INF, 4],
    [INF, 1,   0,   INF],
    [INF, INF, 2,   0]
]
result = floyd_warshall(graph)
for row in result:
    print(row)

5. ソートアルゴリズム

5.1 ソートアルゴリズム比較表

アルゴリズム最良平均最悪空間安定性備考
バブルソートO(n)O(n^2)O(n^2)O(1)安定教育用
選択ソートO(n^2)O(n^2)O(n^2)O(1)不安定交換回数が少ない
挿入ソートO(n)O(n^2)O(n^2)O(1)安定ほぼ整列済みに高速
マージソートO(n log n)O(n log n)O(n log n)O(n)安定安定で予測可能
クイックソートO(n log n)O(n log n)O(n^2)O(log n)不安定実用的に最速
ヒープソートO(n log n)O(n log n)O(n log n)O(1)不安定インプレース
基数ソートO(nk)O(nk)O(nk)O(n+k)安定非比較ソート
計数ソートO(n+k)O(n+k)O(n+k)O(k)安定非比較ソート

5.2 バブルソート(Bubble Sort)

隣接する要素を比較し、順序が逆なら交換する操作を繰り返す。

def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        swapped = False
        for j in range(0, n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                swapped = True
        if not swapped:
            break  # 交換がなければ早期終了
    return arr
public static void bubbleSort(int[] arr) {
    int n = arr.length;
    for (int i = 0; i < n; i++) {
        boolean swapped = false;
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
                swapped = true;
            }
        }
        if (!swapped) break;
    }
}
function bubbleSort(arr: number[]): number[] {
    const n = arr.length;
    for (let i = 0; i < n; i++) {
        let swapped = false;
        for (let j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
                swapped = true;
            }
        }
        if (!swapped) break;
    }
    return arr;
}

5.3 選択ソート(Selection Sort)

未ソート部分から最小値を見つけ、先頭と交換する操作を繰り返す。

def selection_sort(arr):
    n = len(arr)
    for i in range(n):
        min_idx = i
        for j in range(i + 1, n):
            if arr[j] < arr[min_idx]:
                min_idx = j
        arr[i], arr[min_idx] = arr[min_idx], arr[i]
    return arr
public static void selectionSort(int[] arr) {
    int n = arr.length;
    for (int i = 0; i < n; i++) {
        int minIdx = i;
        for (int j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIdx]) minIdx = j;
        }
        int tmp = arr[i];
        arr[i] = arr[minIdx];
        arr[minIdx] = tmp;
    }
}
function selectionSort(arr: number[]): number[] {
    const n = arr.length;
    for (let i = 0; i < n; i++) {
        let minIdx = i;
        for (let j = i + 1; j < n; j++) {
            if (arr[j] < arr[minIdx]) minIdx = j;
        }
        [arr[i], arr[minIdx]] = [arr[minIdx], arr[i]];
    }
    return arr;
}

5.4 挿入ソート(Insertion Sort)

各要素を、ソート済み部分の適切な位置に挿入する。

def insertion_sort(arr):
    for i in range(1, len(arr)):
        key = arr[i]
        j = i - 1
        while j >= 0 and arr[j] > key:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = key
    return arr
public static void insertionSort(int[] arr) {
    for (int i = 1; i < arr.length; i++) {
        int key = arr[i];
        int j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
}
function insertionSort(arr: number[]): number[] {
    for (let i = 1; i < arr.length; i++) {
        const key = arr[i];
        let j = i - 1;
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = key;
    }
    return arr;
}

5.5 マージソート(Merge Sort)

配列を半分に分割し、再帰的にソートした後、マージする分割統治法ベースのアルゴリズム。

def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    return merge(left, right)

def merge(left, right):
    result = []
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:  # <= で安定ソート
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    result.extend(left[i:])
    result.extend(right[j:])
    return result

# インプレース版マージソート
def merge_sort_inplace(arr, left=0, right=None):
    if right is None:
        right = len(arr) - 1
    if left < right:
        mid = (left + right) // 2
        merge_sort_inplace(arr, left, mid)
        merge_sort_inplace(arr, mid + 1, right)
        merge_inplace(arr, left, mid, right)

def merge_inplace(arr, left, mid, right):
    temp = []
    i, j = left, mid + 1
    while i <= mid and j <= right:
        if arr[i] <= arr[j]:
            temp.append(arr[i])
            i += 1
        else:
            temp.append(arr[j])
            j += 1
    while i <= mid:
        temp.append(arr[i])
        i += 1
    while j <= right:
        temp.append(arr[j])
        j += 1
    for k in range(len(temp)):
        arr[left + k] = temp[k]
public class MergeSort {
    public static void mergeSort(int[] arr, int left, int right) {
        if (left < right) {
            int mid = (left + right) / 2;
            mergeSort(arr, left, mid);
            mergeSort(arr, mid + 1, right);
            merge(arr, left, mid, right);
        }
    }

    private static void merge(int[] arr, int left, int mid, int right) {
        int[] temp = new int[right - left + 1];
        int i = left, j = mid + 1, k = 0;
        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j]) temp[k++] = arr[i++];
            else temp[k++] = arr[j++];
        }
        while (i <= mid) temp[k++] = arr[i++];
        while (j <= right) temp[k++] = arr[j++];
        System.arraycopy(temp, 0, arr, left, temp.length);
    }
}
function mergeSort(arr: number[]): number[] {
    if (arr.length <= 1) return arr;
    const mid = Math.floor(arr.length / 2);
    const left = mergeSort(arr.slice(0, mid));
    const right = mergeSort(arr.slice(mid));
    return mergeSorted(left, right);
}

function mergeSorted(left: number[], right: number[]): number[] {
    const result: number[] = [];
    let i = 0, j = 0;
    while (i < left.length && j < right.length) {
        if (left[i] <= right[j]) result.push(left[i++]);
        else result.push(right[j++]);
    }
    return result.concat(left.slice(i), right.slice(j));
}

5.6 クイックソート(Quick Sort)

ピボットを選び、それより小さい要素と大きい要素に分割する分割統治法ベースのアルゴリズム。

def quick_sort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quick_sort(left) + middle + quick_sort(right)

# インプレース版(Lomutoパーティション)
def quick_sort_inplace(arr, low=0, high=None):
    if high is None:
        high = len(arr) - 1
    if low < high:
        pivot_idx = partition_lomuto(arr, low, high)
        quick_sort_inplace(arr, low, pivot_idx - 1)
        quick_sort_inplace(arr, pivot_idx + 1, high)

def partition_lomuto(arr, low, high):
    pivot = arr[high]
    i = low - 1
    for j in range(low, high):
        if arr[j] <= pivot:
            i += 1
            arr[i], arr[j] = arr[j], arr[i]
    arr[i + 1], arr[high] = arr[high], arr[i + 1]
    return i + 1

# Hoareパーティション(交換回数が少ない)
def partition_hoare(arr, low, high):
    pivot = arr[(low + high) // 2]
    i, j = low - 1, high + 1
    while True:
        i += 1
        while arr[i] < pivot:
            i += 1
        j -= 1
        while arr[j] > pivot:
            j -= 1
        if i >= j:
            return j
        arr[i], arr[j] = arr[j], arr[i]

# 三つの中央値でピボット選択(最悪ケースの回避)
import random

def quick_sort_randomized(arr, low=0, high=None):
    if high is None:
        high = len(arr) - 1
    if low < high:
        # ランダムにピボットを選択
        rand_idx = random.randint(low, high)
        arr[rand_idx], arr[high] = arr[high], arr[rand_idx]
        pivot_idx = partition_lomuto(arr, low, high)
        quick_sort_randomized(arr, low, pivot_idx - 1)
        quick_sort_randomized(arr, pivot_idx + 1, high)
public class QuickSort {
    public static void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            int pivotIdx = partition(arr, low, high);
            quickSort(arr, low, pivotIdx - 1);
            quickSort(arr, pivotIdx + 1, high);
        }
    }

    private static int partition(int[] arr, int low, int high) {
        int pivot = arr[high];
        int i = low - 1;
        for (int j = low; j < high; j++) {
            if (arr[j] <= pivot) {
                i++;
                int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp;
            }
        }
        int tmp = arr[i + 1]; arr[i + 1] = arr[high]; arr[high] = tmp;
        return i + 1;
    }
}
function quickSort(arr: number[], low = 0, high = arr.length - 1): void {
    if (low < high) {
        const pivotIdx = partition(arr, low, high);
        quickSort(arr, low, pivotIdx - 1);
        quickSort(arr, pivotIdx + 1, high);
    }
}

function partition(arr: number[], low: number, high: number): number {
    const pivot = arr[high];
    let i = low - 1;
    for (let j = low; j < high; j++) {
        if (arr[j] <= pivot) {
            i++;
            [arr[i], arr[j]] = [arr[j], arr[i]];
        }
    }
    [arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
    return i + 1;
}

5.7 ヒープソート(Heap Sort)

二分ヒープを構築し、最大値を繰り返し取り出すことでソートする。

def heap_sort(arr):
    n = len(arr)

    def heapify(arr, n, i):
        largest = i
        left = 2 * i + 1
        right = 2 * i + 2
        if left < n and arr[left] > arr[largest]:
            largest = left
        if right < n and arr[right] > arr[largest]:
            largest = right
        if largest != i:
            arr[i], arr[largest] = arr[largest], arr[i]
            heapify(arr, n, largest)

    # 最大ヒープの構築 O(n)
    for i in range(n // 2 - 1, -1, -1):
        heapify(arr, n, i)

    # 要素を一つずつ取り出す
    for i in range(n - 1, 0, -1):
        arr[0], arr[i] = arr[i], arr[0]
        heapify(arr, i, 0)

    return arr
public class HeapSort {
    public static void heapSort(int[] arr) {
        int n = arr.length;
        // 最大ヒープの構築
        for (int i = n / 2 - 1; i >= 0; i--) {
            heapify(arr, n, i);
        }
        // 要素を一つずつ取り出す
        for (int i = n - 1; i > 0; i--) {
            int tmp = arr[0]; arr[0] = arr[i]; arr[i] = tmp;
            heapify(arr, i, 0);
        }
    }

    private static void heapify(int[] arr, int n, int i) {
        int largest = i;
        int left = 2 * i + 1;
        int right = 2 * i + 2;
        if (left < n && arr[left] > arr[largest]) largest = left;
        if (right < n && arr[right] > arr[largest]) largest = right;
        if (largest != i) {
            int tmp = arr[i]; arr[i] = arr[largest]; arr[largest] = tmp;
            heapify(arr, n, largest);
        }
    }
}
function heapSort(arr: number[]): number[] {
    const n = arr.length;

    function heapify(arr: number[], n: number, i: number): void {
        let largest = i;
        const left = 2 * i + 1;
        const right = 2 * i + 2;
        if (left < n && arr[left] > arr[largest]) largest = left;
        if (right < n && arr[right] > arr[largest]) largest = right;
        if (largest !== i) {
            [arr[i], arr[largest]] = [arr[largest], arr[i]];
            heapify(arr, n, largest);
        }
    }

    for (let i = Math.floor(n / 2) - 1; i >= 0; i--) heapify(arr, n, i);
    for (let i = n - 1; i > 0; i--) {
        [arr[0], arr[i]] = [arr[i], arr[0]];
        heapify(arr, i, 0);
    }
    return arr;
}

5.8 基数ソート(Radix Sort)

各桁ごとに安定ソート(通常は計数ソート)を適用する非比較ソート。

def counting_sort_by_digit(arr, exp):
    n = len(arr)
    output = [0] * n
    count = [0] * 10

    for num in arr:
        index = (num // exp) % 10
        count[index] += 1

    for i in range(1, 10):
        count[i] += count[i - 1]

    for i in range(n - 1, -1, -1):
        index = (arr[i] // exp) % 10
        output[count[index] - 1] = arr[i]
        count[index] -= 1

    for i in range(n):
        arr[i] = output[i]

def radix_sort(arr):
    if not arr:
        return arr
    max_val = max(arr)
    exp = 1
    while max_val // exp > 0:
        counting_sort_by_digit(arr, exp)
        exp *= 10
    return arr

5.9 計数ソート(Counting Sort)

要素の値の範囲が限定されている場合に使える非比較ソート。

def counting_sort(arr):
    if not arr:
        return arr
    max_val = max(arr)
    min_val = min(arr)
    range_val = max_val - min_val + 1

    count = [0] * range_val
    output = [0] * len(arr)

    for num in arr:
        count[num - min_val] += 1

    for i in range(1, range_val):
        count[i] += count[i - 1]

    for i in range(len(arr) - 1, -1, -1):
        output[count[arr[i] - min_val] - 1] = arr[i]
        count[arr[i] - min_val] -= 1

    return output
public static int[] countingSort(int[] arr) {
    if (arr.length == 0) return arr;
    int max = Arrays.stream(arr).max().getAsInt();
    int min = Arrays.stream(arr).min().getAsInt();
    int range = max - min + 1;

    int[] count = new int[range];
    int[] output = new int[arr.length];

    for (int num : arr) count[num - min]++;
    for (int i = 1; i < range; i++) count[i] += count[i - 1];
    for (int i = arr.length - 1; i >= 0; i--) {
        output[count[arr[i] - min] - 1] = arr[i];
        count[arr[i] - min]--;
    }
    return output;
}
function countingSort(arr: number[]): number[] {
    if (arr.length === 0) return arr;
    const max = Math.max(...arr);
    const min = Math.min(...arr);
    const range = max - min + 1;

    const count = new Array(range).fill(0);
    const output = new Array(arr.length);

    for (const num of arr) count[num - min]++;
    for (let i = 1; i < range; i++) count[i] += count[i - 1];
    for (let i = arr.length - 1; i >= 0; i--) {
        output[count[arr[i] - min] - 1] = arr[i];
        count[arr[i] - min]--;
    }
    return output;
}

5.10 ソートの選択指針

状況推奨アルゴリズム理由
小さい配列(n < 50)挿入ソートオーバーヘッドが小さい
ほぼ整列済み挿入ソートO(n) に近い
安定ソートが必要マージソート安定で O(n log n) 保証
メモリ制約ありヒープソートO(1) 追加メモリ
一般的な用途クイックソート平均的に最速
整数値の範囲が小さい計数ソート/基数ソートO(n) で動作

6. 探索アルゴリズムと動的計画法

6.1 線形探索(Linear Search)

配列の先頭から順に要素を比較する最も単純な探索法。

def linear_search(arr, target):
    """O(n)"""
    for i, val in enumerate(arr):
        if val == target:
            return i
    return -1
public static int linearSearch(int[] arr, int target) {
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] == target) return i;
    }
    return -1;
}
function linearSearch(arr: number[], target: number): number {
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] === target) return i;
    }
    return -1;
}

6.2 二分探索(Binary Search)

ソート済み配列を半分に分割しながら探索する。

def binary_search(arr, target):
    """O(log n) — 配列はソート済みであること"""
    lo, hi = 0, len(arr) - 1
    while lo <= hi:
        mid = lo + (hi - lo) // 2  # オーバーフロー対策
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            lo = mid + 1
        else:
            hi = mid - 1
    return -1

# lower_bound: target以上の最小インデックス
def lower_bound(arr, target):
    lo, hi = 0, len(arr)
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if arr[mid] < target:
            lo = mid + 1
        else:
            hi = mid
    return lo

# upper_bound: targetより大きい最小インデックス
def upper_bound(arr, target):
    lo, hi = 0, len(arr)
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if arr[mid] <= target:
            lo = mid + 1
        else:
            hi = mid
    return lo

# 二分探索の応用: 答えに対する二分探索
def min_max_binary_search(arr, k):
    """
    配列をk個のグループに分割したときの
    各グループの合計の最大値を最小化する
    """
    def can_split(max_sum):
        count = 1
        current_sum = 0
        for num in arr:
            if current_sum + num > max_sum:
                count += 1
                current_sum = num
                if count > k:
                    return False
            else:
                current_sum += num
        return True

    lo = max(arr)
    hi = sum(arr)
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if can_split(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo
public class BinarySearch {
    public static int binarySearch(int[] arr, int target) {
        int lo = 0, hi = arr.length - 1;
        while (lo <= hi) {
            int mid = lo + (hi - lo) / 2;
            if (arr[mid] == target) return mid;
            else if (arr[mid] < target) lo = mid + 1;
            else hi = mid - 1;
        }
        return -1;
    }

    public static int lowerBound(int[] arr, int target) {
        int lo = 0, hi = arr.length;
        while (lo < hi) {
            int mid = lo + (hi - lo) / 2;
            if (arr[mid] < target) lo = mid + 1;
            else hi = mid;
        }
        return lo;
    }

    public static int upperBound(int[] arr, int target) {
        int lo = 0, hi = arr.length;
        while (lo < hi) {
            int mid = lo + (hi - lo) / 2;
            if (arr[mid] <= target) lo = mid + 1;
            else hi = mid;
        }
        return lo;
    }
}
function binarySearch(arr: number[], target: number): number {
    let lo = 0, hi = arr.length - 1;
    while (lo <= hi) {
        const mid = lo + Math.floor((hi - lo) / 2);
        if (arr[mid] === target) return mid;
        else if (arr[mid] < target) lo = mid + 1;
        else hi = mid - 1;
    }
    return -1;
}

function lowerBound(arr: number[], target: number): number {
    let lo = 0, hi = arr.length;
    while (lo < hi) {
        const mid = lo + Math.floor((hi - lo) / 2);
        if (arr[mid] < target) lo = mid + 1;
        else hi = mid;
    }
    return lo;
}

6.3 動的計画法(Dynamic Programming)の基本概念

動的計画法(DP)は、重複する部分問題を持つ最適化問題を効率的に解くための手法である。

DPの2つの条件

  1. 最適部分構造(Optimal Substructure): 問題の最適解がその部分問題の最適解から構成できる
  2. 重複部分問題(Overlapping Subproblems): 同じ部分問題が繰り返し出現する

DPの2つのアプローチ

アプローチ説明方向
メモ化再帰(Top-down)再帰 + キャッシュ上から下へ
テーブル(Bottom-up)小さい問題から順に解く下から上へ

6.4 フィボナッチ数列(DPの入門例)

# 素朴な再帰 — O(2^n)
def fib_naive(n):
    if n <= 1:
        return n
    return fib_naive(n - 1) + fib_naive(n - 2)

# メモ化再帰(Top-down) — O(n)
from functools import lru_cache

@lru_cache(maxsize=None)
def fib_memo(n):
    if n <= 1:
        return n
    return fib_memo(n - 1) + fib_memo(n - 2)

# テーブル(Bottom-up) — O(n), 空間O(n)
def fib_table(n):
    if n <= 1:
        return n
    dp = [0] * (n + 1)
    dp[1] = 1
    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    return dp[n]

# 空間最適化 — O(n), 空間O(1)
def fib_optimized(n):
    if n <= 1:
        return n
    prev2, prev1 = 0, 1
    for _ in range(2, n + 1):
        curr = prev1 + prev2
        prev2, prev1 = prev1, curr
    return prev1

6.5 ナップサック問題(Knapsack Problem)

0/1ナップサック問題

各アイテムを選ぶか選ばないかの二択。

def knapsack_01(weights, values, capacity):
    """
    0/1ナップサック問題
    O(n * capacity) 時間、O(n * capacity) 空間
    """
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]

    for i in range(1, n + 1):
        for w in range(capacity + 1):
            dp[i][w] = dp[i - 1][w]  # i番目を選ばない
            if weights[i - 1] <= w:
                dp[i][w] = max(
                    dp[i][w],
                    dp[i - 1][w - weights[i - 1]] + values[i - 1]
                )
    return dp[n][capacity]

# 空間最適化版 O(capacity) 空間
def knapsack_01_optimized(weights, values, capacity):
    n = len(weights)
    dp = [0] * (capacity + 1)
    for i in range(n):
        for w in range(capacity, weights[i] - 1, -1):  # 逆順!
            dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
    return dp[capacity]

# 選んだアイテムの復元
def knapsack_01_with_items(weights, values, capacity):
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        for w in range(capacity + 1):
            dp[i][w] = dp[i - 1][w]
            if weights[i - 1] <= w:
                dp[i][w] = max(dp[i][w],
                    dp[i - 1][w - weights[i - 1]] + values[i - 1])

    # アイテムの復元
    items = []
    w = capacity
    for i in range(n, 0, -1):
        if dp[i][w] != dp[i - 1][w]:
            items.append(i - 1)
            w -= weights[i - 1]
    items.reverse()
    return dp[n][capacity], items

# 使用例
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 8
max_val, selected = knapsack_01_with_items(weights, values, capacity)
print(f"最大価値: {max_val}, 選んだアイテム: {selected}")
public class Knapsack {
    public static int knapsack01(int[] weights, int[] values, int capacity) {
        int n = weights.length;
        int[] dp = new int[capacity + 1];
        for (int i = 0; i < n; i++) {
            for (int w = capacity; w >= weights[i]; w--) {
                dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
            }
        }
        return dp[capacity];
    }
}
function knapsack01(
    weights: number[], values: number[], capacity: number
): number {
    const dp = new Array(capacity + 1).fill(0);
    for (let i = 0; i < weights.length; i++) {
        for (let w = capacity; w >= weights[i]; w--) {
            dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
        }
    }
    return dp[capacity];
}

6.6 最長共通部分列(LCS: Longest Common Subsequence)

2つの文字列の最長の共通部分列の長さを求める。

def lcs(s1, s2):
    """O(m * n)"""
    m, n = len(s1), len(s2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]

    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if s1[i - 1] == s2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

    # LCSの復元
    result = []
    i, j = m, n
    while i > 0 and j > 0:
        if s1[i - 1] == s2[j - 1]:
            result.append(s1[i - 1])
            i -= 1
            j -= 1
        elif dp[i - 1][j] > dp[i][j - 1]:
            i -= 1
        else:
            j -= 1
    result.reverse()

    return dp[m][n], ''.join(result)

# 使用例
length, subsequence = lcs("ABCBDAB", "BDCAB")
print(f"LCS長: {length}, LCS: {subsequence}")
# LCS長: 4, LCS: BCAB
public class LCS {
    public static int lcs(String s1, String s2) {
        int m = s1.length(), n = s2.length();
        int[][] dp = new int[m + 1][n + 1];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[m][n];
    }
}

6.7 編集距離(Edit Distance / Levenshtein Distance)

2つの文字列間の最小編集操作回数(挿入・削除・置換)を求める。

def edit_distance(s1, s2):
    """O(m * n)"""
    m, n = len(s1), len(s2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]

    # ベースケース
    for i in range(m + 1):
        dp[i][0] = i
    for j in range(n + 1):
        dp[0][j] = j

    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if s1[i - 1] == s2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1]
            else:
                dp[i][j] = 1 + min(
                    dp[i - 1][j],     # 削除
                    dp[i][j - 1],     # 挿入
                    dp[i - 1][j - 1]  # 置換
                )
    return dp[m][n]

print(edit_distance("kitten", "sitting"))  # 3
public static int editDistance(String s1, String s2) {
    int m = s1.length(), n = s2.length();
    int[][] dp = new int[m + 1][n + 1];
    for (int i = 0; i <= m; i++) dp[i][0] = i;
    for (int j = 0; j <= n; j++) dp[0][j] = j;
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                dp[i][j] = 1 + Math.min(dp[i - 1][j - 1],
                    Math.min(dp[i - 1][j], dp[i][j - 1]));
            }
        }
    }
    return dp[m][n];
}
function editDistance(s1: string, s2: string): number {
    const m = s1.length, n = s2.length;
    const dp: number[][] = Array.from(
        { length: m + 1 }, () => new Array(n + 1).fill(0)
    );
    for (let i = 0; i <= m; i++) dp[i][0] = i;
    for (let j = 0; j <= n; j++) dp[0][j] = j;
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            if (s1[i - 1] === s2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                dp[i][j] = 1 + Math.min(
                    dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]
                );
            }
        }
    }
    return dp[m][n];
}

6.8 最長増加部分列(LIS: Longest Increasing Subsequence)

# O(n^2) DP解法
def lis_dp(arr):
    n = len(arr)
    dp = [1] * n
    for i in range(1, n):
        for j in range(i):
            if arr[j] < arr[i]:
                dp[i] = max(dp[i], dp[j] + 1)
    return max(dp)

# O(n log n) 二分探索解法
import bisect

def lis_binary_search(arr):
    tails = []  # tails[i] = 長さi+1のLISの末尾の最小値
    for num in arr:
        pos = bisect.bisect_left(tails, num)
        if pos == len(tails):
            tails.append(num)
        else:
            tails[pos] = num
    return len(tails)

print(lis_binary_search([10, 9, 2, 5, 3, 7, 101, 18]))  # 4: [2, 3, 7, 101]

6.9 コイン問題(Coin Change)

def coin_change(coins, amount):
    """最小コイン数 O(n * amount)"""
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    for coin in coins:
        for a in range(coin, amount + 1):
            dp[a] = min(dp[a], dp[a - coin] + 1)
    return dp[amount] if dp[amount] != float('inf') else -1

def coin_change_count(coins, amount):
    """組み合わせ数 O(n * amount)"""
    dp = [0] * (amount + 1)
    dp[0] = 1
    for coin in coins:
        for a in range(coin, amount + 1):
            dp[a] += dp[a - coin]
    return dp[amount]

print(coin_change([1, 5, 10, 25], 30))        # 2 (25+5)
print(coin_change_count([1, 5, 10, 25], 30))   # 組み合わせ数

6.10 DPの典型パターンまとめ

パターン漸化式の形
線形DPフィボナッチ、階段dp[i] = f(dp[i-1], dp[i-2])
区間DP行列連鎖乗算dp[i][j] = f(dp[i][k], dp[k][j])
ナップサック0/1ナップサックdp[i][w] = f(dp[i-1][w], dp[i-1][w-wi])
LCS/編集距離文字列比較dp[i][j] = f(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
木DP木の最大独立集合dp[v] = f(dp[children(v)])
ビットDP巡回セールスマンdp[S][v] (S: 訪問済み集合)
桁DPN以下の条件付き数え上げdp[i][tight][state]

7. 高度なアルゴリズム

7.1 貪欲法(Greedy Algorithm)

各ステップで局所的に最適な選択を行い、全体の最適解を求める手法。常に最適解が得られるわけではないが、特定の問題構造(貪欲選択性質とマトロイド構造)を持つ問題では最適解が保証される。

活動選択問題(Activity Selection)

def activity_selection(activities):
    """
    終了時間が早い順に貪欲に選択
    activities: [(start, end), ...]
    O(n log n)
    """
    activities.sort(key=lambda x: x[1])
    selected = [activities[0]]
    last_end = activities[0][1]
    for start, end in activities[1:]:
        if start >= last_end:
            selected.append((start, end))
            last_end = end
    return selected

acts = [(1, 4), (3, 5), (0, 6), (5, 7), (3, 9), (5, 9), (6, 10), (8, 11), (8, 12), (2, 14), (12, 16)]
print(activity_selection(acts))
# [(1, 4), (5, 7), (8, 11), (12, 16)]

ハフマン符号化(Huffman Coding)

import heapq

class HuffmanNode:
    def __init__(self, char, freq):
        self.char = char
        self.freq = freq
        self.left = None
        self.right = None

    def __lt__(self, other):
        return self.freq < other.freq

def huffman_coding(text):
    # 頻度カウント
    freq = {}
    for ch in text:
        freq[ch] = freq.get(ch, 0) + 1

    # 優先度付きキューの構築
    heap = [HuffmanNode(ch, f) for ch, f in freq.items()]
    heapq.heapify(heap)

    # ハフマン木の構築
    while len(heap) > 1:
        left = heapq.heappop(heap)
        right = heapq.heappop(heap)
        merged = HuffmanNode(None, left.freq + right.freq)
        merged.left = left
        merged.right = right
        heapq.heappush(heap, merged)

    # 符号の生成
    codes = {}
    def build_codes(node, code=""):
        if node is None:
            return
        if node.char is not None:
            codes[node.char] = code if code else "0"
            return
        build_codes(node.left, code + "0")
        build_codes(node.right, code + "1")

    build_codes(heap[0])
    return codes

codes = huffman_coding("abracadabra")
for ch, code in sorted(codes.items()):
    print(f"  '{ch}': {code}")

Java 貪欲法の例(フラクショナルナップサック)

import java.util.*;

public class GreedyKnapsack {
    public static double fractionalKnapsack(
            int[] weights, int[] values, int capacity) {
        int n = weights.length;
        Integer[] indices = new Integer[n];
        for (int i = 0; i < n; i++) indices[i] = i;

        // 単位重量あたりの価値でソート
        Arrays.sort(indices, (a, b) ->
            Double.compare(
                (double) values[b] / weights[b],
                (double) values[a] / weights[a]
            )
        );

        double totalValue = 0;
        int remaining = capacity;
        for (int i : indices) {
            if (remaining <= 0) break;
            int take = Math.min(weights[i], remaining);
            totalValue += (double) take / weights[i] * values[i];
            remaining -= take;
        }
        return totalValue;
    }
}

7.2 分割統治法(Divide and Conquer)

問題を小さな部分問題に分割し、それぞれを再帰的に解いて、結果を統合する手法。

最近点対問題(Closest Pair of Points)

import math

def closest_pair(points):
    """O(n log n)"""
    points.sort()  # x座標でソート

    def distance(p1, p2):
        return math.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)

    def closest_rec(pts):
        n = len(pts)
        if n <= 3:
            min_d = float('inf')
            for i in range(n):
                for j in range(i + 1, n):
                    min_d = min(min_d, distance(pts[i], pts[j]))
            return min_d

        mid = n // 2
        mid_x = pts[mid][0]
        d_left = closest_rec(pts[:mid])
        d_right = closest_rec(pts[mid:])
        d = min(d_left, d_right)

        # ストリップ内の点を確認
        strip = [p for p in pts if abs(p[0] - mid_x) < d]
        strip.sort(key=lambda p: p[1])
        for i in range(len(strip)):
            j = i + 1
            while j < len(strip) and strip[j][1] - strip[i][1] < d:
                d = min(d, distance(strip[i], strip[j]))
                j += 1
        return d

    return closest_rec(points)

べき乗の高速計算

def fast_power(base, exp, mod=None):
    """繰り返し二乗法: O(log n)"""
    result = 1
    base = base % mod if mod else base
    while exp > 0:
        if exp % 2 == 1:
            result = (result * base) % mod if mod else result * base
        exp //= 2
        base = (base * base) % mod if mod else base * base
    return result

# 行列のべき乗(フィボナッチ数列のO(log n)計算)
def matrix_mult(A, B, mod=None):
    n = len(A)
    C = [[0] * n for _ in range(n)]
    for i in range(n):
        for j in range(n):
            for k in range(n):
                C[i][j] += A[i][k] * B[k][j]
            if mod:
                C[i][j] %= mod
    return C

def matrix_power(M, p, mod=None):
    n = len(M)
    result = [[1 if i == j else 0 for j in range(n)] for i in range(n)]
    while p > 0:
        if p % 2 == 1:
            result = matrix_mult(result, M, mod)
        M = matrix_mult(M, M, mod)
        p //= 2
    return result

def fib_fast(n, mod=10**9+7):
    """O(log n) でフィボナッチ数を求める"""
    if n <= 1:
        return n
    M = [[1, 1], [1, 0]]
    result = matrix_power(M, n - 1, mod)
    return result[0][0]

7.3 バックトラッキング(Backtracking)

解の候補を段階的に構築し、条件を満たさない場合は直前の選択を取り消して(バックトラック)別の選択を試みる手法。

N-Queens問題

def solve_n_queens(n):
    """N-Queens問題のすべての解を求める"""
    solutions = []

    def backtrack(row, cols, diag1, diag2, board):
        if row == n:
            solutions.append([''.join(r) for r in board])
            return
        for col in range(n):
            if col in cols or (row - col) in diag1 or (row + col) in diag2:
                continue
            board[row][col] = 'Q'
            cols.add(col)
            diag1.add(row - col)
            diag2.add(row + col)
            backtrack(row + 1, cols, diag1, diag2, board)
            board[row][col] = '.'
            cols.remove(col)
            diag1.remove(row - col)
            diag2.remove(row + col)

    board = [['.' for _ in range(n)] for _ in range(n)]
    backtrack(0, set(), set(), set(), board)
    return solutions

for sol in solve_n_queens(4):
    for row in sol:
        print(row)
    print()

数独ソルバー

def solve_sudoku(board):
    """バックトラッキングによる数独ソルバー"""
    def is_valid(board, row, col, num):
        for i in range(9):
            if board[row][i] == num:
                return False
            if board[i][col] == num:
                return False
        box_r, box_c = 3 * (row // 3), 3 * (col // 3)
        for i in range(box_r, box_r + 3):
            for j in range(box_c, box_c + 3):
                if board[i][j] == num:
                    return False
        return True

    def solve(board):
        for i in range(9):
            for j in range(9):
                if board[i][j] == 0:
                    for num in range(1, 10):
                        if is_valid(board, i, j, num):
                            board[i][j] = num
                            if solve(board):
                                return True
                            board[i][j] = 0
                    return False
        return True

    solve(board)
    return board
public class NQueens {
    private List<List<String>> solutions = new ArrayList<>();

    public List<List<String>> solveNQueens(int n) {
        char[][] board = new char[n][n];
        for (char[] row : board) Arrays.fill(row, '.');
        backtrack(0, n, board, new boolean[n],
                  new boolean[2 * n], new boolean[2 * n]);
        return solutions;
    }

    private void backtrack(int row, int n, char[][] board,
            boolean[] cols, boolean[] diag1, boolean[] diag2) {
        if (row == n) {
            List<String> sol = new ArrayList<>();
            for (char[] r : board) sol.add(new String(r));
            solutions.add(sol);
            return;
        }
        for (int col = 0; col < n; col++) {
            if (cols[col] || diag1[row - col + n] || diag2[row + col])
                continue;
            board[row][col] = 'Q';
            cols[col] = diag1[row - col + n] = diag2[row + col] = true;
            backtrack(row + 1, n, board, cols, diag1, diag2);
            board[row][col] = '.';
            cols[col] = diag1[row - col + n] = diag2[row + col] = false;
        }
    }
}

7.4 Union-Find(素集合データ構造)

要素のグループ化(Union)と同じグループかの判定(Find)を効率的に行うデータ構造。

class UnionFind:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n
        self.size = [1] * n
        self.count = n  # 連結成分の数

    def find(self, x):
        """経路圧縮付きFind — 償却O(α(n))"""
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        """ランクによる統合 — 償却O(α(n))"""
        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
        self.size[px] += self.size[py]
        if self.rank[px] == self.rank[py]:
            self.rank[px] += 1
        self.count -= 1
        return True

    def connected(self, x, y):
        return self.find(x) == self.find(y)

    def get_size(self, x):
        return self.size[self.find(x)]

# 使用例: 島の数
def num_islands(grid):
    if not grid:
        return 0
    rows, cols = len(grid), len(grid[0])
    uf = UnionFind(rows * cols)
    water = 0
    for i in range(rows):
        for j in range(cols):
            if grid[i][j] == '0':
                water += 1
                continue
            for di, dj in [(0, 1), (1, 0)]:
                ni, nj = i + di, j + dj
                if 0 <= ni < rows and 0 <= nj < cols and grid[ni][nj] == '1':
                    uf.union(i * cols + j, ni * cols + nj)
    return uf.count - water
class UnionFind {
    private parent: number[];
    private rank: number[];
    count: number;

    constructor(n: number) {
        this.parent = Array.from({ length: n }, (_, i) => i);
        this.rank = new Array(n).fill(0);
        this.count = n;
    }

    find(x: number): number {
        if (this.parent[x] !== x) {
            this.parent[x] = this.find(this.parent[x]);
        }
        return this.parent[x];
    }

    union(x: number, y: number): boolean {
        let px = this.find(x), py = this.find(y);
        if (px === py) return false;
        if (this.rank[px] < this.rank[py]) [px, py] = [py, px];
        this.parent[py] = px;
        if (this.rank[px] === this.rank[py]) this.rank[px]++;
        this.count--;
        return true;
    }

    connected(x: number, y: number): boolean {
        return this.find(x) === this.find(y);
    }
}

7.5 トポロジカルソート(Topological Sort)

有向非巡回グラフ(DAG)の頂点を、すべての辺が左から右を向くように順序付ける。

from collections import deque

def topological_sort_kahn(graph, V):
    """カーンのアルゴリズム(BFSベース)O(V + E)"""
    in_degree = [0] * V
    for u in range(V):
        for v, _ in graph.neighbors(u):
            in_degree[v] += 1

    queue = deque([v for v in range(V) if in_degree[v] == 0])
    result = []

    while queue:
        u = queue.popleft()
        result.append(u)
        for v, _ in graph.neighbors(u):
            in_degree[v] -= 1
            if in_degree[v] == 0:
                queue.append(v)

    if len(result) != V:
        raise ValueError("グラフに閉路が存在します")
    return result

def topological_sort_dfs(graph, V):
    """DFSベースのトポロジカルソート O(V + E)"""
    visited = [False] * V
    stack = []

    def dfs(v):
        visited[v] = True
        for u, _ in graph.neighbors(v):
            if not visited[u]:
                dfs(u)
        stack.append(v)

    for v in range(V):
        if not visited[v]:
            dfs(v)
    return stack[::-1]
public class TopologicalSort {
    public static List<Integer> kahnSort(List<List<int[]>> adj, int V) {
        int[] inDegree = new int[V];
        for (int u = 0; u < V; u++)
            for (int[] e : adj.get(u)) inDegree[e[0]]++;

        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < V; i++)
            if (inDegree[i] == 0) queue.offer(i);

        List<Integer> result = new ArrayList<>();
        while (!queue.isEmpty()) {
            int u = queue.poll();
            result.add(u);
            for (int[] e : adj.get(u)) {
                if (--inDegree[e[0]] == 0) queue.offer(e[0]);
            }
        }
        if (result.size() != V) throw new RuntimeException("Cycle detected");
        return result;
    }
}

7.6 最大フロー(Maximum Flow)

ソースからシンクへの最大流量を求める問題。Ford-Fulkerson法とEdmonds-Karp法を示す。

from collections import deque

def max_flow_edmonds_karp(capacity, source, sink):
    """
    Edmonds-Karp法(BFSベースのFord-Fulkerson法)
    O(V * E^2)
    """
    V = len(capacity)
    flow = [[0] * V for _ in range(V)]

    def bfs(source, sink, parent):
        visited = {source}
        queue = deque([source])
        while queue:
            u = queue.popleft()
            for v in range(V):
                if v not in visited and capacity[u][v] - flow[u][v] > 0:
                    visited.add(v)
                    parent[v] = u
                    if v == sink:
                        return True
                    queue.append(v)
        return False

    total_flow = 0
    parent = [-1] * V

    while bfs(source, sink, parent):
        # 増加パスのボトルネックを見つける
        path_flow = float('inf')
        v = sink
        while v != source:
            u = parent[v]
            path_flow = min(path_flow, capacity[u][v] - flow[u][v])
            v = u

        # フローを更新
        v = sink
        while v != source:
            u = parent[v]
            flow[u][v] += path_flow
            flow[v][u] -= path_flow
            v = u

        total_flow += path_flow
        parent = [-1] * V

    return total_flow

# 使用例
capacity = [
    [0, 16, 13, 0,  0,  0],
    [0, 0,  10, 12, 0,  0],
    [0, 4,  0,  0,  14, 0],
    [0, 0,  9,  0,  0,  20],
    [0, 0,  0,  7,  0,  4],
    [0, 0,  0,  0,  0,  0]
]
print(f"最大フロー: {max_flow_edmonds_karp(capacity, 0, 5)}")  # 23

7.7 文字列アルゴリズム

KMP法(Knuth-Morris-Pratt)

def kmp_search(text, pattern):
    """
    KMP法: O(n + m)
    """
    def build_failure(pattern):
        m = len(pattern)
        failure = [0] * m
        j = 0
        for i in range(1, m):
            while j > 0 and pattern[i] != pattern[j]:
                j = failure[j - 1]
            if pattern[i] == pattern[j]:
                j += 1
            failure[i] = j
        return failure

    failure = build_failure(pattern)
    n, m = len(text), len(pattern)
    j = 0
    matches = []
    for i in range(n):
        while j > 0 and text[i] != pattern[j]:
            j = failure[j - 1]
        if text[i] == pattern[j]:
            j += 1
        if j == m:
            matches.append(i - m + 1)
            j = failure[j - 1]
    return matches

print(kmp_search("ababcababcabc", "ababc"))  # [0, 5]

Rabin-Karp法

def rabin_karp(text, pattern, base=256, mod=10**9+7):
    """
    Rabin-Karp法: O(n + m) 平均, O(nm) 最悪
    """
    n, m = len(text), len(pattern)
    if m > n:
        return []

    # パターンのハッシュ
    p_hash = 0
    t_hash = 0
    h = pow(base, m - 1, mod)

    for i in range(m):
        p_hash = (p_hash * base + ord(pattern[i])) % mod
        t_hash = (t_hash * base + ord(text[i])) % mod

    matches = []
    for i in range(n - m + 1):
        if p_hash == t_hash:
            if text[i:i+m] == pattern:  # 本当に一致するか確認
                matches.append(i)
        if i < n - m:
            t_hash = ((t_hash - ord(text[i]) * h) * base + ord(text[i + m])) % mod
            t_hash = (t_hash + mod) % mod

    return matches

print(rabin_karp("ababcababcabc", "ababc"))  # [0, 5]
public class KMP {
    public static List<Integer> search(String text, String pattern) {
        int[] failure = buildFailure(pattern);
        List<Integer> matches = new ArrayList<>();
        int j = 0;
        for (int i = 0; i < text.length(); i++) {
            while (j > 0 && text.charAt(i) != pattern.charAt(j))
                j = failure[j - 1];
            if (text.charAt(i) == pattern.charAt(j)) j++;
            if (j == pattern.length()) {
                matches.add(i - pattern.length() + 1);
                j = failure[j - 1];
            }
        }
        return matches;
    }

    private static int[] buildFailure(String pattern) {
        int[] f = new int[pattern.length()];
        int j = 0;
        for (int i = 1; i < pattern.length(); i++) {
            while (j > 0 && pattern.charAt(i) != pattern.charAt(j))
                j = f[j - 1];
            if (pattern.charAt(i) == pattern.charAt(j)) j++;
            f[i] = j;
        }
        return f;
    }
}

7.8 幾何アルゴリズム基礎

凸包(Convex Hull)— Graham Scan法

def convex_hull(points):
    """Graham Scan法: O(n log n)"""
    def cross(o, a, b):
        return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])

    points = sorted(set(points))
    if len(points) <= 1:
        return points

    # 下側凸包
    lower = []
    for p in points:
        while len(lower) >= 2 and cross(lower[-2], lower[-1], p) <= 0:
            lower.pop()
        lower.append(p)

    # 上側凸包
    upper = []
    for p in reversed(points):
        while len(upper) >= 2 and cross(upper[-2], upper[-1], p) <= 0:
            upper.pop()
        upper.append(p)

    return lower[:-1] + upper[:-1]

points = [(0, 0), (1, 1), (2, 2), (4, 4), (0, 3), (1, 2), (3, 1), (3, 3)]
hull = convex_hull(points)
print(f"凸包の頂点: {hull}")

線分の交差判定

def on_segment(p, q, r):
    return (min(p[0], r[0]) <= q[0] <= max(p[0], r[0]) and
            min(p[1], r[1]) <= q[1] <= max(p[1], r[1]))

def orientation(p, q, r):
    val = (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1])
    if val == 0:
        return 0  # 同一直線上
    return 1 if val > 0 else 2  # 時計回り / 反時計回り

def segments_intersect(p1, q1, p2, q2):
    """2つの線分が交差するかを判定 O(1)"""
    o1 = orientation(p1, q1, p2)
    o2 = orientation(p1, q1, q2)
    o3 = orientation(p2, q2, p1)
    o4 = orientation(p2, q2, q1)

    if o1 != o2 and o3 != o4:
        return True
    if o1 == 0 and on_segment(p1, p2, q1): return True
    if o2 == 0 and on_segment(p1, q2, q1): return True
    if o3 == 0 and on_segment(p2, p1, q2): return True
    if o4 == 0 and on_segment(p2, q1, q2): return True
    return False

8. 実践ガイド

8.1 競技プログラミングでの活用

よく出るアルゴリズムと出題頻度

アルゴリズム出題頻度典型的な問題
全探索・BFS/DFS非常に高迷路探索、連結成分
二分探索非常に高答えに対する二分探索
動的計画法非常に高ナップサック、LCS
貪欲法区間スケジューリング
Union-Find連結判定、クラスカル
セグメント木/BIT区間クエリ
最短経路ダイクストラ、ワーシャルフロイド
文字列アルゴリズムパターンマッチング
最大フロー二部マッチング

競技プログラミング用テンプレート(Python)

import sys
from collections import deque, defaultdict, Counter
from heapq import heappush, heappop
from bisect import bisect_left, bisect_right
from functools import lru_cache
from itertools import permutations, combinations, accumulate

input = sys.stdin.readline
INF = float('inf')
MOD = 10**9 + 7

def solve():
    n = int(input())
    arr = list(map(int, input().split()))
    # ここに解法を書く

solve()

Binary Indexed Tree(BIT / Fenwick Tree)

競技プログラミングで頻出のデータ構造。

class BIT:
    """Binary Indexed Tree (Fenwick Tree)
    点更新・区間和クエリ O(log n)
    """
    def __init__(self, n):
        self.n = n
        self.tree = [0] * (n + 1)

    def update(self, i, delta):
        """i番目の要素にdeltaを加算 (1-indexed)"""
        while i <= self.n:
            self.tree[i] += delta
            i += i & (-i)

    def query(self, i):
        """[1, i] の区間和を求める"""
        s = 0
        while i > 0:
            s += self.tree[i]
            i -= i & (-i)
        return s

    def range_query(self, l, r):
        """[l, r] の区間和を求める"""
        return self.query(r) - self.query(l - 1)

# 使用例: 転倒数(inversion count)
def count_inversions(arr):
    n = len(arr)
    # 座標圧縮
    sorted_unique = sorted(set(arr))
    rank = {v: i + 1 for i, v in enumerate(sorted_unique)}

    bit = BIT(len(sorted_unique))
    inversions = 0
    for i in range(n - 1, -1, -1):
        inversions += bit.query(rank[arr[i]] - 1)
        bit.update(rank[arr[i]], 1)
    return inversions

print(count_inversions([3, 1, 2, 5, 4]))  # 3
public class BIT {
    private int[] tree;
    private int n;

    public BIT(int n) {
        this.n = n;
        this.tree = new int[n + 1];
    }

    public void update(int i, int delta) {
        for (; i <= n; i += i & (-i))
            tree[i] += delta;
    }

    public int query(int i) {
        int sum = 0;
        for (; i > 0; i -= i & (-i))
            sum += tree[i];
        return sum;
    }

    public int rangeQuery(int l, int r) {
        return query(r) - query(l - 1);
    }
}

8.2 実務でのデータ構造選択指針

要件に基づくデータ構造の選択

要件推奨データ構造理由
キーバリューの高速検索ハッシュマップO(1) 平均
ソート済みデータの管理平衡BST / TreeMapO(log n)
頻繁な先頭/末尾の操作デック / 双方向連結リストO(1)
優先度ベースの処理ヒープ / 優先度付きキューO(log n)
集合の統合・判定Union-Find償却O(α(n))
前方一致検索トライ木O(m)
区間の集約クエリセグメント木 / BITO(log n)
グラフの表現隣接リスト疎グラフに効率的
高速な挿入/削除連結リストO(1)

言語標準ライブラリの対応表

データ構造PythonJavaTypeScript/JS
動的配列listArrayListArray
連結リストcollections.dequeLinkedList― (自作)
スタックlist (append/pop)Deque (ArrayDeque)Array (push/pop)
キューcollections.dequeQueue (LinkedList)― (自作)
ハッシュマップdictHashMapMap
ハッシュセットsetHashSetSet
ソート済みマップ― (sortedcontainers)TreeMap― (自作)
ソート済みセット― (sortedcontainers)TreeSet― (自作)
優先度付きキューheapqPriorityQueue― (自作)
デックcollections.dequeArrayDeque― (自作)

Pythonでの効率的なデータ構造の使い方

# dict(ハッシュマップ)の活用
from collections import defaultdict, Counter, OrderedDict

# defaultdict: 存在しないキーにデフォルト値
graph = defaultdict(list)
graph[0].append(1)
graph[0].append(2)

# Counter: 要素の出現回数
counter = Counter("abracadabra")
print(counter.most_common(3))  # [('a', 5), ('b', 2), ('r', 2)]

# heapq: 優先度付きキュー(最小ヒープ)
import heapq
heap = []
heapq.heappush(heap, (3, 'low'))
heapq.heappush(heap, (1, 'high'))
heapq.heappush(heap, (2, 'medium'))
print(heapq.heappop(heap))  # (1, 'high')

# bisect: ソート済みリストへの二分探索挿入
import bisect
sorted_list = [1, 3, 5, 7, 9]
bisect.insort(sorted_list, 4)
print(sorted_list)  # [1, 3, 4, 5, 7, 9]

# deque: 両端キュー(BFS等に最適)
from collections import deque
dq = deque([1, 2, 3])
dq.appendleft(0)  # O(1)
dq.append(4)      # O(1)
dq.popleft()       # O(1)

# itertools: 組み合わせ列挙
from itertools import combinations, permutations, product
list(combinations([1, 2, 3], 2))  # [(1,2), (1,3), (2,3)]
list(permutations([1, 2, 3], 2))  # [(1,2), (1,3), (2,1), ...]

8.3 計算量の見積もり方

制約からアルゴリズムを逆算する

一般的なオンラインジャッジの制限時間(1〜2秒)に対し、Pythonでは約 10^7 程度の操作が目安である(C++/Javaは 10^8〜10^9)。

入力サイズ N許容される計算量適用可能なアルゴリズム例
N ≦ 10O(N!)全順列探索
N ≦ 20O(2^N)ビット全探索
N ≦ 500O(N^3)ワーシャルフロイド
N ≦ 5,000O(N^2)二重ループDP
N ≦ 500,000O(N log N)ソート、セグメント木
N ≦ 10,000,000O(N)線形走査、カウンティング
N ≦ 10^18O(log N)二分探索、行列累乗

計算量を改善する一般的なテクニック

# テクニック1: 累積和で区間合計を O(1) に
def prefix_sum_example(arr, queries):
    # 前処理 O(n)
    prefix = [0] * (len(arr) + 1)
    for i in range(len(arr)):
        prefix[i + 1] = prefix[i] + arr[i]
    # 各クエリ O(1)
    results = []
    for l, r in queries:
        results.append(prefix[r + 1] - prefix[l])
    return results

# テクニック2: 座標圧縮
def coordinate_compression(arr):
    sorted_unique = sorted(set(arr))
    mapping = {v: i for i, v in enumerate(sorted_unique)}
    return [mapping[x] for x in arr]

# テクニック3: 二つのポインタ(Two Pointers)
def two_sum_sorted(arr, target):
    """ソート済み配列での2数の和 O(n)"""
    left, right = 0, len(arr) - 1
    while left < right:
        s = arr[left] + arr[right]
        if s == target:
            return (left, right)
        elif s < target:
            left += 1
        else:
            right -= 1
    return None

# テクニック4: しゃくとり法(Sliding Window)
def max_sum_subarray(arr, k):
    """サイズkの部分配列の最大合計 O(n)"""
    window_sum = sum(arr[:k])
    max_sum = window_sum
    for i in range(k, len(arr)):
        window_sum += arr[i] - arr[i - k]
        max_sum = max(max_sum, window_sum)
    return max_sum

def longest_substring_no_repeat(s):
    """重複文字なしの最長部分文字列 O(n)"""
    char_set = set()
    left = 0
    max_len = 0
    for right in range(len(s)):
        while s[right] in char_set:
            char_set.remove(s[left])
            left += 1
        char_set.add(s[right])
        max_len = max(max_len, right - left + 1)
    return max_len

8.4 面接対策としてのアルゴリズム

頻出トピックと代表問題

トピック代表的な問題難易度
配列/文字列Two Sum, 最大部分配列和Easy〜Medium
連結リスト逆転、環検出、マージEasy〜Medium
スタック/キュー有効な括弧、最小スタックEasy
二分探索回転配列の探索Medium
ツリーLCA、直径、シリアライズMedium〜Hard
グラフ島の数、コースの前提条件Medium
DPコイン問題、ロングパスMedium〜Hard
バックトラッキングN-Queens、組み合わせの和Medium

面接での問題解法ステップ

  1. 問題の理解: 例を使って条件を確認
  2. ブルートフォース: まず愚直な解法を考える
  3. 最適化: より効率的なアルゴリズムへ改善
  4. 実装: 明確で読みやすいコードを書く
  5. テスト: エッジケースを含むテストケースで検証
# 面接頻出: 最大部分配列和(Kadane's Algorithm)
def max_subarray_sum(nums):
    """O(n) 時間, O(1) 空間"""
    max_sum = current_sum = nums[0]
    for num in nums[1:]:
        current_sum = max(num, current_sum + num)
        max_sum = max(max_sum, current_sum)
    return max_sum

# 面接頻出: 連結リストの環検出(Floyd's Cycle Detection)
def has_cycle(head):
    """O(n) 時間, O(1) 空間"""
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            return True
    return False

# 面接頻出: LRU Cache
from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity: int):
        self.cache = OrderedDict()
        self.capacity = capacity

    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1
        self.cache.move_to_end(key)
        return self.cache[key]

    def put(self, key: int, value: int) -> None:
        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)
// LRU Cache in Java
import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache extends LinkedHashMap<Integer, Integer> {
    private final int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    public int get(int key) {
        return super.getOrDefault(key, -1);
    }

    public void put(int key, int value) {
        super.put(key, value);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity;
    }
}
// LRU Cache in TypeScript
class LRUCache {
    private capacity: number;
    private cache: Map<number, number>;

    constructor(capacity: number) {
        this.capacity = capacity;
        this.cache = new Map();
    }

    get(key: number): number {
        if (!this.cache.has(key)) return -1;
        const val = this.cache.get(key)!;
        this.cache.delete(key);
        this.cache.set(key, val);
        return val;
    }

    put(key: number, value: number): void {
        if (this.cache.has(key)) this.cache.delete(key);
        this.cache.set(key, value);
        if (this.cache.size > this.capacity) {
            const firstKey = this.cache.keys().next().value;
            this.cache.delete(firstKey);
        }
    }
}

9. まとめと参考文献

9.1 全体の振り返り

本記事では、アルゴリズムとデータ構造の基礎から応用までを体系的に解説した。

学習ロードマップ

レベル1(初級)
  ├── 配列、連結リスト、スタック、キュー
  ├── 線形探索、二分探索
  ├── バブルソート、挿入ソート
  └── Big-O記法の理解

レベル2(中級)
  ├── ハッシュテーブル、ツリー(BST、AVL)
  ├── マージソート、クイックソート、ヒープソート
  ├── BFS、DFS
  ├── 動的計画法の基本
  └── 二分探索の応用

レベル3(上級)
  ├── セグメント木、BIT、トライ木
  ├── ダイクストラ、ベルマンフォード
  ├── Union-Find、トポロジカルソート
  ├── 高度なDP(ビットDP、桁DP)
  └── 文字列アルゴリズム(KMP、Rabin-Karp)

レベル4(発展)
  ├── 赤黒木、B木
  ├── 最大フロー、最小カット
  ├── 幾何アルゴリズム
  ├── 高速フーリエ変換(FFT)
  └── 永続データ構造

データ構造の計算量一覧

データ構造アクセス探索挿入削除備考
配列O(1)O(n)O(n)O(n)ランダムアクセス
連結リストO(n)O(n)O(1)O(1)参照がある場合
スタックO(n)O(n)O(1)O(1)LIFO
キューO(n)O(n)O(1)O(1)FIFO
ハッシュテーブルN/AO(1)*O(1)*O(1)**平均
二分探索木O(log n)*O(log n)*O(log n)*O(log n)**平衡時
AVL木O(log n)O(log n)O(log n)O(log n)常にバランス
赤黒木O(log n)O(log n)O(log n)O(log n)回転が少ない
B木O(log n)O(log n)O(log n)O(log n)ディスク最適
ヒープO(1)min/maxO(n)O(log n)O(log n)最小/最大値
トライ木-O(m)O(m)O(m)文字列長m
セグメント木-O(log n)O(log n)-区間クエリ

ソートアルゴリズムの計算量一覧

アルゴリズム最良平均最悪空間安定
バブルソートO(n)O(n^2)O(n^2)O(1)Yes
選択ソートO(n^2)O(n^2)O(n^2)O(1)No
挿入ソートO(n)O(n^2)O(n^2)O(1)Yes
マージソートO(n log n)O(n log n)O(n log n)O(n)Yes
クイックソートO(n log n)O(n log n)O(n^2)O(log n)No
ヒープソートO(n log n)O(n log n)O(n log n)O(1)No
計数ソートO(n+k)O(n+k)O(n+k)O(k)Yes
基数ソートO(nk)O(nk)O(nk)O(n+k)Yes

9.2 参考文献・学習リソース

書籍

  1. Introduction to Algorithms (CLRS) — Cormen, Leiserson, Rivest, Stein
    • アルゴリズムの教科書として世界的に最も有名な一冊
  2. アルゴリズムイントロダクション — 上記の日本語訳
  3. プログラミングコンテストチャレンジブック(蟻本) — 秋葉拓哉、岩田陽一、北川宜稔
    • 日本の競技プログラマーのバイブル
  4. Algorithm Design — Kleinberg, Tardos
    • アルゴリズム設計のテクニックに焦点
  5. Competitive Programming 3 — Steven Halim, Felix Halim
    • 競技プログラミング向けの実践的な一冊

オンライン学習リソース

リソースURL特徴
LeetCodeleetcode.com面接対策問題集
AtCoderatcoder.jp日本の競技プログラミング
Codeforcescodeforces.com国際的な競技プログラミング
Visualgovisualgo.netアルゴリズムの可視化
GeeksforGeeksgeeksforgeeks.orgアルゴリズム解説

学習のコツ

  1. 理解してから書く: アルゴリズムの動作を紙上でトレースしてから実装する
  2. 計算量を常に意識: すべてのアルゴリズムについて時間・空間計算量を把握する
  3. 手を動かす: 理論だけでなく、実際にコードを書いて動作を確認する
  4. 問題を解く: LeetCodeやAtCoderで多くの問題を解き、パターンを身につける
  5. 復習する: 一度解いた問題を時間を空けて再度解く

本記事は、アルゴリズムとデータ構造の主要なトピックを網羅的に解説した。各概念について理論的な説明と実践的なコード例を併せて提示したため、初学者から中上級者まで幅広い読者の学習に役立てていただければ幸いである。