Sortowanie przez scalanie (merge sort)

  • Informatyka
  • algorytm sortowania
  • dziel i zwyciężaj
  • złożoność czasowa O(n log n)
  • złożoność pamięciowa O(n)
  • jak działa Merge Sort
  • Sortowanie przez scalanie…

Podcast

Sortowanie przez scalanie: Metoda 'Dziel i Zwyciężaj'0:00 / 14:43
0:001:00 pozostało

Sortowanie przez Scalanie (Merge Sort): Kompleksowy Przewodnik dla Studentów

Sortowanie przez scalanie, znane również jako Merge Sort, to jeden z najbardziej efektywnych i powszechnie stosowanych algorytmów sortowania. Jest to algorytm porównawczy, co oznacza, że porządkuje elementy poprzez ich wzajemne porównywanie. Jego kluczową zaletą jest gwarantowana złożoność czasowa O(n log n) w każdym przypadku, co czyni go przewidywalnym i niezawodnym. Algorytm ten opiera się na paradygmacie „dziel i zwyciężaj” (ang. Divide and Conquer), fundamentalnej technice w informatyce, którą warto znać.

Na Czym Polega Paradygmat „Dziel i Zwyciężaj” w Merge Sort?

Paradygmat „dziel i zwyciężaj” w sortowaniu przez scalanie polega na rozwiązywaniu problemu poprzez rozbicie go na mniejsze, łatwiejsze do rozwiązania podproblemy. Następnie rozwiązania tych podproblemów są łączone w celu uzyskania rozwiązania oryginalnego problemu. W przypadku Merge Sort proces ten obejmuje trzy główne kroki:

  1. Dzielenie (Divide): Problem sortowania listy n elementów jest dzielony na dwa podproblemy sortowania list o około n/2 elementach. Proces ten jest kontynuowany rekurencyjnie, aż do momentu, gdy każda podlista zawiera tylko jeden element. Lista składająca się z jednego elementu jest z definicji posortowana.
  2. Zwyciężanie (Conquer): Podproblemy są rozwiązywane rekurencyjnie. „Rozwiązaniem” najmniejszego podproblemu (listy jednoelementowej) jest po prostu ta sama lista, ponieważ jest już posortowana.
  3. Scalanie (Combine): Posortowane podlisty są łączone (scalane) w taki sposób, aby utworzyć jedną, większą, posortowaną listę. Ten krok scalania jest kluczowy dla działania algorytmu i odpowiada za jego efektywność.

Rekurencyjny Proces Dzielenia w Sortowaniu przez Scalanie

Algorytm Merge Sort rozpoczyna się od nieposortowanej tablicy. Pierwszym krokiem jest podział tej tablicy na dwie równe (lub prawie równe) części. Każda z tych części jest następnie traktowana jako osobny problem i ponownie dzielona na pół. Ten proces rekurencyjnego dzielenia trwa, aż do momentu, gdy uzyskamy podtablice zawierające tylko jeden element. Każda taka jednoelementowa podtablica jest uznawana za posortowaną, co stanowi warunek bazowy rekurencji.

Przykład procesu dzielenia: Rozważmy tablicę: [38, 27, 43, 3, 9, 82, 10]

    1. Podziel na dwie części: [38, 27, 43, 3] i [9, 82, 10]
    1. Pierwsza część: [38, 27, 43, 3] dzieli się na [38, 27] i [43, 3]
    1. [38, 27] dzieli się na [38] i [27]
    1. [43, 3] dzieli się na [43] i [3]
    1. Druga część: [9, 82, 10] dzieli się na [9, 82] i [10]
    1. [9, 82] dzieli się na [9] i [82]

W tym momencie mamy wiele jednoelementowych tablic: [38], [27], [43], [3], [9], [82], [10]. Wszystkie są już posortowane.

Szczegółowy Proces Scalania (Merge) w Merge Sort

Po osiągnięciu jednoelementowych podtablic, algorytm przechodzi do fazy scalania. Scalanie polega na łączeniu dwóch posortowanych podtablic w jedną większą, również posortowaną tablicę. Odbywa się to poprzez porównywanie pierwszych elementów z obu podtablic i wybieranie mniejszego z nich, który jest następnie umieszczany w wynikowej tablicy. Proces ten jest powtarzany, aż wszystkie elementy z obu podtablic zostaną przeniesione do tablicy wynikowej. Aby efektywnie scalić dwie posortowane tablice, zazwyczaj potrzebna jest dodatkowa pamięć (tablica pomocnicza) o rozmiarze równym sumie rozmiarów scalanych tablic. Dzięki temu elementy są umieszczane w odpowiedniej kolejności bez nadpisywania danych w oryginalnych tablicach.

