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 niejbabelkowe? 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
- 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.
- 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.
- 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.