Dataczwartek, 13 sierpnia 2026 Czas18:01:11
← Algorytmy i złożoność

Lekcja 3. Algorytmy sortowania — bąbelkowe i przez scalanie (dziel i zwyciężaj)

Średni

Po co się tego uczymy?

Sortowanie to jedna z najczęściej wykonywanych operacji w informatyce — a strategia "dziel i zwyciężaj" (poznana tu na przykładzie sortowania przez scalanie) to jeden z najważniejszych, uniwersalnych sposobów myślenia o dużych problemach, wymagany na maturze rozszerzonej.

Teoria

Rekursja (ang. recursion) to sytuacja, w której funkcja wywołuje SAMĄ SIEBIE, aż do osiągnięcia warunku stopu. Klasyczny przykład — silnia: silnia(n) = n * silnia(n-1), z warunkiem stopu silnia(0) = 1.

Strategia "dziel i zwyciężaj" (divide and conquer) to sposób rozwiązywania problemów w trzech krokach: Podziel (Divide) — duży problem dzielimy na mniejsze, prostsze podproblemy; Rozwiąż (Conquer) — rozwiązujemy każdy podproblem (najczęściej rekurencyjnie); Połącz (Combine) — składamy rozwiązania podproblemów w całość. Wyszukiwanie binarne (poprzednia lekcja) to najprostszy przykład tej strategii — za każdym razem bierzemy tylko połowę listy i rozwiązujemy problem tak samo (rekurencyjnie) na mniejszym fragmencie.

Sortowanie przez scalanie (MergeSort) to algorytm sortowania oparty WŁAŚNIE na strategii dziel i zwyciężaj: (1) dzielimy tablicę na dwie połowy, (2) sortujemy REKURENCYJNIE każdą połowę, (3) SCALAMY (łączymy) obie posortowane listy w jedną posortowaną całość. Cechy: zawsze działa w czasie O(n log n) niezależnie od danych wejściowych, wymaga dodatkowej pamięci na proces scalania (O(n)), jest STABILNY (zachowuje kolejność elementów równych).

Proces scalania dwóch posortowanych list — porównujemy PIERWSZE elementy obu list, bierzemy MNIEJSZY z nich do wyniku, i powtarzamy, aż jedna z list się skończy — wtedy dokładamy resztę drugiej.

Sortowanie bąbelkowe (BubbleSort) to jeden z najprostszych algorytmów sortowania — NIE stosuje strategii dziel i zwyciężaj, tylko wielokrotnie przechodzi przez tablicę, porównując SĄSIEDNIE elementy i zamieniając je miejscami, jeśli są w złej kolejności. Największe elementy stopniowo "wypływają" na koniec tablicy — jak bąbelki powietrza w wodzie (stąd nazwa). Prostszy w implementacji niż MergeSort, ale znacznie WOLNIEJSZY dla dużych zbiorów danych (złożoność O(n²) kontra O(n log n)).

Schemat

MERGESORT (dziel i zwyciężaj):
[42, 17, 23, 17, 58, 34]
        ↓ DZIEL na połowy
[42, 17, 23]        [17, 58, 34]
   ↓ dziel dalej         ↓ dziel dalej
[42][17,23]           [17][58,34]
      ↓ scal               ↓ scal
[42][17,23]→[17,23]   [17][34,58]→[17,34,58]
        ↓ scal                ↓
   [17,23,42]            [17,34,58]
             ↓ SCAL OBIE POŁÓWKI
        [17, 17, 23, 34, 42, 58]

BUBBLESORT (porównania sąsiadów, [5,3,8,4,2]):
przejście 1: 5,3→zamiana | 3,5,8→bez | 8,4→zamiana | 8,2→zamiana → [3,5,4,2,8]
przejście 2-4: kolejne przejścia "wypychają" coraz mniejsze liczby w lewo
wynik: [2, 3, 4, 5, 8]

Przykład z życia

Sortowanie listy uczniów wg średniej ocen w dzienniku elektronicznym, sortowanie wyników wyszukiwania w sklepie internetowym wg ceny, sortowanie zdjęć wg daty w telefonie — wszystkie te operacje "pod maską" korzystają z algorytmów bardzo podobnych do MergeSort, bo jest szybki i przewidywalny nawet dla milionów elementów.

mergesort.py

# Rekursja - przypomnienie (silnia)
def silnia(n):
    if n == 0:
        return 1        # warunek stopu
    else:
        return n * silnia(n-1)