Kontynuując przykład scalania:

    1. Scal [38] i [27] → [27, 38]
    1. Scal [43] i [3] → [3, 43]
    1. Scal [9] i [82] → [9, 82]
    1. [10] pozostaje jako [10] (nie ma z czym scalać na tym poziomie)

Teraz mamy posortowane podtablice: [27, 38], [3, 43], [9, 82], [10].

Następny poziom scalania:

    1. Scal [27, 38] i [3, 43]:
  • Porównaj 27 i 3. Mniejsze jest 3. Wynik: [3]
  • Porównaj 27 i 43. Mniejsze jest 27. Wynik: [3, 27]
  • Porównaj 38 i 43. Mniejsze jest 38. Wynik: [3, 27, 38]
  • Pozostał 43. Wynik: [3, 27, 38, 43]
    1. Scal [9, 82] i [10]:
  • Porównaj 9 i 10. Mniejsze jest 9. Wynik: [9]
  • Porównaj 82 i 10. Mniejsze jest 10. Wynik: [9, 10]
  • Pozostał 82. Wynik: [9, 10, 82]

Teraz mamy posortowane podtablice: [3, 27, 38, 43] i [9, 10, 82].

Ostatnie scalanie:

    1. Scal [3, 27, 38, 43] i [9, 10, 82]:
  • Porównaj 3 i 9. Mniejsze jest 3. Wynik: [3]
  • Porównaj 27 i 9. Mniejsze jest 9. Wynik: [3, 9]
  • Porównaj 27 i 10. Mniejsze jest 10. Wynik: [3, 9, 10]
  • Porównaj 27 i 82. Mniejsze jest 27. Wynik: [3, 9, 10, 27]
  • Porównaj 38 i 82. Mniejsze jest 38. Wynik: [3, 9, 10, 27, 38]
  • Porównaj 43 i 82. Mniejsze jest 43. Wynik: [3, 9, 10, 27, 38, 43]
  • Pozostał 82. Wynik: [3, 9, 10, 27, 38, 43, 82]

Ostateczna posortowana tablica to: [3, 9, 10, 27, 38, 43, 82].

Złożoność Czasowa Merge Sort: O(n log n)

Złożoność czasowa sortowania przez scalanie jest wyrażana jako O(n log n). Aby zrozumieć, dlaczego tak jest, należy rozważyć dwa główne etapy:

  1. Dzielenie: Tablica o rozmiarze n jest dzielona na pół, a następnie te połówki są dzielone dalej, aż do uzyskania jednoelementowych tablic. Liczba poziomów podziału jest logarytmiczna względem rozmiaru tablicy, czyli log₂n. Na przykład, dla tablicy 8-elementowej, mamy log₂8 = 3 poziomy podziału.
  2. Scalanie: Na każdym poziomie rekurencji, operacja scalania dwóch podtablic o łącznym rozmiarze k zajmuje czas proporcjonalny do k. Dzieje się tak, ponieważ każdy element jest porównywany i przenoszony raz. Ponieważ na każdym poziomie rekurencji sumaryczna liczba elementów do scalenia wynosi n, koszt scalania na każdym poziomie wynosi O(n). Na najniższym poziomie scalane są n/2 pary jednoelementowych tablic, na kolejnym n/4 pary dwuelementowych tablic itd. Suma tych operacji zawsze wynosi O(n).

Ponieważ mamy log n poziomów, a każdy poziom kosztuje O(n), całkowita złożoność czasowa wynosi O(n · log n). Jest to prawda dla najlepszego, średniego i najgorszego przypadku, co jest jedną z głównych zalet Merge Sortu w porównaniu do algorytmów takich jak Quick Sort, którego złożoność w najgorszym przypadku może wynosić O(n²).

Złożoność Pamięciowa Sortowania przez Scalanie: O(n)

Sortowanie przez scalanie wymaga dodatkowej pamięci, co jest jego główną wadą w porównaniu do niektórych algorytmów sortowania w miejscu. Podczas operacji scalania dwóch podtablic, zazwyczaj tworzona jest tymczasowa tablica pomocnicza do przechowywania scalonych elementów. Rozmiar tej tablicy pomocniczej jest proporcjonalny do liczby scalanych elementów, co oznacza, że potrzebujemy dodatkowej pamięci o rozmiarze O(n), gdzie n jest liczbą elementów w oryginalnej tablicy. Istnieją warianty Merge Sortu, które próbują zmniejszyć wymagania pamięciowe, ale są one zazwyczaj bardziej skomplikowane lub mniej wydajne czasowo.

