第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(?)
コード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)だけど大丈夫?」と判断できる力が重要である。