Sortowanie stogowe - Heap Sort krok po kroku

Sortowanie stogowe, znane również jako Heap Sort, to algorytm sortowania wykorzystujący specjalną strukturę danych nazywaną kopcem. Zamiast porównywać elementy jeden po drugim, algorytm najpierw układa dane w taki sposób, aby największa wartość znajdowała się na początku struktury. Następnie przenosi ją na koniec tablicy, przebudowuje pozostały fragment i powtarza całą operację. Brzmi trochę mniej intuicyjnie niż sortowanie przez wstawianie czy bąbelkowe, ale po prześledzeniu kilku kroków mechanizm Heap Sort staje się całkiem logiczny.

Na czym polega sortowanie stogowe?

Najłatwiej wyobrazić sobie Heap Sort jako ciągłe wybieranie największego elementu z tablicy. Różnica polega na tym, że algorytm nie szuka go za każdym razem od początku. Najpierw odpowiednio organizuje elementy, dzięki czemu największa wartość znajduje się w znanym miejscu.

W przypadku sortowania rosnącego budowany jest tak zwany kopiec maksymalny. Oznacza to, że na jego początku znajduje się największy element. Algorytm zamienia go miejscami z ostatnim elementem tablicy. Ten element można już uznać za ustawiony prawidłowo.

Następnie rozmiar obszaru wymagającego sortowania zostaje zmniejszony o jeden. Kopiec jest poprawiany, ponownie największa liczba trafia na początek i cały proces zaczyna się od nowa.

W efekcie największe wartości stopniowo wędrują na prawą stronę tablicy, aż wszystkie elementy znajdą się na swoich miejscach.

Czym jest kopiec w Heap Sort?

Słowo "kopiec" może początkowo sugerować coś skomplikowanego, ale chodzi po prostu o określony sposób organizacji elementów.

Kopiec można przedstawić jako drzewo binarne. W kopcu maksymalnym każdy rodzic ma wartość nie mniejszą niż jego dzieci. Dzięki temu największy element całej struktury znajduje się na samej górze.

Weźmy przykładową tablicę:

[10, 5, 8, 3, 4]

Można ją potraktować jak takie drzewo:

        10
       /  \
      5    8
     / \
    3   4

Wartość 10 jest większa od 5 i 8. Z kolei 5 jest większe od 3 i 4. Taka struktura spełnia więc warunek kopca maksymalnego.

Co ciekawe, w programie nie trzeba tworzyć osobnych obiektów drzewa. Całą strukturę można przechowywać w zwykłej tablicy.

Dla elementu znajdującego się pod indeksem i jego dzieci można znaleźć za pomocą wzorów:

lewe dziecko  = 2 * i + 1
prawe dziecko = 2 * i + 2

Ta zależność pozwala obsługiwać kopiec bez dodatkowych struktur danych.

Heap Sort krok po kroku

Cały algorytm można podzielić na dwie większe części. Najpierw z nieuporządkowanej tablicy tworzymy kopiec maksymalny. Dopiero potem zaczyna się właściwe sortowanie.

  1. Utwórz kopiec maksymalny z elementów znajdujących się w tablicy.
  2. Zamień pierwszy element kopca z ostatnim elementem sortowanego fragmentu.
  3. Zmniejsz rozmiar kopca o jeden.
  4. Przywróć poprawną strukturę kopca.
  5. Ponownie przenieś największy element na koniec.
  6. Powtarzaj operację aż do posortowania całej tablicy.

Ważne jest to, że elementy umieszczone już na końcu tablicy nie biorą udziału w kolejnych przebudowach kopca. Są one traktowane jako gotowa, posortowana część.

Przykład sortowania stogowego

Weźmy niewielką tablicę:

[4, 10, 3, 5, 1]

Najpierw trzeba przekształcić ją w kopiec maksymalny. Po uporządkowaniu relacji pomiędzy elementami możemy otrzymać:

[10, 5, 3, 4, 1]

Największa wartość, czyli 10, znajduje się teraz na początku. Zamieniamy ją z ostatnim elementem:

[1, 5, 3, 4, 10]

Liczba 10 jest już na właściwym miejscu i nie będzie dalej ruszana. Pozostały fragment trzeba ponownie uporządkować jako kopiec:

[5, 4, 3, 1, 10]

Teraz największą wartością w niesortowanej części jest 5. Przenosimy ją na przedostatnią pozycję:

[1, 4, 3, 5, 10]

Po kolejnej naprawie kopca:

[4, 1, 3, 5, 10]

Następna zamiana daje:

[3, 1, 4, 5, 10]

Po wykonaniu wszystkich potrzebnych operacji otrzymujemy:

[1, 3, 4, 5, 10]