Pseudokod Algorytmu Merge Sort

PROCEDURA MergeSort(tablica, lewy, prawy):
 JEŻELI lewy < prawy:
 srodek = (lewy + prawy) / 2
 MergeSort(tablica, lewy, srodek) // Rekurencyjnie sortuj lewą połowę
 MergeSort(tablica, srodek + 1, prawy) // Rekurencyjnie sortuj prawą połowę
 Merge(tablica, lewy, srodek, prawy) // Scal dwie posortowane połowy

PROCEDURA Merge(tablica, lewy, srodek, prawy):
 rozmiar_lewej = srodek - lewy + 1
 rozmiar_prawej = prawy - srodek
 // Utwórz tymczasowe tablice
 L = nowa tablica o rozmiarze rozmiar_lewej
 R = nowa tablica o rozmiarze rozmiar_prawej
 // Skopiuj dane do tymczasowych tablic L[] i R[]
 DLA i OD 0 DO rozmiar_lewej - 1:
 L[i] = tablica[lewy + i]
 DLA j OD 0 DO rozmiar_prawej - 1:
 R[j] = tablica[srodek + 1 + j]
 // Scal tymczasowe tablice z powrotem do tablica[lewy..prawy]
 i = 0 // Początkowy indeks pierwszej podtablicy
 j = 0 // Początkowy indeks drugiej podtablicy
 k = lewy // Początkowy indeks scalanej podtablicy
 DOPÓKI i < rozmiar_lewej ORAZ j < rozmiar_prawej:
 JEŻELI L[i] <= R[j]:
 tablica[k] = L[i]
 i = i + 1
 W PRZECIWNYM RAZIE:
 tablica[k] = R[j]
 j = j + 1
 k = k + 1
 // Skopiuj pozostałe elementy L[], jeśli jakieś są
 DOPÓKI i < rozmiar_lewej:
 tablica[k] = L[i]
 i = i + 1
 k = k + 1
 // Skopiuj pozostałe elementy R[], jeśli jakieś są
 DOPÓKI j < rozmiar_prawej:
 tablica[k] = R[j]
 j = j + 1
 k = k + 1

Najczęstsze Błędy i Pułapki przy Implementacji Merge Sort

Zrozumienie potencjalnych problemów jest kluczowe dla efektywnej pracy z Merge Sortem:

  1. Złożoność pamięciowa: Częstym błędem jest niedocenianie wymagań pamięciowych Merge Sortu. Dodatkowa pamięć O(n) może być problemem w systemach z ograniczonymi zasobami. Przy bardzo dużych zbiorach danych, które nie mieszczą się w pamięci RAM, inne algorytmy mogą być preferowane.
  2. Implementacja funkcji scalającej: Błędy indeksowania (tzw. off-by-one errors) są bardzo powszechne podczas implementacji funkcji Merge. Należy dokładnie śledzić granice podtablic i indeksy, aby upewnić się, że wszystkie elementy są poprawnie przenoszone i żadne nie są pomijane.
  3. Warunek bazowy rekurencji: Zapomnienie o warunku bazowym (JEŻELI lewy < prawy) lub jego nieprawidłowe zdefiniowanie może prowadzić do nieskończonej rekurencji i przepełnienia stosu.
  4. Stabilność algorytmu: Sortowanie przez scalanie jest algorytmem stabilnym. Oznacza to, że zachowuje względną kolejność elementów o równych wartościach. Niewłaściwe zrozumienie tej cechy może prowadzić do błędnych założeń w aplikacjach, gdzie kolejność równych elementów ma znaczenie.

Jak Symulacja Merge Sort Pomaga w Zrozumieniu?

