「同じ“探す”でも、やり方次第でスピードが何倍も変わるって本当?」——そんな疑問を持ったことはありませんか。Pythonでデータを探すとき、どのアルゴリズムを選ぶかで処理時間は大きく変わります。
今回は、IT初心者にもわかりやすい形で時間計算量(ビッグオー記法)の直感をつかみつつ、線形探索と二分探索の違いを丁寧に解説します。コードの動きも、読み物のように追えるよう説明していきます。
まずはウォームアップです。時間計算量とは、「入力の大きさが大きくなるとき、処理時間がおおまかにどう増えるか」を表す指標です。詳しい数式を知らなくても、増え方の傾向だけ押さえれば十分に使えます。「ビッグオー記法」と呼ばれ、O(1)、O(n)、O(log n) のように書きます。ここで n は「データ件数(要素数)」のことです。
いくつか代表的な増え方を目でイメージしておきましょう。以下はよく登場する順番の参考です(左が速い傾向)。
厳密な秒数ではなく「増え方の違い」をつかむのがポイントです。では実際の探索アルゴリズムを見ていきましょう。
まずは一番シンプルな「片っ端から見る」方法です。リストの先頭から順に、探したい値と一致するかを1つずつ確かめていきます。見つかったらそこで終了、見つからなければ最後まで調べます。日常で言えば、引き出しの中身を上から順に確認していくイメージですね。「少ないデータならこれで十分」という場面も多いです。
# 配列内でターゲット値を探す
arr = [1, 3, 5, 7, 9]
target = 9
for i in range(len(arr)):
if arr[i] == target:
print(f"Found at index {i}")
break
このコードはとても素直です。先頭から i を増やしながら、arr[i] が target と一致した瞬間に見つかった位置を表示してループを抜けます。ターゲットが末尾にある場合や存在しない場合は、最後まで順に確かめることになるため、必要な比較回数は要素数 n にほぼ比例します。これが時間計算量 O(n) という意味です。運が良ければ最初の1回で見つかることもあるので、最良ケースは O(1)、平均や最悪では O(n) という見方をします。
なお、Python のリストに対する in 演算子(target in arr)や list.index は、基本的にこの線形探索と同じ増え方(O(n))になります。「とりあえず動かす」「データが小さい」ときは気軽に使えますが、データが増えると遅くなりやすい点は覚えておきましょう。
次に、「範囲を半分に狭めながら探す」方法です。条件はひとつ。データがあらかじめ昇順(または降順)にソートされていること。ソート済みなら、真ん中を見るだけで「左側にあるか」「右側にあるか」を判断でき、無駄な確認を大幅に減らせます。まるで本の索引で、だいたいのページを一気に絞り込むような感覚です。
二分探索の例(Python)
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = left + (right - left) // 2 # 中央のインデックス
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1 # 右半分に絞る
else:
right = mid - 1 # 左半分に絞る
return -1
arr = [1, 3, 5, 7, 9]
target = 9
print(binary_search(arr, target)) # 出力は 4(インデックス)
この関数は、left と right で探索範囲を表し、中央 mid の要素を調べて「左に寄せるか」「右に寄せるか」を決めています。1回の比較で探索範囲がほぼ半分になるため、必要な比較回数はデータ数 n に対して対数的に増えます。これが O(log n) です。つまり、n が何倍になっても、比較回数は少しずつしか増えません。最良ケースは1回で見つかるので O(1)、平均や最悪でも O(log n) という捉え方をします。
注意点として、二分探索は「ソートされていること」が前提です。毎回ソートから始めると O(n log n) のコストが別途かかるため、単発で1回だけ探すなら線形探索のほうが速いこともあります。逆に「同じ配列に対して何度も検索する」なら、最初に一度ソートしておき、二分探索(または bisect モジュール)で繰り返し高速に探すのが定石です。
また、Python には bisect モジュールがあり、二分探索での挿入位置や検索に便利です。実装バグ(オフバイワンなど)の心配を減らせるので、初心者にもおすすめです。
ここでは、増え方の違いを体感しやすいように、規模の違うデータを想像してみましょう。数字はあくまで比較の目安です。
「え、本当にそんなに違うの?」と思った方は、次の簡単な計測コードで実感してみましょう。
このサンプルは、リストの作成と探索の時間をざっくり比べるためのものです。まずは小さめの n から試し、あとで n を大きくして違いを観察すると良いでしょう。
import time
import bisect
import random
# データ準備
n = 1_000_00 # 10万件(値は調整してOK)
arr = list(range(n)) # ソート済み
targets = [random.randint(0, n - 1) for _ in range(1_000)]
# 線形探索の計測
start = time.time()
hits = 0
for t in targets:
# in はリストでは O(n) 相当
if t in arr:
hits += 1
lin_time = time.time() - start
# 二分探索(bisect)の計測
start = time.time()
hits_b = 0
for t in targets:
idx = bisect.bisect_left(arr, t)
if idx < len(arr) and arr[idx] == t:
hits_b += 1
bin_time = time.time() - start
print(f"linear hits={hits}, time={lin_time:.4f}s")
print(f"binary hits={hits_b}, time={bin_time:.4f}s")
ここでは、線形探索として in を使い、二分探索として bisect_left を使っています。どちらも見つけた件数は同じになるはずですが、n を増やしていくと処理時間の差が広がるのが分かるでしょう。「小さいうちは誤差、大きくなると本質的な差」——これが時間計算量の考え方です。
最後に要点を整理して、今日の学びを自分の言葉に落とし込んでみましょう。どの場面で何を選ぶか、もう迷わなくなるはずです。
次のステップでは、実際に自分のデータで計測してみてください。「本当に速いのはどっち?」という問いに、あなた自身の手で答えを出せるようになります。どのアルゴリズムを選ぶか——それが“Pythonで考える力”の第一歩です。