コンテンツにスキップ

第2回:計算量解析とビッグオー記法

「速い」とは

アルゴリズムの「速さ」を考えるとき、2つの基準がある。

  • 実行時間が短い?
  • 使うメモリが少ない?

両方とも大事だが、今回は時間に注目する。

実行時間はPCの性能に依存する。同じコードでも、速いPCと遅いPCで結果が違ってしまう。そこで、環境に依存しない「ものさし」が必要になる。

入力サイズ n に対して、処理回数がどう増えるかを見る

デモ1:1からnまでの合計

1 + 2 + 3 + ... + n を求める方法を2つ考える。

# 方法A:ループで足す
def sum_loop(n):
    total = 0
    for i in range(1, n + 1):
        total += i
    return total

# 方法B:公式で一発
def sum_formula(n):
    return n * (n + 1) // 2
  • ループ(方法A): nに比例して処理回数が増える → O(n)
  • 公式(方法B): nが増えてもほぼ変わらない → O(1)

デモ2:n人の握手ペア数

n人で全員が他の全員と1回ずつ握手するとき、ペア数を求める。

# 方法A:全ペアを実際に数える(力技)
def count_pairs_brute(n):
    count = 0
    for i in range(n):
        for j in range(i + 1, n):
            count += 1
    return count

# 方法B:公式(nC2)
def count_pairs_formula(n):
    return n * (n - 1) // 2
  • 力技(方法A): n^2に比例して処理回数が増える → O(n^2)
  • 公式(方法B): nが増えても変わらない → O(1)

二重ループは本当に遅い。

2つのデモから見えたパターン

  • デモ1: ループ1つ → nに比例して遅くなる
  • デモ2: ループ2つ(二重) → n^2に比例して遅くなる
  • 公式: nが増えても変わらない

この「増え方のパターン」に名前をつけたい。

ビッグオー記法(O記法)

ビッグオー記法(Big-O notation)は、アルゴリズムの「増え方のパターン」を表す記法である。

  • デモ1のループ: 処理回数はn回 → O(n)
  • デモ2の力技: 処理回数はn(n-1)/2回 → O(n^2)
  • 公式: 処理回数は常に数回 → O(1)

O記法のルール

  • 定数は無視する: 2n → O(n)、100n → O(n)
  • 低い次数は無視する: n^2 + 5n → O(n^2)
  • 最も成長が速い項だけ残す

例:

  • 3n + 50 → O(n)
  • 2n^2 + 100n + 7 → O(n^2)
  • 42 → O(1)

なぜ定数を無視するのか

  • 2nと5nは定数倍の違い → PCが速くなれば解消する
  • nとn^2は質的な違い → PCを速くしても追いつかない

O記法は「本質的な成長率の違い」だけを捉える。

代表的な計算量

計算量 名前
O(1) 定数時間 公式、配列の先頭を取り出す
O(log n) 対数時間 二分探索
O(n) 線形時間 ループ1つ(デモ1)
O(n log n) --- 効率的なソート(第7回)
O(n^2) 二乗時間 二重ループ(デモ2)

nの大きさによる影響

n=100くらいまでは大差ない。しかしn=10,000を超えるとO(n^2)は使い物にならなくなる。

演習1:計算量を自分で判断しよう

以下の3つのコードについて、それぞれの計算量をO記法で答えよ。

# コードA
def find_max(lst):
    max_val = lst[0]
    for x in lst:
        if x > max_val:
            max_val = x
    return max_val
# → O(?)
# コードB
def has_duplicate(lst):
    for i in range(len(lst)):
        for j in range(i + 1, len(lst)):
            if lst[i] == lst[j]:
                return True
    return False
# → O(?)
# コードC
def get_first(lst):
    return lst[0]
# → O(?)

コードBを計測して確認しよう

import time

def has_duplicate(lst):
    for i in range(len(lst)):
        for j in range(i + 1, len(lst)):
            if lst[i] == lst[j]:
                return True
    return False

for n in [1000, 2000, 4000, 8000]:
    lst = list(range(n))  # 重複なし(最悪ケース)
    start = time.time()
    has_duplicate(lst)
    t = time.time() - start
    print(f"n={n:>6} 時間: {t:.4f}秒")
# nが2倍 → 時間は約4倍になるはず(n^2だから)

演習2:生成AIのコードを評価しよう

「リストの中に重複する要素があるかを判定する」目的で、生成AIが2つのコードを提案した。

# 提案A
def check_a(lst):
    for i in range(len(lst)):
        for j in range(i + 1, len(lst)):
            if lst[i] == lst[j]:
                return True
    return False

# 提案B
def check_b(lst):
    return len(lst) != len(set(lst))
  • Q1: それぞれの計算量はO(何)か?
  • Q2: n=100万件のデータに使うなら、どちらを採用すべきか?
  • Q3: 実際に計測して確認しよう

以下のコードで計測できる。

import time

def check_a(lst):
    for i in range(len(lst)):
        for j in range(i + 1, len(lst)):
            if lst[i] == lst[j]:
                return True
    return False

def check_b(lst):
    return len(lst) != len(set(lst))

for n in [1000, 5000, 10000, 50000]:
    lst = list(range(n))

    start = time.time()
    check_a(lst)
    ta = time.time() - start

    start = time.time()
    check_b(lst)
    tb = time.time() - start

    print(f"n={n:>6}  A: {ta:.4f}秒  B: {tb:.6f}秒")

LLM時代にO記法が必要な理由

「動くコード」は生成AIでも書ける。しかし、そのコードが効率的かどうかを判断するのは人間である。n=100なら何でも一瞬だが、n=100万なら? O(n)は約0.1秒、O(n^2)は数時間かかることもある。「AIが出したコード、これO(n^2)だけど大丈夫?」と判断できる力が重要である。