Médio · Corrida de ordenação

Lição 6: Funções e recursividade

Construa em Python o padrão “Corrida de ordenação” e aprenda pelo caminho:

  • funções, e passar uma função como valor
  • recursividade: uma função que se chama a si mesma sobre partes menores
  • um contador guardado num dicionário
  • assert para verificar um resultado
  • como cresce o trabalho: n² contra n·log n

Veja-o primeiro

É reproduzido a partir do YouTube depois de um clique.

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

Não tem Python neste computador? O notebook executa o mesmo script, os exercícios e as respostas no navegador, no Google Colab. O notebook está em inglês.

Linha a linha

  1. def less(a, b):

    Todos os algoritmos comparam através desta única função, por isso as comparações podem ser contadas num só lugar.

  2. count["comparisons"] += 1

    Um dicionário guarda a contagem, para que a função a possa mudar sem a palavra-chave global.

  3. def merge(xs):

    Merge sort: ordenar cada metade e depois juntar as duas metades ordenadas, tirando sempre o menor dos elementos da frente.

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

    Recursividade: merge chama-se a si mesma sobre cada metade, até uma parte ter menos de dois elementos.

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

    assert para o script se uma ordenação errar a ordem: um teste embutido no programa.

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

    O limite inferior: log₂ do número de ordens possíveis, n!, arredondado para cima.

Experimente

  1. Para n = 100, que ordenação precisa de menos comparações, e quão perto fica do limite?

    Pista

    Execute o script; ele mostra as contagens e o limite.

    Resposta

    O merge sort, cerca de 540 contra um limite de 525. O bubble sort precisa de exatamente 4950, ou seja, 100 · 99 ÷ 2.

  2. Por que razão este bubble sort precisa sempre de n·(n − 1) ÷ 2 comparações?

    Pista

    Olhe para os seus dois loops.

    Resposta

    Esta versão percorre sempre os dois loops até o fim: (n − 1) + (n − 2) + … + 1 = n·(n − 1) ÷ 2 comparações, seja qual for a ordem.

  3. Dê ao quicksort uma lista que já está ordenada. O que acontece à sua contagem?

    Pista

    data = list(range(n)).

    Resposta

    Salta para 4950, tão mau como o bubble sort: com o primeiro elemento como pivô, cada divisão deixa um dos lados vazio.

Quiz

1. O que é a recursividade?
2. Por que razão less guarda a sua contagem num dicionário?
3. Em quantas ordens podem vir 4 elementos?
4. O que faz assert quando a sua condição é False?
5. Para 1000 elementos, de cerca de quantas comparações precisa o merge sort?

Flashcards

Leia a frente, tente recordar o verso e só depois abra o cartão.

  • Recursividade

    Uma função que resolve um problema chamando-se a si mesma sobre partes menores dele.

  • assert condition

    Para o programa com um erro se a condição for False.

  • log₂(n!)

    O menor número de comparações de sim ou não capaz de distinguir n! ordens: o limite de qualquer ordenação por comparação.

  • n² contra n·log n

    Para 1000 elementos: cerca de 500 000 contra cerca de 10 000 passos.

  • Passar uma função

    for name, sort in [("merge", merge), …]: as funções são valores, como os números.

Estes flashcards numa página à parte →