中級 · ソート競争

レッスン6:関数と再帰

Pythonで 「ソート競争」 を作りながら、次のことを学びます:

  • 関数と、関数を値として渡すこと
  • 再帰:小さな部分について自分自身を呼ぶ関数
  • 辞書に入れておくカウンター
  • 結果を確かめる assert
  • 仕事の増え方:n² 対 n·log n

まず見てみる

クリックするとYouTubeから再生されます。

sorting-race.py
"""Sorting race: four algorithms on the same shuffled list, counting their comparisons.
Run: python sorting-race.py   (needs: pip install numpy matplotlib)
"""
import math

import numpy as np
import matplotlib.pyplot as plt

plt.style.use("dark_background")

n = 100
data = list(np.random.default_rng().permutation(n))
count = {"comparisons": 0}


def less(a, b):  # every algorithm compares only through this, so we can count
    count["comparisons"] += 1
    return a < b


def bubble(xs):
    xs = xs[:]
    for end in range(len(xs) - 1, 0, -1):
        for i in range(end):
            if less(xs[i + 1], xs[i]):
                xs[i], xs[i + 1] = xs[i + 1], xs[i]
    return xs


def insertion(xs):
    xs = xs[:]
    for i in range(1, len(xs)):
        j = i
        while j > 0 and less(xs[j], xs[j - 1]):
            xs[j], xs[j - 1] = xs[j - 1], xs[j]
            j -= 1
    return xs


def merge(xs):
    if len(xs) < 2:
        return xs
    left, right = merge(xs[: len(xs) // 2]), merge(xs[len(xs) // 2 :])
    out = []
    while left and right:
        out.append(left.pop(0) if not less(right[0], left[0]) else right.pop(0))
    return out + left + right


def quick(xs):
    if len(xs) < 2:
        return xs
    pivot, rest = xs[0], xs[1:]
    smaller = [x for x in rest if less(x, pivot)]
    return quick(smaller) + [pivot] + quick([x for x in rest if x not in smaller])


results = {}
for name, sort in [("bubble", bubble), ("insertion", insertion), ("merge", merge), ("quick", quick)]:
    count["comparisons"] = 0
    assert sort(data) == sorted(data)
    results[name] = count["comparisons"]
print(results, "| at least log2(n!) =", math.ceil(math.log2(math.factorial(n))))

plt.figure(figsize=(7, 4))
plt.bar(list(results), list(results.values()), color=["#f0abfc", "#c4b5fd", "#5eead4", "#5eead4"])
plt.axhline(math.log2(math.factorial(n)), color="white", linestyle="--", label="log2(n!): no comparison sort does better")
plt.ylabel(f"comparisons for n = {n}")
plt.legend()
plt.show()

# Try: n = 1000 (bubble sort needs about n*n/2, merge sort about n*log2(n))

このコンピューターにPythonが入っていませんか?ノートブックなら、同じスクリプト、練習問題、答えを、ブラウザ上のGoogle Colabで実行できます。ノートブックは英語です。

1行ずつ

  1. def less(a, b):

    どのアルゴリズムもこのひとつの関数を通して比べるので、比較の回数を1か所で数えられます。

  2. count["comparisons"] += 1

    回数は辞書に入れておくので、関数は global キーワードなしでそれを変えられます。

  3. def merge(xs):

    マージソート:半分ずつを並べ替え、それから並べ替えた2つの半分を、先頭の小さいほうを取り続けることでまとめます。

  4. left, right = merge(xs[: len(xs) // 2]), merge(xs[len(xs) // 2 :])

    再帰です。merge は、部分の要素が2個より少なくなるまで、それぞれの半分について自分自身を呼びます。

  5. assert sort(data) == sorted(data)

    ソートが順番をまちがえると、assert がスクリプトを止めます。プログラムに組みこまれたテストです。

  6. math.ceil(math.log2(math.factorial(n)))

    下限です。ありうる順番の数 n! の log₂ を、切り上げたものです。

やってみよう

  1. n = 100 のとき、比較がいちばん少なくてすむのはどのソートですか?また、下限にどれくらい近いですか?

    ヒント

    スクリプトを実行しましょう。回数と下限が表示されます。

    答え

    マージソートで、下限525に対して約540回です。バブルソートはちょうど4,950回、つまり 100 · 99 ÷ 2 回必要です。

  2. このバブルソートが、いつも n·(n − 1) ÷ 2 回の比較を必要とするのはなぜですか?

    ヒント

    2つのループを見てみましょう。

    答え

    この版は、どちらのループもいつも最後まで回します:(n − 1) + (n − 2) + … + 1 = n·(n − 1) ÷ 2 回の比較で、もとの順番には関係ありません。

  3. クイックソートに、すでに並んでいるリストを渡してみましょう。回数はどうなりますか?

    ヒント

    data = list(range(n))。

    答え

    4,950回にはね上がり、バブルソートと同じくらい悪くなります。最初の要素をピボットにすると、どの分割でも片側が空になるからです。

クイズ

1. 再帰とは何ですか?
2. less が回数を辞書に入れておくのはなぜですか?
3. 4個の要素は、何通りの順番で並びえますか?
4. 条件が False のとき、assert は何をしますか?
5. 1,000個の要素なら、マージソートにはおよそ何回の比較が必要ですか?

フラッシュカード

表を読み、裏に何があるかを自分で言ってみてから、カードを開いてください。

  • 再帰

    問題の小さな部分について自分自身を呼ぶことで、問題を解く関数。

  • assert condition

    条件が False なら、エラーでプログラムを止めます。

  • log₂(n!)

    n! 通りの順番を見分けられる、「はい」か「いいえ」の比較のいちばん少ない回数。比較によるどのソートにとっても限界です。

  • n² 対 n·log n

    1,000個なら、約500,000ステップ対約10,000ステップ。

  • 関数を渡す

    for name, sort in [("merge", merge), …]:関数は、数と同じように値です。

このフラッシュカードを別のページで →