Średnio zaawansowane · Wyścig sortowań

Lekcja 6: Funkcje i rekurencja

Zbuduj w Pythonie wzór „Wyścig sortowań” i po drodze naucz się:

  • funkcje i przekazywanie funkcji jako wartości
  • rekurencja: funkcja, która wywołuje samą siebie na mniejszych częściach
  • licznik trzymany w słowniku
  • assert do sprawdzenia wyniku
  • jak rośnie praca: n² wobec n·log n

Najpierw obejrzyj

Film gra z YouTube po kliknięciu.

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))

Nie masz Pythona na tym komputerze? Notatnik uruchamia ten sam skrypt, zadania i odpowiedzi w przeglądarce, w Google Colab. Notatnik jest po angielsku.

Linijka po linijce

  1. def less(a, b):

    Każdy algorytm porównuje przez tę jedną funkcję, więc porównania można liczyć w jednym miejscu.

  2. count["comparisons"] += 1

    Licznik leży w słowniku, więc funkcja może go zmieniać bez słowa global.

  3. def merge(xs):

    Sortowanie przez scalanie: posortuj każdą połowę, potem scal dwie posortowane połowy, biorąc zawsze mniejszy element z przodu.

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

    Rekurencja: merge wywołuje samą siebie na każdej połowie, aż część ma mniej niż dwa elementy.

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

    assert zatrzymuje skrypt, jeśli sortowanie pomyli kolejność: test wbudowany w program.

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

    Dolna granica: log₂ liczby możliwych kolejności, n!, zaokrąglony w górę.

Twoja kolej

  1. Dla n = 100 które sortowanie potrzebuje najmniej porównań i jak blisko jest granicy?

    Podpowiedź

    Uruchom skrypt; wypisuje liczniki i granicę.

    Odpowiedź

    Sortowanie przez scalanie, około 540 przy granicy 525. Sortowanie bąbelkowe potrzebuje dokładnie 4950, czyli 100 · 99 ÷ 2.

  2. Dlaczego to sortowanie bąbelkowe zawsze potrzebuje n·(n − 1) ÷ 2 porównań?

    Podpowiedź

    Spójrz na jego dwie pętle.

    Odpowiedź

    Ta wersja zawsze przechodzi obie pętle do końca: (n − 1) + (n − 2) + … + 1 = n·(n − 1) ÷ 2 porównań, niezależnie od kolejności.

  3. Daj sortowaniu szybkiemu listę, która jest już posortowana. Co dzieje się z licznikiem?

    Podpowiedź

    data = list(range(n)).

    Odpowiedź

    Skacze do 4950, tak źle jak sortowanie bąbelkowe: gdy elementem dzielącym jest pierwszy element, każdy podział zostawia jedną stronę pustą.

Quiz

1. Czym jest rekurencja?
2. Dlaczego less trzyma licznik w słowniku?
3. W ilu kolejnościach mogą przyjść 4 elementy?
4. Co robi assert, gdy jego warunek jest False?
5. Ile mniej więcej porównań potrzebuje sortowanie przez scalanie dla 1000 elementów?

Fiszki

Przeczytaj przód, powiedz sobie, co jest z tyłu, potem otwórz kartę.

  • Rekurencja

    Funkcja, która rozwiązuje problem, wywołując samą siebie na jego mniejszych częściach.

  • assert condition

    Zatrzymuje program z błędem, jeśli warunek jest False.

  • log₂(n!)

    Najmniejsza liczba porównań tak/nie, która odróżni n! kolejności: granica każdego sortowania przez porównania.

  • n² wobec n·log n

    Dla 1000 elementów: około 500 000 wobec około 10 000 kroków.

  • Przekazywanie funkcji

    for name, sort in [("merge", merge), …]: funkcje są wartościami, jak liczby.

Te fiszki na osobnej stronie →