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.
Graf – struktura składająca się ze zbioru wierzchołków oraz zbioru krawędzi. Grafy mają szerokie zastosowanie w informatyce, można za ich pomocą przedstawić wiele zagadnień.
Wyróżniamy grafy nieskierowane oraz grafy skierowane. W grafie nieskierowanym relacja sąsiedztwa jest symetryczna, tzn. krawędź łączy wierzchołki „w obie strony”. W grafie skierowanym krawędzie są „jednokierunkowe”. Krawędź grafu skierowanego zazwyczaj jest określana jako łuk.
Graf ważony (inaczej graf z wagami) to taki graf, w którym każdej krawędzi przypisana jest pewna wartość liczbowa. Wartość ta może oznaczać np. długość krawędzi lub jej przepustowość.
Algorytm Dijkstry – algorytm służący do wyznaczania najkrótszych ścieżek w grafie. Wyznacza najkrótsze ścieżki z jednego wierzchołka (zwanego wierzchołkiem źródłowym) do pozostałych wierzchołków. Algorytm wymaga, aby wagi krawędzi grafu nie były ujemne. Autorem algorytmu jest holenderski naukowiec Edsger Dijkstra.
Algorytm realizuje podejście zachłanne. W każdej iteracji wybierany jest ten spośród nieodwiedzonych wierzchołków, do którego można dotrzeć najmniejszym kosztem. Po wyznaczeniu ścieżki do konkretnego wierzchołka nie zostanie ona zmodyfikowana w trakcie wykonywania dalszej części algorytmu.
Notacja dużego O – notacja przedstawiająca asymptotyczne tempo wzrostu, wykorzystywana do zapisywania złożoności obliczeniowej algorytmu. Za pomocą tej notacji zapisywany jest rząd wielkości funkcji wyrażającej liczbę operacji dominujących (w przypadku złożoności czasowej) lub rozmiar wymaganej pamięci (w przypadku złożoności pamięciowej) w zależności od liczby danych wejściowych.
Wykorzystując notację dużego O nie podajemy dokładnego wzoru funkcji, a jedynie jej najbardziej znaczący składnik, w dodatku z pominięciem stałego współczynnika. Przykładowo, funkcję postaci f(n)=5n2+20n+100 możemy zapisać jako O(n2). Zakładamy bowiem, że dla dostatecznie dużych n wpływ pomijanych elementów jest znikomy. Choć oczywiście dla małych n może się zdarzyć, że funkcja o gorszej złożoności będzie się wykonywała szybciej.
Weźmy dla przykładu funkcje f(n) = 1000n+2000 i g(n) = n2. Choć pierwsza funkcja ma pozornie bardzo duże stałe współczynniki, to dla n ≥ 1002 będzie ona przyjmowała wartości mniejsze. Im większe n, tym ta różnica będzie wyraźniejsza. Dla n = 10000 (w przypadku danych przetwarzanych komputerowo nie jest to wielka wartość) f(n) = 10002000 (ok. 10 mln), a g(n) = 100000000 (100 mln), czyli blisko 10 razy więcej.
Możliwe jest również wykorzystanie notacji dużego O dla funkcji wielu zmiennych. Wówczas zapis może wyglądać tak: O(v2e). Znajduje to zastosowanie np. dla algorytmów operujących na grafach, gdzie złożoność zależy zarówno od liczby wierzchołków, jak i liczby krawędzi w grafie.