Sortowanie bąbelkowe

🎯 Po co Ci to?

Wchodzimy do kuchni sortowania — i zaczynamy od algorytmu, którego nikt nie używa w produkcji, a każdy informatyk świata zna go na pamięć. Paradoks? Nie: bąbelkowe jest dla sortowań tym, czym rower treningowy dla kolarstwa. Jest tak proste, że całą uwagę możesz poświęcić rzeczom, które w sortowaniu naprawdę trudne: liczeniu porównań i zamian, rozumowaniu „dlaczego to na pewno się kończy i na pewno sortuje" i pierwszym optymalizacjom. A nazwa — zobaczysz — jest najładniejsza w całej informatyce.

✅ Czego się nauczysz

Po tej jednostce potrafisz:

  • wykonać i zaprogramować sortowanie bąbelkowe;
  • uzasadnić jego poprawność („po $i$-tym przebiegu $i$ największych stoi na miejscu");
  • policzyć koszt $\sim n^2$ i przyspieszyć algorytm flagą wczesnego wyjścia.

📘 Wyjaśnienie

Pomysł. Idź wzdłuż listy i porównuj sąsiadów: jeśli stoją w złej kolejności (lewy większy) — zamień. Po jednym takim przebiegu największy element „przebąbelkuje" na sam koniec, jak bańka powietrza w wodzie — stąd nazwa. Powtórz przebieg dla reszty; po $n-1$ przebiegach wszystko stoi.

def babelkowe(L):
    n = len(L)
    for i in range(n - 1):                 # przebiegi
        for j in range(n - 1 - i):         # sąsiedzi: coraz krótszy odcinek!
            if L[j] > L[j + 1]:
                L[j], L[j + 1] = L[j + 1], L[j]     # zamiana sąsiadów

Dwa smaczki. Zamiana L[j], L[j+1] = L[j+1], L[j] to podmiana równoczesna (znasz ją z Fibonacciego 4.3 — bez niej potrzebowałbyś zmiennej pomocniczej). A zasięg wewnętrznej pętli maleje z każdym przebiegiem (n - 1 - i): skoro po pierwszym przebiegu największy element stoi na końcu, nie ma po co go więcej odwiedzać. Ta obserwacja to zarazem dowód poprawności: po $i$ przebiegach ostatnie $i$ elementów to $i$ największych, we właściwej kolejności (niezmiennik! — jak przy binarnym); po $n-1$ przebiegach niezmiennik obejmuje całą listę.

Koszt. Porównań: $(n-1) + (n-2) + \dots + 1 = \frac{n(n-1)}{2}$ — suma Gaussa z 1.5, tym razem w roli rachunku kosztów! Dla $n = 1000$: pół miliona porównań; dla miliona elementów: pół biliona — kwadratowy wzrost, o którym ostrzegał algorytm C z jednostki 1.5. Zamian bywa od zera (lista już posortowana) do tyluż co porównań (lista odwrócona).

💭 Pomyśl: Lista [1, 2, 3, 4, 5] jest już posortowana. Ile porównań i ile zamian wykona na niej babelkowe? Co Cię w tej odpowiedzi drażni?

Sprawdź odpowiedź

Zamian: zero. Porównań: pełne $\frac{5 \cdot 4}{2} = 10$ — algorytm sumiennie odbębnia wszystkie przebiegi, niczego nie zauważając. Drażni słusznie: skoro w całym przebiegu nie było ani jednej zamiany, to lista już jest posortowana i dalsze przebiegi są puste. Wystarczy to zauważać — flaga:

def babelkowe_flaga(L):
    n = len(L)
    for i in range(n - 1):
        bylo = False
        for j in range(n - 1 - i):
            if L[j] > L[j + 1]:
                L[j], L[j + 1] = L[j + 1], L[j]
                bylo = True
        if not bylo:
            return                     # przebieg bez zamian = koniec pracy

Na posortowanej liście: jeden przebieg, $n-1$ porównań, wyjście. Na „prawie posortowanej" — kilka przebiegów. Najgorszy przypadek bez zmian ($n^2$), ale najlepszy spadł do liniowego. Warto zapamiętać samą figurę: wykryj, że nie ma już nic do roboty, i przestań.

🧮 Prześledź

Posortuj bąbelkowo [5, 1, 4, 2] — wypisz listę po każdej zamianie (nie tylko po przebiegach).

Sprawdź odpowiedź

Przebieg 1: porównaj 5,1 → zamiana → [1,5,4,2]; 5,4 → zamiana → [1,4,5,2]; 5,2 → zamiana → [1,4,2,5] (piątka na miejscu). Przebieg 2: 1,4 — ok; 4,2 → zamiana → [1,2,4,5]. Przebieg 3: 1,2 — ok. Wynik [1,2,4,5], 6 porównań, 4 zamiany. Widać „bąbelek": 5 wędrowała w prawo przez całą listę w jednym przebiegu. A 2, żeby dojść na miejsce, potrzebowała… oj — dwójka wędrowała w lewo po jednym oczku na przebieg. Duże elementy płyną szybko, małe toną powoli — ta asymetria to charakterystyczna cecha bąbelkowego (i powód istnienia wariantu dwukierunkowego, tzw. koktajlowego).

⚠️ Uwaga, pułapka

babelkowe(L) sortuje listę w miejscu — zmienia dane wywołującego (lista to wspólny obiekt, 3.4!). To bywa pożądane (oszczędność pamięci) i bywa katastrofą (kolejność „oryginalna" przepada bezpowrotnie). Obie umowy są legalne — sortowanie w miejscu albo zwracanie posortowanej kopii — ale specyfikacja musi mówić, którą zawarto, a wywołujący musi wiedzieć. Wbudowane narzędzia Pythona oferują obie: L.sort() w miejscu, sorted(L) — kopia.

🛠️ Teraz Ty

Bez komputera: posortuj bąbelkowo (z notowaniem zamian) listę ocen [3, 6, 2, 6, 1]. Z komputerem: zaimplementuj wersję z flagą i dołóż liczniki porównań i zamian; uruchom na trzech listach po 20 elementów — posortowanej, odwróconej i losowej — i zanotuj wyniki. Te trzy liczby wrócą w 6.5 jako materiał dowodowy.

📐 Definicje tej lekcji

  • Sortowanie bąbelkowe — przebiegi zamian sąsiadów; po $i$-tym przebiegu $i$ największych elementów stoi na końcu.
  • Flaga wczesnego wyjścia — przebieg bez zamian kończy sortowanie; najlepszy przypadek spada do $n-1$ porównań.
  • Sortowanie w miejscu — algorytm przestawia elementy w oryginalnej liście, zamiast budować nową.

📌 Najważniejsze w pigułce

  • Porównuj sąsiadów, zamieniaj złe pary; największy bąbelkuje na koniec — to zarazem szkic dowodu poprawności.
  • Koszt $\frac{n(n-1)}{2}$ porównań — Gauss w rachunku kosztów; kwadrat rośnie bezlitośnie.
  • Flaga „bez zamian = koniec" ratuje najlepszy przypadek; o umowie „w miejscu czy kopia" mówi specyfikacja.

🎒 Zadania

  1. Ile dokładnie porównań i zamian wykona bąbelkowe (bez flagi) na liście [4, 3, 2, 1]? Zapisz stan po każdym przebiegu.
Wskazówka i odpowiedź

Odwrócona lista to najgorszy przypadek: każde porównanie to zamiana. Przebieg 1: [3,2,1,4] (3 zamiany); przebieg 2: [2,1,3,4] (2); przebieg 3: [1,2,3,4] (1). Porównań $3+2+1 = 6$, zamian 6. Dla odwróconej listy długości $n$: $\frac{n(n-1)}{2}$ obu — komplet roboty przy każdym kroku.

  1. W bąbelkowym zamieniamy tylko sąsiadów. Uzasadnij, że liczba zamian, którą musi wykonać (na dowolnej liście), jest równa liczbie par elementów stojących w złej kolejności (tzw. inwersji) — i policz inwersje w [3, 1, 4, 2].
Wskazówka i odpowiedź

Każda zamiana sąsiadów naprawia dokładnie jedną inwersję (tę między nimi) i żadnej nie psuje — więc zamian musi być tyle, ile inwersji na starcie. [3,1,4,2]: pary złe to (3,1), (3,2), (4,2) — trzy inwersje, więc bąbelkowe wykona dokładnie 3 zamiany (sprawdź!). Inwersje to „miara bałaganu" listy — i granica dolna dla każdego sortowania zamieniającego tylko sąsiadów. Chcesz sortować szybciej niż $n^2$? Musisz zamieniać elementy odległe — zapamiętaj tę myśl do 6.6 i 6.7.

  1. Zaproponuj dane, na których wersja z flagą jest dramatycznie szybsza od wersji bez flagi, oraz takie, na których flaga nic nie daje. Sformułuj regułę: kiedy flaga się opłaca?
Wskazówka i odpowiedź

Dramatyczna wygrana: lista posortowana z jednym „świeżym" elementem blisko końca — jeden-dwa przebiegi zamiast $n-1$. Zero zysku: lista odwrócona (zamiany do samego końca). Reguła: flaga opłaca się na danych prawie posortowanych — a takie są w praktyce zaskakująco częste (dopisujesz nowe wyniki do wczorajszego rankingu). „Jakie dane są typowe?" to pytanie o realny koszt — usłyszysz je jeszcze w 6.4 i 6.7.

🔍 Sprawdź, czy umiesz

  • Wykonać bąbelkowe na kartce, licząc porównania i zamiany.
  • Wypowiedzieć niezmiennik przebiegów i wyprowadzić z niego poprawność.
  • Dodać flagę i wskazać dane, na których zmienia ona świat.

Ucz się tej jednostki z asystentem