Na małej tablicy może się wydawać, że operacji jest sporo. Przewaga Heap Sort pojawia się przede wszystkim wtedy, gdy liczba elementów rośnie i zależy nam na przewidywalnej złożoności algorytmu.

Sortowanie stogowe w Pythonie

Implementacja Heap Sort w Pythonie składa się zazwyczaj z dwóch funkcji. Pierwsza odpowiada za poprawianie kopca, a druga przeprowadza właściwe sortowanie.

def heapify(tablica, n, i):
    najwiekszy = i
    lewy = 2 * i + 1
    prawy = 2 * i + 2

    if lewy < n and tablica[lewy] > tablica[najwiekszy]:
        najwiekszy = lewy

    if prawy < n and tablica[prawy] > tablica[najwiekszy]:
        najwiekszy = prawy

    if najwiekszy != i:
        tablica[i], tablica[najwiekszy] = tablica[najwiekszy], tablica[i]
        heapify(tablica, n, najwiekszy)


def heap_sort(tablica):
    n = len(tablica)

    for i in range(n // 2 - 1, -1, -1):
        heapify(tablica, n, i)

    for i in range(n - 1, 0, -1):
        tablica[0], tablica[i] = tablica[i], tablica[0]
        heapify(tablica, i, 0)


tablica = [4, 10, 3, 5, 1]

print("Przed sortowaniem:", tablica)

heap_sort(tablica)

print("Po sortowaniu:", tablica)

Wynik działania programu:

Przed sortowaniem: [4, 10, 3, 5, 1]
Po sortowaniu: [1, 3, 4, 5, 10]

Jak działa kod Heap Sort?

Najwięcej uwagi wymaga funkcja heapify(). To ona sprawdza, czy element znajdujący się pod indeksem i jest odpowiednio ustawiony względem swoich dzieci.

Najpierw zakładamy, że największym elementem jest rodzic:

najwiekszy = i

Następnie wyliczane są indeksy jego dzieci:

lewy = 2 * i + 1
prawy = 2 * i + 2

Program sprawdza kolejno lewe i prawe dziecko. Jeżeli któreś z nich jest większe od rodzica, zapamiętywany jest jego indeks.

Gdy okaże się, że największy element nie znajduje się na pozycji rodzica, wykonywana jest zamiana:

tablica[i], tablica[najwiekszy] = tablica[najwiekszy], tablica[i]

Po zamianie funkcja wywołuje samą siebie dla niższego poziomu kopca. Jest to potrzebne, ponieważ przesunięcie jednego elementu może zaburzyć strukturę znajdującą się niżej.

Pierwsza pętla funkcji heap_sort() buduje kopiec maksymalny:

for i in range(n // 2 - 1, -1, -1):
    heapify(tablica, n, i)

Druga pętla odpowiada już za przenoszenie największych elementów na koniec:

for i in range(n - 1, 0, -1):
    tablica[0], tablica[i] = tablica[i], tablica[0]
    heapify(tablica, i, 0)

Warto zwrócić uwagę na argument i przekazywany do funkcji heapify(). Oznacza on, że końcowa część tablicy nie jest już brana pod uwagę, ponieważ znajdują się tam wcześniej ustawione elementy.

Złożoność obliczeniowa Heap Sort

Jedną z mocniejszych stron sortowania stogowego jest jego przewidywalność. W przeciwieństwie do niektórych algorytmów Heap Sort nie wpada w bardzo niekorzystny przypadek O(n²) tylko dlatego, że dane zostały ułożone w określony sposób.

Przypadek Złożoność czasowa
Najlepszy przypadek O(n log n)
Przeciętny przypadek O(n log n)
Najgorszy przypadek O(n log n)
Dodatkowa pamięć O(1) dla klasycznej wersji iteracyjnej kopcowania

Samo utworzenie kopca można wykonać w czasie O(n). Później dla każdego elementu wykonywane są operacje związane z przywracaniem kopca, które wymagają maksymalnie O(log n) kroków. Stąd dla całego sortowania otrzymujemy O(n log n).

Pokazana wyżej wersja funkcji heapify() korzysta z rekurencji, dlatego w Pythonie stos wywołań zużywa niewielką dodatkową ilość pamięci. Klasyczny Heap Sort można jednak napisać również bez rekurencji.

Zalety i wady sortowania stogowego

Heap Sort ma kilka właściwości, które sprawiają, że nadal jest ważnym algorytmem omawianym przy nauce struktur danych i sortowania.

Zalety Heap Sort

  • złożoność O(n log n) także w najgorszym przypadku,
  • nie wymaga tworzenia drugiej tablicy o rozmiarze zależnym od liczby elementów,
  • dobrze pokazuje praktyczne wykorzystanie struktury kopca,
  • czas działania jest bardziej przewidywalny niż w przypadku klasycznego Quick Sort w niekorzystnym układzie danych,
  • może sortować tablicę bezpośrednio w jej własnej pamięci.

Wady Heap Sort

  • jest trudniejszy do zrozumienia niż sortowanie bąbelkowe czy przez wstawianie,
  • nie jest algorytmem stabilnym,
  • w praktycznych zastosowaniach może mieć gorszą lokalność pamięci niż niektóre inne algorytmy,
  • jego implementacja jest mniej intuicyjna dla początkujących.

Brak stabilności oznacza, że dwa elementy o tej samej wartości mogą po sortowaniu zmienić swoją wzajemną kolejność. Dla zwykłej tablicy liczb zazwyczaj nie ma to znaczenia, ale może być istotne podczas sortowania bardziej złożonych danych.

Heap Sort a inne algorytmy sortowania

Sortowanie stogowe dobrze wypada pod względem teoretycznej złożoności, ale nie oznacza to, że zawsze będzie najlepszym sposobem sortowania danych.

Algorytm Typowa złożoność Najgorszy przypadek Stabilny
Heap Sort O(n log n) O(n log n) Nie
Insertion Sort O(n²) O(n²) Tak
Merge Sort O(n log n) O(n log n) Tak
Quick Sort O(n log n) O(n²) Zwykle nie

Sortowanie przez wstawianie jest prostsze i może dobrze sprawdzać się dla niewielkich albo prawie uporządkowanych tablic. Heap Sort jest bardziej złożony, ale utrzymuje O(n log n) niezależnie od początkowego ułożenia danych.

Merge Sort również gwarantuje O(n log n), lecz jego klasyczna wersja potrzebuje dodatkowej pamięci na tablice pomocnicze. Quick Sort natomiast jest bardzo popularny i zwykle szybki, ale jego najgorszy przypadek ma złożoność O(n²).

W praktyce języki programowania korzystają z bardziej dopracowanych lub hybrydowych rozwiązań. Dlatego Heap Sort warto znać przede wszystkim jako algorytm, który dobrze łączy temat sortowania z nauką drzew i kopców.

FAQ - sortowanie stogowe

Co to jest sortowanie stogowe?
Sortowanie stogowe, czyli Heap Sort, jest algorytmem sortowania wykorzystującym strukturę kopca. Przy sortowaniu rosnącym najczęściej tworzony jest kopiec maksymalny, dzięki czemu największy element można łatwo przenosić na koniec tablicy.
Co to jest Heap Sort?
Heap Sort to angielska nazwa sortowania stogowego lub sortowania przez kopcowanie. Algorytm najpierw buduje kopiec, a następnie wielokrotnie przenosi największy element na koniec sortowanej tablicy.
Jaka jest złożoność Heap Sort?
Złożoność czasowa Heap Sort wynosi O(n log n) w najlepszym, przeciętnym i najgorszym przypadku. To jedna z cech odróżniających go od algorytmów, które w niekorzystnej sytuacji mogą osiągać O(n²).
Czym jest kopiec maksymalny?
Kopiec maksymalny jest strukturą, w której wartość każdego rodzica jest nie mniejsza od wartości jego dzieci. Dzięki temu największy element znajduje się na początku kopca.
Do czego służy funkcja heapify?
Funkcja heapify przywraca właściwą strukturę kopca. Porównuje rodzica z jego dziećmi i w razie potrzeby zamienia elementy miejscami, a następnie sprawdza niższą część struktury.
Czy Heap Sort jest stabilny?
Nie. Heap Sort nie należy do algorytmów stabilnych. Elementy o identycznej wartości mogą podczas wykonywania kolejnych zamian zmienić swoją wcześniejszą kolejność.
Czy Heap Sort wymaga dodatkowej tablicy?
Klasyczna wersja Heap Sort może działać bezpośrednio na sortowanej tablicy. Nie trzeba tworzyć drugiej tablicy o podobnym rozmiarze, jak ma to miejsce w klasycznym Merge Sort.
Czy Heap Sort jest szybszy od Insertion Sort?
Dla większych, ogólnych zbiorów danych Heap Sort ma korzystniejszą złożoność O(n log n), podczas gdy Insertion Sort osiąga zwykle O(n²). Przy bardzo małych lub prawie posortowanych danych sortowanie przez wstawianie nadal może być bardzo sprawne.
Jak znaleźć dzieci elementu w kopcu?
Jeżeli indeks rodzica wynosi i, indeks lewego dziecka można obliczyć jako 2 * i + 1, a prawego jako 2 * i + 2. Dzięki temu kopiec można przechowywać bezpośrednio w tablicy.
Czy warto uczyć się Heap Sort?
Tak, szczególnie podczas nauki algorytmów i struktur danych. Heap Sort pozwala jednocześnie zrozumieć sortowanie, reprezentację drzewa w tablicy oraz działanie kopca.

Komentarze