|
Serwis Edukacyjny w I-LO w Tarnowie
Materiały dla uczniów liceum |
Wyjście Spis treści Wstecz Dalej
Autor artykułu: mgr Jerzy Wałaszek |
©2026 mgr Jerzy Wałaszek
|
Sortowanie danych (ang. data sort) jest jednym z podstawowych problemów programowania komputerów, z którym prędzej czy później spotka się każdy programista. Poniżej przedstawiono kilka zastosowań sortowania danych:
Zainteresowanych głębiej problemami sortowania odsyłam do artykułu o algorytmach sortujących.
Zaczniemy od jednego z najprostszych algorytmów sortujących, zwanego Sortowaniem Bąbelkowym (ang. buble sort). Zasada działania jest następująca:
Mamy zbiór n liczb. Wykonujemy n - 1 obiegów. W każdym obiegu przeglądamy kolejne pary elementów. W każdej parze porównujemy ze sobą element pierwszy z drugim. Jeśli są w złej kolejności ze względu na porządek docelowy, to elementy zamieniamy miejscami.
| Obieg 1 | ||
| 8 2 6 3 1 | 2 8 6 3 1 | Elementy w złej kolejności, zamieniamy je miejscami. |
| 2 8 6 3 1 | 2 6 8 3 1 | Elementy w złej kolejności, zamieniamy je miejscami. |
| 2 6 8 3 1 | 2 6 3 8 1 | Elementy w złej kolejności, zamieniamy je miejscami. |
| 2 6 3 8 1 | 2 6 3 1 8 | Elementy w złej kolejności, zamieniamy je miejscami. |
| Obieg 2 | ||
| 2 6 3 1 8 | 2 6 3 1 8 | Elementy w dobrej kolejności. |
| 2 6 3 1 8 | 2 3 6 1 8 | Elementy w złej kolejności, zamieniamy je miejscami. |
| 2 3 6 1 8 | 2 3 1 6 8 | Elementy w złej kolejności, zamieniamy je miejscami. |
| 2 3 1 6 8 | 2 3 1 6 8 | Elementy w dobrej kolejności. |
| Obieg 3 | ||
| 2 3 1 6 8 | 2 3 1 6 8 | Elementy w dobrej kolejności. |
| 2 3 1 6 8 | 2 1 3 6 8 | Elementy w złej kolejności, zamieniamy je miejscami. |
| 2 1 3 6 8 | 2 1 3 6 8 | Elementy w dobrej kolejności. |
| 2 1 3 6 8 | 2 1 3 6 8 | Elementy w dobrej kolejności. |
| Obieg 4 | ||
| 2 1 3 6 8 | 1 2 3 6 8 | Elementy w złej kolejności, zamieniamy je miejscami. |
| 1 2 3 6 8 | 1 2 3 6 8 | Elementy w dobrej kolejności. |
| 1 2 3 6 8 | 1 2 3 6 8 | Elementy w dobrej kolejności. |
| 1 2 3 6 8 | 1 2 3 6 8 | Elementy w dobrej kolejności. |
{ 8 2 6 3 1 } → { 1 2 3 6 8 }
Zwróć uwagę na kilka rzeczy. W każdym obiegu elementy kolejno największe są przesuwane w kierunku końca zbioru. Element najmniejszy jest natomiast przesuwany o jedną pozycję w kierunku początku zbioru. Zatem jeśli element najmniejszy był na końcu zbioru na pozycji n - 1, to aby trafił na pozycję 0, potrzeba n - 1 przesunięć, dlatego wymagane jest n - 1 obiegów.
W algorytmie będą dwie pętle: pętla zewnętrzna zliczająca obiegi oraz zagnieżdżona pętla wewnętrzna przeglądająca pary kolejnych elementów. Jeśli zbiór ma n elementów, to tworzą one n - 1 kolejnych par:
Dlatego pętla wewnętrzna też będzie wykonywać n - 1 obiegów przy każdym obiegu pętli zewnętrznej. Otrzymamy zatem obiegów:
Przy dużych n możemy pominąć -2n i 1, otrzymamy w przybliżeniu n2 obiegów, zatem algorytm będzie miał kwadratową klasę czasowej złożoności obliczeniowej O(n2).
| Wejście: | ||
| n | – | liczba elementów tablicy |
| T | – | tablica n-elementowa do posortowania |
| Wyjście: | ||
| T | – | posortowana rosnąco tablica n-elementowa |
Poniższy program tworzy tablicę 100 elementową i wypełnia ją liczbami pseudolosowymi od 0 do 999. Wyświetla tablicę nieposortowaną, następnie sortuje ją algorytmem bąbelkowym i wyświetla wynik.
Przedstawiony algorytm jest bardzo prymitywny i jego jedyną chyba zaletą jest to, iż działa. Postarajmy się go ulepszyć.
Zauważ, iż w pierwszym obiegu pętli wewnętrznej liczba największa w zbiorze zostaje zawsze przesunięta na koniec zbioru, a zatem trafia na swoje miejsce docelowe. W drugim obiegu nie ma sensu jej już porównywać z liczbą poprzednią. Zatem możemy założyć, iż po pierwszym obiegu, element końcowy jest posortowany. Dlatego w drugim obiegu można zbiór zmniejszyć o ten element i sortować n - 1 liczb. W drugim obiegu znów liczba największa w tym zredukowanym zbiorze trafi na koniec i będziemy mieli już dwa największe elementy posortowane. Procedurę tę możemy kontynuować i po każdym obiegu pętli zewnętrznej zmniejszać o 1 liczbę elementów do posortowania, "wyrzucając" ostatni element. W ten sposób algorytm będzie działał szybciej, gdyż będzie systematycznie malała liczba elementów do posortowania.
Zobaczmy to na przykładzie sortowania zbioru { 8 2 6 3 1 }:
| Obieg 1: zbiór 5-elementowy | ||
| 8 2 6 3 1 | 2 8 6 3 1 | Elementy w złej kolejności, zamieniamy je miejscami. |
| 2 8 6 3 1 | 2 6 8 3 1 | Elementy w złej kolejności, zamieniamy je miejscami. |
| 2 6 8 3 1 | 2 6 3 8 1 | Elementy w złej kolejności, zamieniamy je miejscami. |
| 2 6 3 8 1 | 2 6 3 1 8 | Elementy w złej kolejności, zamieniamy je miejscami. |
| Obieg 2: zbiór 4-elementowy | ||
| 2 6 3 1 : 8 | 2 6 3 1 : 8 | Elementy w dobrej kolejności. |
| 2 6 3 1 : 8 | 2 3 6 1 : 8 | Elementy w złej kolejności, zamieniamy je miejscami. |
| 2 3 6 1 : 8 | 2 3 1 6 : 8 | Elementy w złej kolejności, zamieniamy je miejscami. |
| Obieg 3: zbiór 3-elementowy | ||
| 2 3 1 : 6 8 | 2 3 1 : 6 8 | Elementy w dobrej kolejności. |
| 2 3 1 : 6 8 | 2 1 3 : 6 8 | Elementy w złej kolejności, zamieniamy je miejscami. |
| Obieg 4: zbiór 2-elementowy | ||
| 2 1 : 3 6 8 | 1 2 : 3 6 8 | Elementy w złej kolejności, zamieniamy je miejscami. |
{ 8 2 6 3 1 } → { 1 2 3 6 8 }
Zauważ rzecz drugą: Jeśli zbiór jest posortowany, to nie wystąpi żadna zamiana miejsc elementów. Zatem musimy sprawdzać po wykonaniu obiegu zewnętrznego, czy wystąpiła zamiana miejsc elementów. Jeśli nie, to kończymy sortowanie. Jak to zrealizować w algorytmie? Bardzo prosto. Tworzymy zmienną logiczną, np. o nazwie ok. Na początku każdego obiegu pętli zewnętrznej nadajemy jej wartość true. W pętli wewnętrznej przy zamianie elementów zmieniamy wartość tej zmiennej na false. Po każdym obiegu zewnętrznym sprawdzamy, czy ok ma wartość true. Jeśli tak, to nie było przestawienia elementów, czyli zbiór jest posortowany i możemy zakończyć algorytm.
| Wejście: | ||
| n | – | liczba elementów tablicy |
| T | – | tablica n-elementowa do posortowania |
| Wyjście: | ||
| T | – | posortowana rosnąco tablica n-elementowa |
W poprzednim programie wymień funkcję bublesort( ) na poniższą:
Sortowanie Bąbelkowe uważane jest za zły algorytm sortujący i w praktyce można go stosować do niedużych zbiorów (do około 5000 elementów). Klasa czasowej złożoności obliczeniowej Sortowania Bąbelkowego jest równa O(n2). Przy sortowaniu większych zbiorów należy wybrać lepszy algorytm.
Przy sortowaniu algorytm nie wymaga dodatkowego miejsca w pamięci komputera, tylko obszar dla n sortowanych elementów, zatem klasa pamięciowej złożoności obliczeniowej jest równa O(1).
Algorytm wykonuje sortowanie bezpośrednio na sortowanym zbiorze, taką własność nazywamy sortowaniem w miejscu (ang. in-place sorting).
Jeśli zbiór zawiera elementy równe, to po sortowaniu elementy te nie zmieniają kolejności względem siebie. Taka własność nosi nazwę stabilności (ang. stability). Przy sortowaniu zwykłych liczb nie ma to znaczenia. Jeśli jednak sortowane elementy są jakimiś obiektami, to sortowanie odbywa się wg klucza (np. numeru obiektu) i czasem kolejność elementów o równych kluczach jest istotna.
| Własności Sortowania Bąbelkowego | |
| Nazwa angielska | Buble Sort |
| Klasa złożoności czasowej | O(n2) |
| Klasa złożoności pamięciowej | O(1) |
| Sortowanie w miejscu | Tak |
| Stabilność | Tak |
Zadanie:
Zmodyfikuj algorytm 2 tak, aby sortował zbiór malejąco.
Załóżmy, iż chcemy posortować zbiór rosnąco. Szukamy w zbiorze elementu najmniejszego. Gdy element ten znajdziemy, wymieniamy go z pierwszym elementem. Element najmniejszy znajdzie się na swojej docelowej pozycji. Redukujemy zbiór tak, aby pierwszy element do niego już nie należał. W tak zredukowanym zbiorze ponownie wyszukujemy kolejny element najmniejszy i wymieniamy go z elementem na początku zbioru. Procedurę kontynuujemy, aż zbiór zredukuje się do 1 elementu. Elementy usunięte będą tworzyły zbiór uporządkowany.
Zobaczmy na prostym przykładzie działanie tego algorytmu. Posortujemy zbór: { 5 4 6 3 1 2 }
| Przed | Po | Opis |
| 5 4 6 3 1 2 | 1 4 6 3 5 2 | Wymieniamy 1 z 5 |
| 1 4 6 3 5 2 | 1 2 6 3 5 4 | Wymieniamy 2 z 4 |
| 1 2 6 3 5 4 | 1 2 3 6 5 4 | Wymieniamy 3 z 6 |
| 1 2 3 6 5 4 | 1 2 3 4 5 6 | Wymieniamy 4 z 6 |
| 1 2 3 4 5 6 | 1 2 3 4 5 6 | Wymieniamy 5 z 5 |
| 1 2 3 4 5 6 | 1 2 3 4 5 6 | Jeden element – koniec |
| Wejście: | ||
| n | – | liczba elementów tablicy |
| T | – | tablica n-elementowa do posortowania |
| Wyjście: | ||
| T | – | posortowana rosnąco tablica n-elementowa |
Poniższy program jest programem z poprzedniego rozdziału, w którym sortowanie jest wykonywane nowym algorytmem.
Sortowanie przez Wybór ogólnie jest szybsze od przedstawionego w poprzednim rozdziale Sortowania Bąbelkowego. W algorytmie mamy dwie pętle: zewnętrzną i zagnieżdżoną. Klasa czasowej złożoności obliczeniowej jest kwadratowa O(n2). Ponieważ algorytm wymienia kolejne elementy z początku zbioru ze znalezionymi elementami najmniejszymi, elementy równe nie zachowują pozycji względem siebie, zatem algorytm nie jest stabilny. Elementy są sortowane w tym samym zbiorze, zatem sortowanie odbywa się w miejscu i algorytm nie potrzebuje dodatkowej pamięci, dlatego jego klasa pamięciowej złożoności obliczeniowej jest równa O(1).
| Własności Sortowania przez Wybór | |
| Nazwa angielska | Selection Sort |
| Klasa złożoności czasowej | O(n2) |
| Klasa złożoności pamięciowej | O(1) |
| Sortowanie w miejscu | Tak |
| Stabilność | Nie |
Zadanie:
Zmodyfikuj algorytm tak, aby sortował zbiór malejąco.
Algorytm Sortowania przez Wstawianie można porównać do sposobu układania w ręce kart pobieranych z talii. Najpierw bierzemy pierwszą kartę. Następnie pobieramy kolejne, aż do wyczerpania talii. Każdą pobraną kartę porównujemy z kartami, które już trzymamy w ręce i szukamy dla niej miejsca przed pierwszą kartą starszą (młodszą w przypadku porządku malejącego). Gdy znajdziemy takie miejsce, rozsuwamy karty i nową wstawiamy na przygotowane w ten sposób miejsce (stąd pochodzi nazwa algorytmu - Sortowanie przez Wstawianie, ang. Insertion Sort). Jeśli nasza karta jest najstarsza (najmłodsza), to umieszczamy ją na samym końcu. Tak porządkujemy karty. A jak przenieść tę ideę do świata komputerów i zbiorów liczbowych?
Algorytm Sortowania przez Wstawianie będzie składał się z dwóch pętli. Pętla główna (zewnętrzna) symuluje pobieranie kart, czyli w tym wypadku elementów zbioru. Odpowiednikiem kart na ręce jest tzw. lista uporządkowana (ang. sorted list), którą sukcesywnie będziemy tworzyli na końcu zbioru (istnieje też odmiana algorytmu umieszczająca listę uporządkowaną na początku zbioru). Pętla sortująca (wewnętrzna) szuka dla pobranego elementu miejsca na liście uporządkowanej. Jeśli takie miejsce zostanie znalezione, to elementy listy są odpowiednio rozsuwane, aby tworzyć miejsce na nowy element i element wybrany przez pętlę główną trafia tam. W ten sposób lista uporządkowana rozrasta się. Jeśli na liście uporządkowanej nie ma elementu większego od wybranego, to element ten trafia na koniec listy. Sortowanie zakończymy, gdy pętla główna wybierze wszystkie elementy zbioru.
Najważniejszą operacją w opisywanym algorytmie sortowania jest wstawianie wybranego elementu na listę uporządkowaną. Zasady są następujące:
Przykład:
Mamy zbiór { 7 1 3 5 9 }. Załóżmy, iż lista uporządkowana obejmuje cztery ostatnie elementy 1 2 5 i 9. Pierwszy element 7 wstawimy na tą listę tak, aby była wciąż uporządkowana:
| Przed | Po | Opis |
7 1 3 5 9 |
7 1 3 5 9 |
Wybieramy ze zbioru element do wstawienia na listę. Jego miejsce w zbiorze staje się puste. |
7 <<<1 3 5 9 |
7 1 3 5 9 |
Porównujemy wybrany element z kolejnymi elementami listy. Jeśli element listy jest mniejszy, to przesuwamy go na puste miejsce. 1 jest mniejsze od 7, zatem wędruje na puste miejsce. Puste miejsce przesuwa się tam, gdzie była jedynka. |
7 1 <<<3 5 9 |
7 1 3 5 9 |
Porównujemy 7 z 3. 3 jest mniejsze, przesuwa się na puste
miejsce, a puste miejsce przesuwa się tam, gdzie było 3. |
7 1 3 <<<5 9 |
7 1 3 5 9 |
Porównujemy 7 z 5. 5 jest mniejsze. Przesuwa się na puste miejsce. |
<<<7 1 3 5 9 |
1 3 5 7 9 |
Porównujemy 7 z 9. 9 nie jest mniejsze, na puste miejsce
wstawiamy 7. Lista uporządkowana wzrosła o dodany element. |
| Wejście: | ||
| n | – | liczba elementów tablicy |
| T | – | tablica n-elementowa do posortowania |
| Wyjście: | ||
| T | – | posortowana rosnąco tablica n-elementowa |
Poniższy program jest programem z poprzedniego rozdziału, w którym sortowanie jest wykonywane nowym algorytmem.
| Własności Sortowania przez Wstawianie | |
| Nazwa angielska | Insertion Sort |
| Klasa złożoności czasowej | O(n2) |
| Klasa złożoności pamięciowej | O(1) |
| Sortowanie w miejscu | Tak |
| Stabilność | Tak |
Poczynając od tego rozdziału przechodzimy do opisu algorytmów szybkich, tzn. takich, które posiadają klasę czasowej złożoności obliczeniowej równą O(n·log n) lub nawet lepszą.
W informatyce zwykle obowiązuje zasada, iż prosty algorytm posiada dużą złożoność obliczeniową, natomiast algorytm zaawansowany posiada małą złożoność obliczeniową, ponieważ wykorzystuje on pewne własności, dzięki którym szybciej dochodzi do rozwiązania.
Wiele dobrych algorytmów sortujących korzysta z rekurencji, która powstaje wtedy, gdy do rozwiązania problemu algorytm wykorzystuje samego siebie ze zmienionym zestawem danych.
Wynaleziony w 1945 roku przez Johna von Neumanna algorytm Sortowania przez Scalanie (ang. Merge Sort) jest algorytmem rekurencyjnym. Wykorzystuje zasadę dziel i zwyciężaj, która polega na podziale zadania głównego na zadania mniejsze dotąd, aż rozwiązanie stanie się oczywiste. Algorytm sortujący dzieli porządkowany zbiór na kolejne połowy dopóki taki podział jest możliwy (tzn. podzbiór zawiera co najmniej dwa elementy). Następnie uzyskane w ten sposób części zbioru rekurencyjnie sortuje tym samym algorytmem. Posortowane części łączy ze sobą za pomocą scalania, tak aby wynikowy zbiór był posortowany.
Podstawową operacją algorytmu jest scalanie dwóch zbiorów uporządkowanych w jeden zbiór również uporządkowany. Operację scalania realizujemy wykorzystując pomocniczy zbiór, w którym będziemy tymczasowo odkładać scalane elementy dwóch zbiorów. Ogólna zasada jest następująca:
Przygotuj pusty zbiór tymczasowy
Dopóki żaden ze scalanych zbiorów nie jest pusty, porównuj ze sobą pierwsze elementy każdego z nich i w zbiorze tymczasowym umieszczaj mniejszy z elementów usuwając go jednocześnie ze scalanego zbioru.
W zbiorze tymczasowym umieść zawartość tego scalanego zbioru, który zawiera jeszcze elementy.
Zawartość zbioru tymczasowego przepisz do zbioru wynikowego i zakończ algorytm.
Przykład:
Połączmy za pomocą opisanego algorytmu dwa uporządkowane zbiory: { 1 3 6 7 9 } z { 2 3 4 6 8 } :
| Scalane zbiory | Zbiór tymczasowy | Opis wykonywanych działań |
[1] 3 6 7 9
2 3 4 6 8
|
|
Porównujemy ze sobą najmniejsze elementy scalanych
zbiorów. Ponieważ zbiory te są już uporządkowane, to najmniejszymi elementami będą zawsze ich pierwsze elementy. |
3 6 7 9 2 3 4 6 8 |
1
|
W zbiorze tymczasowym umieszczamy mniejszy element, w tym przypadku będzie to liczba 1. Jednocześnie element ten zostaje usunięty z pierwszego zbioru |
3 6 7 9 [2] 3 4 6 8 |
1
|
Porównujemy dwa pierwsze elementy i mniejszy umieszczamy w zbiorze tymczasowym na kolejnej pozycji. |
[3] 6 7 9 3 4 6 8 |
1 2 |
Następne porównanie i w zbiorze tymczasowym umieszczamy
liczbę 3. Ponieważ są to elementy równe, to nie ma znaczenia, z którego zbioru weźmiemy element 3. |
6 7 9 [3] 4 6 8 |
1 2 3 |
Teraz do zbioru tymczasowego trafi drugie 3. |
6 7 9 [4] 6 8 |
1 2 3 3 |
W zbiorze tymczasowym umieszczamy mniejszy z porównywanych elementów, czyli liczbę 4. |
[6] 7 9 6 8 |
1 2 3 3 4 |
Porównywane elementy są równe, zatem w zbiorze tymczasowym umieszczamy dowolny z nich. |
7 9 [6] 8 |
1 2 3 3 4 6 |
Teraz drugą liczbę 6. |
[7] 9 8 |
1 2 3 3 4 6 6 |
W zbiorze tymczasowym umieszczamy liczbę 7 |
9 [8] |
1 2 3 3 4 6 6 7 |
Teraz 8 |
[9] |
1 2 3 3 4 6 6 7 8 |
Drugi zbiór jest pusty. Od tego momentu już nie
porównujemy, lecz wprowadzamy do zbioru tymczasowego wszystkie pozostałe elementy pierwszego zbioru, w tym przypadku będzie to liczba 9. |
|
|
1 2 3 3 4 6 6 7 8 9 |
Koniec scalania. Zbiór tymczasowy zawiera wszystkie
elementy scalanych zbiorów i jest uporządkowany. Możemy w dalszej kolejności przepisać jego zawartość do zbioru docelowego. |
Przed przystąpieniem do wyjaśniania sposobu łączenia dwóch zbiorów uporządkowanych w jeden zbiór również uporządkowany musimy zastanowić się nad sposobem reprezentacji danych. Przyjmijmy, iż elementy zbioru będą przechowywane w jednej tablicy, którą oznaczymy jako d[ ]. Każdy element w tej tablicy będzie posiadał swój numer, czyli indeks z zakresu od 0 do n-1.
Kolejnym zagadnieniem jest sposób reprezentacji scalanych zbiorów. W przypadku algorytmu Sortowania przez Scalanie zawsze będą to dwie przyległe połówki zbioru, który został przez ten algorytm podzielony. Co więcej, wynik scalenia ma być umieszczony z powrotem w tym samym zbiorze.
Przykład:
Prześledźmy prosty przykład. Mamy posortować zbiór o postaci: { 6 5 4 1 3 7 9 2 }
| Sortowany zbiór | Opis wykonywanych operacji | |||||||
| d[0] | d[1] | d[2] | d[3] | d[4] | d[5] | d[6] | d[7] | |
| 6 | 5 | 4 | 1 | 3 | 7 | 9 | 2 | Zbiór wyjściowy. |
| 6 | 5 | 4 | 1 | 3 | 7 | 9 | 2 | Pierwszy podział. |
| 6 | 5 | 4 | 1 | 3 | 7 | 9 | 2 | Drugi podział |
| 6 | 5 | 4 | 1 | 3 | 7 | 9 | 2 | Trzeci podział. |
| 5 | 6 | 1 | 4 | 3 | 7 | 2 | 9 | Pierwsze scalanie. |
|
1 |
4 | 5 | 6 | 2 | 3 | 7 | 9 | Drugie scalanie. |
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | Trzecie scalanie. Koniec. |
Ponieważ w opisywanym tutaj algorytmie sortującym scalane podzbiory są przyległymi do siebie częściami innego zbioru, zatem logiczne będzie użycie do ich definicji indeksów wybranych elementów tych podzbiorów:
| ip | - indeks pierwszego elementu w młodszym podzbiorze |
| is | - indeks pierwszego elementu w starszym podzbiorze |
| ik | - indeks ostatniego elementu w starszym podzbiorze |
Przez podzbiór młodszy rozumiemy podzbiór zawierający elementy o indeksach mniejszych niż indeksy elementów w podzbiorze starszym.
| pozostała część zbioru | ip | ... | is | ... | ik | pozostała część zbioru |
| młodszy podzbiór | starszy podzbiór | |||||
Indeks końcowego elementu młodszej połówki zbioru z łatwością wyliczamy - będzie on o 1 mniejszy od indeksu pierwszego elementu starszej połówki.
Przykład:
|
Młodsza połówka |
Starsza połówka |
| ip = 0 | is = 4 |
| ik = 7 | |
| Młodsza połówka | Starsza połówka | ||
|
Młodsza ćwiartka |
Starsza ćwiartka |
Młodsza ćwiartka |
Starsza ćwiartka |
| ip = 0 | is = 2 | ip = 4 | is = 6 |
| ik = 3 | ik = 7 | ||
Scalaj(d, ip, is, ik)
| d | - scalany zbiór |
| ip | - indeks pierwszego elementu w młodszym podzbiorze, ip ∈ N |
| is | - indeks pierwszego elementu w starszym podzbiorze, is ∈ N |
| ik | - indeks ostatniego elementu w starszym podzbiorze, ik ∈ N |
| d | - scalony zbiór |
| p | - zbiór pomocniczy, który zawiera tyle samo elementów, co zbiór d. |
| i1 | - indeks elementów w młodszej połówce zbioru d, i1 ∈ N |
| i2 | - indeks elementów w starszej połówce zbioru d, i2 ∈ N |
| i | - indeks elementów w zbiorze pomocniczym p, i ∈ N |
Operacja scalania dwóch podzbiorów wymaga dodatkowej pamięci o rozmiarze równym sumie rozmiarów scalanych podzbiorów. Dla prostoty na potrzeby naszego algorytmu zarezerwujemy tablicę p o rozmiarze równym rozmiarowi tablicy d. W tablicy p algorytm będzie tworzył zbiór tymczasowy, który po zakończeniu scalania zostanie przepisany do tablicy d w miejsce dwóch scalanych podzbiorów.
Parametrami wejściowymi do algorytmu są indeksy ip, is oraz ik, które jednoznacznie definiują położenie dwóch podzbiorów do scalenia w obrębie tablicy d. Elementy tych podzbiorów będą indeksowane za pomocą zmiennych i1 (młodszy podzbiór od pozycji ip do is - 1) oraz i2 (starszy podzbiór od pozycji is do ik). Na początku algorytmu przypisujemy tym zmiennym indeksy pierwszych elementów w każdym podzbiorze.
Zmienna i będzie zawierała indeksy elementów wstawianych do tablicy p. Dla ułatwienia indeksy te przebiegają wartości od ip do ik, co odpowiada obszarowi tablicy d zajętemu przez dwa scalane podzbiory. Na początku do zmiennej i wprowadzamy indeks pierwszego elementu w tym obszarze, czyli ip.
Wewnątrz pętli sprawdzamy, czy indeksy i1 i i2 wskazują elementy podzbiorów. Jeśli któryś z nich wyszedł poza dopuszczalny zakres, to dany podzbiór jest wyczerpany - w takim przypadku do tablicy p przepisujemy elementy drugiego podzbioru.
Jeśli żaden z podzbiorów nie jest wyczerpany, porównujemy kolejne elementy z tych podzbiorów wg indeksów i1 i i2. Do tablicy p zapisujemy zawsze mniejszy z porównywanych elementów. Zapewnia to uporządkowanie elementów w tworzonym zbiorze wynikowym. Po zapisie elementu w tablicy p, odpowiedni indeks i1 lub i2 jest zwiększany o 1. Zwiększany jest również indeks i, aby kolejny zapisywany element w tablicy p trafił na następne wolne miejsce. Pętla jest kontynuowana aż do zapełnienia w tablicy p obszaru o indeksach od ip do ik.
Wtedy przechodzimy do końcowej pętli, która przepisuje ten obszar z tablicy p do tablicy wynikowej d. Scalane zbiory zostają zapisane zbiorem wynikowym, który jest posortowany rosnąco.
Sortuj_przez_scalanie(d,ip, ik)
| d | - sortowany zbiór |
| ip | - indeks pierwszego elementu w młodszym podzbiorze, ip ∈ N |
| ik | - indeks ostatniego elementu w starszym podzbiorze, ik ∈ N |
| d | - posortowany zbiór |
| is | - indeks pierwszego elementu w starszym podzbiorze, is ∈ N |
Algorytm Sortowania przez Scalanie jest algorytmem rekurencyjnym. Wywołuje się go z zadanymi wartościami indeksów ip oraz ik. Przy pierwszym wywołaniu indeksy te powinny objąć całą tablicę d, zatem ip = 0, a ik = n-1.
Najpierw algorytm wyznacza indeks is, który wykorzystywany jest do podziału zbioru na dwie połówki:
Następnie sprawdzamy, czy dana połówka zbioru zawiera więcej niż jeden element. Jeśli tak, to rekurencyjnie sortujemy ją tym samym algorytmem.
Po posortowaniu obu połówek zbioru scalamy je za pomocą opisanej wcześniej procedury scalania podzbiorów uporządkowanych i kończymy algorytm. Zbiór jest posortowany.
Poniższy program jest programem z poprzedniego rozdziału, w którym sortowanie jest wykonywane nowym algorytmem.
| Własności Sortowania przez Scalanie | |
| Nazwa angielska | Merge Sort |
| Klasa złożoności czasowej | O(n·log n) |
| Klasa złożoności pamięciowej | O(n) |
| Sortowanie w miejscu | Nie |
| Stabilność | Tak |
Algorytm Sortowania Szybkiego opiera się na strategii "dziel i zwyciężaj" (ang. divide and conquer), którą możemy krótko scharakteryzować w trzech punktach:
Idea Sortowania Szybkiego jest następująca:
| DZIEL : | najpierw sortowany zbiór dzielimy na dwie części w taki sposób, aby wszystkie elementy leżące w pierwszej części (zwanej lewą partycją) były mniejsze lub równe od wszystkich elementów drugiej części zbioru (zwanej prawą partycją). |
| ZWYCIĘŻAJ : | każdą z partycji sortujemy rekurencyjnie tym samym algorytmem. |
| POŁĄCZ : | połączenie tych dwóch partycji w jeden zbiór daje w wyniku zbiór posortowany. |
prof. Tony Hoare |
Sortowanie Szybkie zostało wynalezione przez angielskiego informatyka, profesora Tony'ego Hoare'a w latach 60-tych ubiegłego wieku. W przypadku typowym algorytm ten jest najszybszym algorytmem sortującym z klasy złożoności obliczeniowej O(n·log n) - stąd pochodzi jego popularność w zastosowaniach. Musimy jednak pamiętać, iż w pewnych sytuacjach (zależnych od sposobu wyboru piwotu oraz niekorzystnego ułożenia danych wejściowych) klasa złożoności obliczeniowej tego algorytmu może się degradować do O(n2), co więcej, poziom wywołań rekurencyjnych może spowodować przepełnienie stosu i zablokowanie komputera. Z tych powodów algorytmu Sortowania Szybkiego nie można stosować bezmyślnie w każdej sytuacji tylko dlatego, iż jest uważany za jeden z najszybszych algorytmów sortujących - zawsze należy przeprowadzić analizę możliwych danych wejściowych właśnie pod kątem przypadku niekorzystnego.
Do utworzenia partycji musimy ze zbioru wybrać jeden z elementów, który nazwiemy piwotem. W lewej partycji znajdą się wszystkie elementy niewiększe od piwotu, a w prawej partycji umieścimy wszystkie elementy niemniejsze od piwotu. Położenie elementów równych nie wpływa na proces sortowania, zatem mogą one występować w obu partycjach. Również porządek elementów w każdej z partycji nie jest ustalony.
Jako piwot można wybierać element pierwszy, środkowy, ostatni, medianę lub losowy. Dla naszych potrzeb wybierzemy element środkowy:
| piwot ← d[(lewy + prawy) div 2] |
|
Dzielenie na partycje polega na umieszczeniu dwóch wskaźników na początku zbioru - i oraz j. Wskaźnik i przebiega przez zbiór poszukując wartości mniejszych od piwotu. Po znalezieniu takiej wartości jest ona wymieniana z elementem na pozycji j. Po tej operacji wskaźnik j jest przesuwany na następną pozycję. Wskaźnik j zapamiętuje pozycję, na którą trafi następny element oraz na końcu wskazuje miejsce, gdzie znajdzie się piwot. W trakcie podziału piwot jest bezpiecznie przechowywany na ostatniej pozycji w zbiorze.
Przykład:
Dla przykładu podzielimy na partycje zbiór: { 7 2 4 7 3 1 4 6 5 8 3 9 2 6 7 6 3 }
| Lp. | Operacja | Opis |
| 1. |
7 2 4 7 3 1 4 6 5 8 3 9 2 6 7 6 3
|
Wyznaczamy na piwot element środkowy. |
| 2. |
7 2 4 7 3 1 4 6 3 8 3 9 2 6 7 6 5 |
Piwot wymieniamy z ostatnim elementem zbioru |
| 3. |
7 2 4 7 3 1 4 6 3 8 3 9 2 6 7 6 5 i j |
Na początku zbioru ustawiamy dwa wskaźniki. Wskaźnik i będzie przeglądał zbiór do przedostatniej pozycji. Wskaźnik j zapamiętuje miejsce wstawiania elementów mniejszych od piwotu |
| 4. |
7 2 4 7 3 1 4 6 3 8 3 9 2 6 7 6 5 i j |
Wskaźnikiem i szukamy elementu mniejszego od piwotu |
| 5. |
2 7 4 7 3 1 4 6 3 8 3 9 2 6 7 6 5 i j |
Znaleziony element wymieniamy z elementem na
pozycji j-tej. Po wymianie wskaźnik j przesuwamy o 1 pozycję. |
| 6. |
2 7 4 7 3 1 4 6 3 8 3 9 2 6 7 6 5 i j |
Szukamy |
| 7. |
2 4 7 7 3 1 4 6 3 8 3 9 2 6 7 6 5 i j |
Wymieniamy i przesuwamy j. |
| 8. |
2 4 7 7 3 1 4 6 3 8 3 9 2 6 7 6 5 i j |
Szukamy |
| 9. |
2 4 3 7 7 1 4 6 3 8 3 9 2 6 7 6 5 i j |
Wymieniamy i przesuwamy j. |
| 10. |
2 4 3 7 7 1 4 6 3 8 3 9 2 6 7 6 5 i j |
Szukamy |
| 11. |
2 4 3 1 7 7 4 6 3 8 3 9 2 6 7 6 5 i j |
Wymieniamy i przesuwamy j. |
| 12. |
2 4 3 1 7 7 4 6 3 8 3 9 2 6 7 6 5 i j |
Szukamy |
| 13. |
2 4 3 1 4 7 7 6 3 8 3 9 2 6 7 6 5 i j |
Wymieniamy i przesuwamy j. |
| 14. |
2 4 3 1 4 7 7 6 3 8 3 9 2 6 7 6 5 i j |
Szukamy |
| 15. |
2 4 3 1 4 3 7 6 7 8 3 9 2 6 7 6 5 i j |
Wymieniamy i przesuwamy j. |
| 16. |
2 4 3 1 4 3 7 6 7 8 3 9 2 6 7 6 5 i j |
Szukamy |
| 17. |
2 4 3 1 4 3 3 6 7 8 7 9 2 6 7 6 5 i j |
Wymieniamy i przesuwamy j. |
| 18. |
2 4 3 1 4 3 3 6 7 8 7 9 2 6 7 6 5 i j |
Szukamy |
| 19. |
2 4 3 1 4 3 3 2 7 8 7 9 6 6 7 6 5 i j |
Wymieniamy i przesuwamy j. |
| 20. |
2 4 3 1 4 3 3 2 5 8 7 9 6 6 7 6 7 ^ i Lewa partycja j Prawa partycja |
Brak dalszych elementów do wymiany. Piwot wymieniamy z elementem na pozycji j-tej. Podział na partycje zakończony. |
Po zakończeniu podziału na partycje wskaźnik j wyznacza pozycję piwotu. Lewa partycja zawiera elementy mniejsze od piwotu i rozciąga się od początku zbioru do pozycji j - 1. Prawa partycja zawiera elementy większe lub równe piwotowi i rozciąga się od pozycji j + 1 do końca zbioru. Operacja podziału na partycje ma liniową klasę złożoności obliczeniowej - O(n).
| d | - Zbiór zawierający elementy do posortowania. |
| lewy | - indeks pierwszego elementu w zbiorze, lewy ∈ C |
| prawy | - indeks ostatniego elementu w zbiorze, prawy ∈ C |
| d | - Zbiór zawierający elementy posortowane rosnąco |
| piwot | - | element podziałowy |
| i, j | - | indeksy, i, j ∈ C |
Algorytm sortowania szybkiego wywołujemy podając za lewy indeks pierwszego elementu zbioru, a za prawy indeks elementu ostatniego (czyli Sortuj_szybko(d,0,n-1)). Zakres indeksów jest dowolny - dzięki temu ten sam algorytm może również sortować fragment zbioru, co wykorzystujemy przy sortowaniu wyliczonych partycji.
Na element podziałowy wybieramy element leżący w środku dzielonej partycji. Wyliczamy jego pozycję i zapamiętujemy ją tymczasowo w zmiennej i. Robimy to po to, aby dwukrotnie nie wykonywać tych samych rachunków.
Element d[i] zapamiętujemy w zmiennej piwot, a do d[i] zapisujemy ostatni element partycji. Dzięki tej operacji piwot został usunięty ze zbioru.
Ustawiamy zmienną j na początek partycji. Zmienna ta zapamiętuje pozycję podziału partycji.
W pętli sterowanej zmienną i przeglądamy kolejne elementy od pierwszego do przedostatniego (ostatni został umieszczony na pozycji piwotu, a piwot zapamiętany). Jeśli i-ty element jest mniejszy od piwotu, to trafia on na początek partycji - wymieniamy ze sobą elementy na pozycjach i-tej i j-tej. Po tej operacji przesuwamy punkt podziałowy partycji j.
Po zakończeniu pętli element z pozycji j-tej przenosimy na koniec partycji, aby zwolnić miejsce dla piwotu, po czym wstawiamy tam piwot. Zmienna j wskazuje zatem wynikową pozycję piwotu. Pierwotna partycja została podzielona na dwie partycje:
|
partycja lewa od pozycji
lewy do j - 1 zawiera elementy mniejsze od piwotu |
Sprawdzamy, czy partycje te obejmują więcej niż jeden element. Jeśli tak, to wywołujemy rekurencyjnie algorytm sortowania szybkiego przekazując mu granice wyznaczonych partycji. Po powrocie z wywołań rekurencyjnych partycja wyjściowa jest posortowana rosnąco. Kończymy algorytm.
Poniższy program jest programem z poprzedniego rozdziału, w którym sortowanie jest wykonywane nowym algorytmem.
| Własności Sortowania Szybkiego | |
| Nazwa angielska | Quick Sort |
| Klasa złożoności czasowej | O(n·log n) |
| Klasa złożoności pamięciowej | O(1) |
| Sortowanie w miejscu | Tak |
| Stabilność | Nie |
Przykład:
Dla przykładu posortujemy opisaną wyżej metodą zbiór { 2 6 4 3 8 7 2 5 7 9 3 5 2 6 }.
Najpierw określamy zakres wartości elementów (w tym celu możemy na przykład wyszukać w zbiorze element najmniejszy i największy). U nas zakres wynosi:
Potrzebujemy zatem:
Numer licznika określamy przy pomocy wzoru:
W ten sposób numery liczników będą zawsze rozpoczynały się od 0, co pozwoli umieścić je w kolejnych komórkach tablicy.
Liczniki ponumerujemy zgodnie z wartościami, które będą zliczały:
Na początku sortowania wszystkie liczniki mają stan zero:
Teraz przeglądamy kolejne elementy zbioru zliczając ich wystąpienia w odpowiednich licznikach (nn = w - wmin):
Przeglądamy kolejne liczniki począwszy od licznika o najmniejszym numerze (w przypadku sortowania malejącego przeglądanie rozpoczynamy od licznika o największym numerze, czyli od końca tablicy liczników) i zapisujemy do zbioru wynikowego tyle razy wartość w = nn + wmin, ile wynosi zawartość licznika:
Otrzymujemy zbiór posortowany.
Algorytm ma klasę złożoności obliczeniowej O(m + n), gdzie n oznacza liczbę elementów, a m oznacza liczbę ich wartości.
| T | - sortowana tablica liczb całkowitych. |
| n | - liczba elementów w tablicy T, n ∈ N |
| T | - posortowana tablica liczb całkowitych. |
Algorytm ma klasę czasowej złożoności obliczeniowej O(m + n), gdzie m oznacza ilość możliwych wartości, które mogą przyjmować elementy tablicy T, a n to ilość sortowanych elementów.
Poniższy program sortuje tablicę algorytmem sortowania kubełkowego. Przeanalizuj komentarze.
| Własności Sortowania Kubełkowego | |
| Nazwa angielska | Bucket Sort |
| Klasa złożoności czasowej | O(n+m) |
| Klasa złożoności pamięciowej | O(m) |
| Sortowanie w miejscu | Tak |
| Stabilność | Tak lub Nie |
Stabilność algorytmu kubełkowego zależna jest od implementacji (zobacz tutaj).
![]() |
Zespół Przedmiotowy Chemii-Fizyki-Informatyki w I Liceum Ogólnokształcącym im. Kazimierza Brodzińskiego w Tarnowie ul. Piłsudskiego 4 ©2026 mgr Jerzy Wałaszek |
Materiały tylko do użytku dydaktycznego. Ich kopiowanie i powielanie jest dozwolone pod warunkiem podania źródła oraz niepobierania za to pieniędzy.
Pytania proszę przesyłać na adres email:
Serwis wykorzystuje pliki cookies. Jeśli nie chcesz ich otrzymywać, zablokuj je w swojej przeglądarce.
Informacje dodatkowe.