Interaktywna symulacja sortowania przez scalanie znacznie ułatwia zrozumienie jego mechaniki. Użytkownik może zaobserwować:

  1. Drzewo podziału i scalania: Symulacja często przedstawia proces dzielenia tablicy na mniejsze podtablice jako drzewo. Pomaga to zrozumieć rekurencyjną naturę algorytmu i zasadę „dziel i zwyciężaj”.
  2. Przepływ danych podczas scalania: Można śledzić, jak elementy są porównywane i przenoszone z dwóch podtablic do tymczasowej tablicy pomocniczej. Wizualizacja wskaźników (indeksów) pomaga zrozumieć logikę funkcji Merge.
  3. Złożoność czasowa w praktyce: Chociaż symulacja nie mierzy dokładnego czasu wykonania, można zaobserwować, że liczba porównań i operacji przenoszenia rośnie w sposób przewidywalny, ilustrując koncepcję O(n log n).
  4. Wpływ danych wejściowych: W przeciwieństwie do Quick Sortu, Merge Sort zachowuje swoją wydajność niezależnie od tego, czy dane wejściowe są już posortowane, posortowane odwrotnie, czy losowe. Symulacja może potwierdzić tę właściwość, pokazując, że zawsze będzie to O(n log n).
  5. Wymagania pamięciowe: Symulacja może wizualizować użycie dodatkowej pamięci przez tymczasowe tablice, co pomaga zrozumieć, dlaczego złożoność pamięciowa wynosi O(n).

Poprzez interakcję z symulacją, studenci mogą nie tylko zobaczyć, jak działa sortowanie przez scalanie, ale także eksperymentować z różnymi danymi i obserwować konsekwencje, co prowadzi do głębszego zrozumienia zarówno samego algorytmu, jak i ogólnych zasad projektowania algorytmów.

FAQ: Sortowanie przez Scalanie – Pytania i Odpowiedzi

Czym jest Sortowanie przez Scalanie (Merge Sort) i do czego służy?

Sortowanie przez scalanie to algorytm sortujący, który porządkuje elementy listy poprzez rekurencyjne dzielenie jej na mniejsze podlisty, sortowanie ich, a następnie łączenie w jedną posortowaną listę. Służy do efektywnego i przewidywalnego sortowania danych w informatyce, niezależnie od początkowego ułożenia elementów.

Jakie są główne zalety i wady Sortowania przez Scalanie?

Główną zaletą Merge Sortu jest gwarantowana złożoność czasowa O(n log n) we wszystkich przypadkach (najlepszym, średnim, najgorszym) oraz stabilność algorytmu. Wadą jest natomiast złożoność pamięciowa O(n), co oznacza, że wymaga dodatkowej pamięci proporcjonalnej do rozmiaru wejściowej tablicy.

Czy Sortowanie przez Scalanie jest algorytmem stabilnym? Co to oznacza?

Tak, Sortowanie przez Scalanie jest algorytmem stabilnym. Oznacza to, że zachowuje względną kolejność elementów, które mają równe wartości. Na przykład, jeśli w oryginalnej liście są dwa elementy 'A' z tą samą wartością, Merge Sort zapewni, że ich kolejność względem siebie nie zostanie zmieniona po posortowaniu.

Kiedy stosuje się Sortowanie przez Scalanie w praktyce?

Merge Sort jest często stosowany, gdy potrzebna jest gwarantowana wydajność (O(n log n)) oraz stabilność sortowania. Jest również preferowany przy sortowaniu list połączonych (linked lists) lub dużych zbiorów danych, które nie mieszczą się w pamięci RAM, gdzie implementuje się jego wersje zewnętrzne. Znajduje zastosowanie w przetwarzaniu danych, algorytmach zewnętrznego sortowania oraz jako podstawa dla innych algorytmów, np. algorytmu sortowania zewnętrznego. Więcej o algorytmach sortowania znajdziesz na Wikipedii.

Sprawdź, co już wiesz

Test · 20 pytań · pierwsze 3 za darmo

Pytanie 1 z 30%

W pseudokodzie algorytmu Merge Sort, procedura `MergeSort` wywołuje samą siebie rekurencyjnie.

Przećwicz to na fiszkach

24 fiszek · pierwsze 3 za darmo, bez konta

1 / 24

Czym jest sortowanie przez scalanie (Merge Sort) i do jakiej kategorii algorytmów należy?

Sortowanie przez scalanie to jeden z najbardziej efektywnych algorytmów sortowania, należący do algorytmów porównawczych.

Dotknij, aby odwrócić · Przesuń, aby nawigować

Przećwicz to, nie tylko przeczytaj

24 fiszek, test z wyjaśnieniami i podcast do tego tematu. Konto jest darmowe i zaczynasz tam, gdzie skończyłeś.

  • Bez karty płatniczej
  • Strona i aplikacja mobilna
  • 330 000+ uczniów
Zacznij za darmo

Powiązane tematy