# MergeSort - sortowanie przez scalanie (dziel i zwyciężaj)
def scalaj(lewa, prawa):
    wynik = []
    i = j = 0
    # dopóki w obu listach są elementy
    while i < len(lewa) and j < len(prawa):
        if lewa[i] <= prawa[j]:
            wynik.append(lewa[i])
            i += 1
        else:
            wynik.append(prawa[j])
            j += 1
    # dokładamy resztę (jedna z list mogła się skończyć wcześniej)
    wynik.extend(lewa[i:])
    wynik.extend(prawa[j:])
    return wynik

def mergesort(tab):
    if len(tab) <= 1:  # warunek stopu
        return tab
    srodek = len(tab) // 2
    lewa = mergesort(tab[:srodek])   # rekurencyjnie sortujemy lewą
    prawa = mergesort(tab[srodek:])  # rekurencyjnie sortujemy prawą
    return scalaj(lewa, prawa)       # scalanie

# przykład
punkty = [42, 17, 23, 17, 58, 34]
print("Posortowane:", mergesort(punkty))
# Wynik: Posortowane: [17, 17, 23, 34, 42, 58]

bubble_sort.py

# BubbleSort - sortowanie bąbelkowe (proste, ale wolniejsze - O(n^2))
def bubble_sort(tab):
    n = len(tab)
    for i in range(n-1):        # powtarzamy n-1 razy
        for j in range(n-1-i):  # za każdym razem o 1 porównanie mniej
            if tab[j] > tab[j+1]:
                # zamiana miejscami
                tab[j], tab[j+1] = tab[j+1], tab[j]
    return tab

# przykład
dane = [5, 3, 8, 4, 2]
print("Posortowane:", bubble_sort(dane))
# Wynik: Posortowane: [2, 3, 4, 5, 8]

Komentarz i wyjaśnienie kodu

W mergesort() warunek stopu if len(tab) <= 1 mówi: LISTA JEDNOELEMENTOWA (albo pusta) jest Z DEFINICJI posortowana — nie trzeba jej już dzielić. Cała "magia" dzieje się w scalaj(), która ZAWSZE łączy dwie JUŻ POSORTOWANE listy, porównując ich czoła.

W bubble_sort() zewnętrzna pętla for i in range(n-1) kontroluje liczbę PRZEJŚĆ przez tablicę, a wewnętrzna range(n-1-i) z każdym przejściem sprawdza O JEDEN ELEMENT MNIEJ — bo po każdym przejściu kolejny "najcięższy" element trafia na swoje ostateczne miejsce na końcu i nie trzeba go już porównywać.

Ćwiczenie samodzielne

Uruchom mergesort() i bubble_sort() dla tej samej listy 6-8 losowych liczb i porównaj wynik. Prześledź ręcznie na kartce, jak dzieli się lista w MergeSort, aż dojdzie do pojedynczych elementów.

Zadania do pracy własnej

  1. Posortuj listę wyników testu uczniów [42, 17, 23, 17, 58, 34] algorytmem bąbelkowym — wypisz stan tablicy PO KAŻDYM przejściu zewnętrznej pętli.

  2. Zaimplementuj funkcję scalaj() samodzielnie (bez podglądania przykładu) i przetestuj ją na dwóch już posortowanych listach: [1, 4, 7] i [2, 3, 9].

  3. Zmodyfikuj mergesort() tak, żeby przy każdym wywołaniu wypisywała, na jakie dwie części dzieli aktualną listę — uruchom dla listy 8-elementowej i prześledź CAŁE drzewo podziałów i scaleń, porównując je z wynikiem ręcznych obliczeń.

Typowe błędy

Zapominanie o warunku stopu w rekurencji (if len(tab) <= 1: return tab) — bez niego mergesort() próbowałby dzielić listę w nieskończoność.

Błędna kolejność porównań w scalaj() — jeśli pomylisz <= z >=, scalanie odwróci kolejność zamiast ją zachować.

Przekonanie, że BubbleSort "wystarczy" dla dużych zbiorów danych — dla 10 elementów różnica między BubbleSort (O(n²)) a MergeSort (O(n log n)) jest niezauważalna, ale dla 1 000 000 elementów BubbleSort może być TYSIĄCE razy wolniejszy.

Nawiązanie do egzaminu zawodowego

Strategia "dziel i zwyciężaj" (MergeSort) i jej złożoność O(n log n) w porównaniu do O(n²) sortowania bąbelkowego to klasyczny temat analizy algorytmów na maturze rozszerzonej — bezpośrednio przygotowuje do lekcji o analizie złożoności obliczeniowej (ostatnia lekcja tego działu) oraz o programowaniu dynamicznym, który rozwija ideę "dziel na mniejsze podproblemy" o dodatkowe zapamiętywanie wyników.