Quicksort, sortowanie szybkie – algorytm sortowania działający w średnim przypadku w czasie liniowo-logarytmicznym. Algorytm jest oparty na metodzie dziel i zwyciężaj. Nie jest to algorytm stabilny ani wykazujący zachowanie naturalne, jednak ze względu na efektywność jest algorytmem bardzo popularnym.
Quicksort jest algorytmem rekurencyjnym, jego działanie można opisać następująco:
Wybór klucza osiowego może być dowolny, jednak powinien być jak najszybszy. W praktyce często stosuje się po prostu wybór pierwszego elementu.
Operacja przerzucania elementów powinna musi być zaimplementowana tak, aby wykonywała się w czasie liniowym. Przykładowo, może ona być zdefiniowana w następujący sposób:
Jak już wspomniano, operacja przenoszenia elementów odbywa się w czasie liniowym. O złożoności algorytmu decyduje więc to, ile nastąpi rekurencyjnych wywołań funkcji. W przypadku optymistycznym tablica będzie dzielona na pół. Wówczas głębokość drzewa wywołań zależy logarytmicznie od liczby danych wejściowych, czyli optymistyczna złożoność czasowa algorytmu jest rzędu O(n logn). Możemy wykazać, że taki sam rząd złożoności wystąpi w przypadku średnim (dowód jest dostępny w książkach podanych w bibliografii).
Przypadek pesymistyczny występuje wtedy, gdy klucz osiowy za każdym razem znajdzie się na brzegu tabeli. Wówczas przy każdym kolejnym wywołaniu liczba elementów do posortowania jest tylko o 1 mniejsza, zatem funkcja zostanie wywołana n razy. Pesymistyczna złożoność czasowa wynosi zatem O(n2).
Przeanalizujmy złożoność pamięciową algorytmu. Sortowanie szybkie nie potrzebuje co prawda dodatkowej pamięci na przechowywanie sortowanych danych, wymaga jednak pamięci potrzebnej na obsługę rekurencyjnych wywołań funkcji. Rozmiar tej pamięci (podobnie jak czas wykonania) będzie zależał od głębokości drzewa wywołań. Złożoność pamięciowa wynosi więc O(logn) w przypadku średnim i O(n) w przypadku pesymistycznym. Nie jest to zatem algorytm sortujący w miejscu.
Zastanówmy się, kiedy może wystąpić przypadek pesymistyczny. Jeśli jako klucz osiowy obieramy pierwszy (lub ostatni) elementu z tablicy, to najgorszy przypadek wystąpi wtedy, gdy tablica jest już posortowana. Widzimy więc, że algorytm zachowuje się w sposób bardzo nienaturalny.
Niedoskonałości algorytmu można nieco poprawić stosując następujące techniki:
Przykładowa implementacja algorytmu w języku C wygląda następująco:
void quicksort(int* tab, int poczatek, int n) { if (n > 1) { int liczba_mniejszych = 0; int liczba_wiekszych = 0; int koniec = poczatek + n; int t; // Zmienna tymczasowa // Przeniesienie elementow wokol klucza osiowego for (int i = (poczatek + 1); i < koniec; ++i) { if (tab[i] < tab[poczatek]) { if (liczba_wiekszych > 0) { // Przeniesienie elementu mniejszego od klucza osiowego // przed elementy wieksze od klucza osiowego t = tab[poczatek+liczba_mniejszych+1]; tab[poczatek+liczba_mniejszych+1] = tab[i]; tab[i] = t; } ++liczba_mniejszych; } else { ++liczba_wiekszych; } } // Wstawienie klucza osiowego na wlasciwe miejsce t = tab[poczatek+liczba_mniejszych]; tab[poczatek+liczba_mniejszych] = tab[poczatek]; tab[poczatek] = t; // Wywołanie rekurencyjne quicksort(tab, poczatek, liczba_mniejszych); quicksort(tab, koniec-liczba_wiekszych, liczba_wiekszych); } } // Przykladowe uzycie funkcji int main() { int tab[] = {3, 7, 5, 1, 5, 8, 4, 2}; quicksort(tab, 0, 8); return 0; }
Dodano: 5 stycznia 2018 18:56, ostatnia edycja: 13 lutego 2019 16:40.
Stos (ang. Stack) – struktura danych, w której bezpośredni dostęp jest tylko do ostatnio dodanego elementu. Stos bywa określany także jako kolejka LIFO (z ang. Last In, First Out, czyli: ostatni na wejściu, pierwszy na wyjściu). Stos można sobie wyobrazić jako kilka rzeczy ułożonych „jedna na drugiej” – łatwo można wziąć tylko rzecz leżącą na samym wierzchu, gdyż pozostałe są przykryte.
Ten artykuł opisuje algorytm zachłanny rozwiązujący problem wydawania reszty. Algorytm ten polega na wybieraniu zawsze największej dostępnej monety, tzn. takiej, która nie jest większa od kwoty pozostałej do wydania.
Algorytm nie zawsze znajduje rozwiązanie optymalne. Przykładowo, dla zbioru nominałów {1, 3, 4} i kwoty 6 algorytm użyje najpierw monety o nominale 4 (pozostaje do wydania kwota 2), potem monety o nominale 1 (pozostaje kwota 1) i jeszcze raz monety o nominale 1. Łącznie algorytm użyje więc trzech monet, podczas gdy rozwiązanie optymalne wymaga użycia tylko dwóch (dwie monety o nominale 3).
Rekurencja (inaczej rekursja) – odwołanie się funkcji lub definicji do samej siebie. Mówiąc inaczej, podejście rekurencyjne polega na tym, że rozwiązanie problemu wyraża się za pomocą rozwiązania tego samego problemu dla mniejszych danych wejściowych. Stosowanie rekurencji jest charakterystyczne dla algorytmów projektowanych metodą dziel i zwyciężaj.
Typowym problemem, dla którego można zastosować rekurencję, jest obliczanie silni. Przypomnijmy, że silnia z n jest zdefiniowana jako n!=1×2×…×n. Funkcja ta może być równoważnie zapisana jako:
n!=(n−1)!×n, dla n>0,
n!=1, dla n=0.
W powyższym przykładzie górny wiersz jest ogólnym równaniem rekurencji, zaś dolny wiersz jest wartością brzegową. W języku C++ powyższa funkcja byłaby zapisana w poniższy sposób.
int silnia(int n) { if (n > 0) { return n * silnia(n-1); } else { return 1; } };
Przekształcenie postaci rekurencyjnej funkcji do postaci zwartej (tzn. takiej, która nie zawiera odwołania do samej siebie) jest określane jako rozwiązanie rekurencji. Metody rozwiązywania rekurencji są dostępne między innymi w książkach podanych w bibliografii.
Algorytmy stosujące rekurencję są zazwyczaj proste w implementacji. Jednocześnie wiążą się one z pewnymi problemami. Przy podejściu rekurencyjnym ta sama funkcja jest wywoływana wielokrotnie, co zużywa pamięć operacyjną (w skrajnych przypadkach może to spowodować przepełnienie stosu).