Podstawy programowania Przykłady algorytmów Cz. 2 Sortowanie
Złożoność obliczeniowa Złożoność obliczeniowa to ilość zasobów niezbędna do wykonania algorytmu. Wyróżnia się dwa rodzaje: Złożoność czasową (obliczeniową) Złożoność pamięciową Złożoność zależy od rozmiaru danych wejściowych (np. sortowanie, niezależnie od użytego algorytmu, będzie trwało tym dłużej, im większy jest zbiór sortowanych elementów)
Złożoność obliczeniowa Istnieje wiele miar złożoności obliczeniowej i wiele notacji. W większości miar najważniejsze jest asymptotyczne tempo wzrostu (tj. jak złożoność rośnie ze wzrostem rozmiaru danych). Do porównywania algorytmów najczęściej używa się notacji O (dużego O), która podaje asymptotyczne ograniczenie górne W notacji O uwzględnia się tylko funkcję dominującą i pomija się współczynniki liczbowe, ponieważ dla odpowiednio dużych zbiorów danych współczynniki stałe mają mniej istotne znaczenie niż rodzaj funkcji dominującej, np. f(n) = 100 n 2 + 50 n => O(n 2 ) f(n) = 1000 n => O(n)
Złożoność obliczeniowa Przykładowe złożoności algorytmów, w porządku rosnącym: O(1) złożoność stała, O(logn) złożoność logarytmiczna, O(n) złożoność liniowa, O(nlogn) złożoność liniowo-logarytmiczna, O(n 2 ) złożoność kwadratowa, O(n k ) złożoność wielomianowa (k jest stałą), O(k n ) złożoność wykładnicza (k jest stałą), O(n!) złożoność rzędu silnia
Złożoność obliczeniona 1,0E+12 1,0E+11 1,0E+10 1000 lat 100 lat 1,0E+09 1,0E+08 1 rok 1,0E+07 1,0E+06 1,0E+05 1,0E+04 1,0E+03 1,0E+02 1,0E+01 1,0E+00 1 doba 1 godz. 1 min. 1 s 1,0E-01 1,0E-02 1,0E-03 1 ms 1,0E-04 1,0E-05 1,0E-06 1 10 100 1000 10000 100000 1000000 10000000 100000000 1000000000 1E+10 N N*logN N^2 2^N N!
Sortowanie Sortowanie Istnieje wiele algorytmów sortowania; niektóre mają bardzo specyficzne lub ograniczone zastosowanie (np. stosowane głównie w systemach wbudowanych albo kiedy możliwość ich użycia zależy od statystycznych cech sortowanego zbioru), inne są uniwersalne. Pierwsze algorytmy sortowania pojawiły się w latach 50 ub. wieku (m.in. bublesort 1956, quicksort 1959), ale ciągle są opracowywane nowe użyteczne algorytmy, które znajdują zastosowania (np. timsort, opracowany w 2002 r., jako hybrydowy algorytm merge sort oraz insertion sort, zastosowany m.in. jako wbudowany algorytm sortowania w językach Python i GNU Octave, oraz na platformach Java SE i Android) Duże zainteresowanie algorytmami sortowania wynika z powszechności zastosowań, dużej złożoności obliczeniowej (oraz potrzeby jej zmniejszenia, ze względu na powszechne i częste zastosowania) Algorytmy sortowania są też wdzięcznym tematem nauki programowania (algorytmy jako takie, ale też np. złożoność obl.)
Sortowanie Właściwości i kryteria oceny algorytmów sortowania: Złożoność obliczeniowa Nie zawsze można ją określić jednoznacznie, ponieważ w wielu algorytmach zależy od rozkładu elementów dlatego wyznacza się złożoność optymistyczną (best), średnią i pesymistyczną (worst); Złożoność pamięciowa Algorytmy sortowania "w miejscu" ("in-place") zamieniają elementy miejscami i nie potrzebują dodatkowej pamięci na same elementy, jednak czasami zużywają inne zasoby; Rekursja Użycie rekursji zużywa zasoby (stos systemowy), co może ograniczać zastosowania algorytmów np. quicksort i syst. wbudowane; Istnieją algorytmy rekusywne, nierekursywne oraz mieszane
Sortowanie Właściwości i kryteria oceny algorytmów sortowania: Stabilność Algorytm sortowania jest stabilny, jeżeli "równe" elementy po sortowaniu zachowują swój porządek sprzed sortowania; Mechanizm sortowania (sposób działania) Wyróżnia się algorytmy sortowania przez porównanie ("comparison sort") oraz takie, które porównania nie używają ("integer sort"); Metoda sortowania Wyróżnia się kilka ogólnych metod: sortowanie przez wstawianie, zamiany, wybór, łączenie konkretne algorytmy należą do jednej z metod ogólnych, np. bublesort jest sortowaniem przez zamiany
Sortowanie Właściwości i kryteria oceny algorytmów sortowania: Adaptacyjność Zdolność do skrócenia czasu sortowania, jeżeli zbiór jest częściowo posortowany (ma część elementów we właściwej kolejności) np. bublesort ma śrenią złożoność O(n 2 ) oraz optymistyczną O(n), natomiast insertion sort zawsze O(n 2 ) Złożoność implementacji Niektóre hybrydowe algorytmy sortowania mają bardzo złożone algorytmy, a ich implementacja może być kłopotliwa dla początkującego programisty; inne, jak bublesort, są dziecinnie proste
Sortowanie Algorytmy sortowania stabilne: sortowanie bąbelkowe (ang. bubblesort) O(n 2 ) sortowanie przez wstawianie (ang. insertion sort) O(n 2 ) sortowanie przez scalanie (ang. merge sort) O(nlogn) wymaga dodatkowej pamięci O(n) sortowanie przez zliczanie (ang. counting sort) O(n+k) wymaga dodatkowej pamięci sortowanie kubełkowe (ang. bucket sort) O(n) wymaga dodatkowej pamięci sortowanie biblioteczne (ang. library sort) O(nlogn)
Sortowanie Algorytmy sortowania niestabilne : sortowanie przez wybieranie (ang. selection sort) O(n 2 ) sortowanie Shella (ang. shellsort) O(nlogn) sortowanie szybkie (ang. quicksort) O(nlogn) sortowanie przez kopcowanie (ang. heapsort) O(nlogn)
Sortowanie Złożoność obliczeniowa Sortowanie przez porównanie Na sortowanie przez porównanie składają się dwie operacje: porównywania elementów i (ewentualnie) ich zamiany która z tych operacji jest bardziej krytyczna? Przy sortowaniu liczb więcej czasu zajmie zamiana (wykonywana w trzech krokach: temp t i, t i t j, t j temp) Przy sortowaniu złożonych danych wielokrotnie więcej czasu zajmie porównanie (np. "Kowalski, Bartłomiej" i "Kowalski, Bartosz"); zamiana zwykle nie dotyczy samych danych to strata czasu tylko referencji do nich Uwaga! W dalszej części zamiana liczb, dla skrócenia zapisu, będzie oznaczana tak: t i t i+1 odpowiada to trzykrokowej operacji powyżej
Sortowanie Złożoność obliczeniowa Sortowanie przez porównanie Najprostsze ("naturalne", intuicyjnie zrozumiałe) algorytmy sortowania mają złożoność obliczeniową O(n 2 ) średnią lub pesymistyczną Dolne ograniczenie złożoności wynosi O(nlogn) istnieją zarówno stabilne, jak i niestabilne algorytmy o tej złożoności Sortowanie bez porównań ("integer sort") Najprostsze algorytmy mają złożoność O(n k) lub O(n+r) (k oznacza rozmiar klucza sortowania, r zakres wartości kluczy) Dolne ograniczenie złożoności wynosi O(n), jednak jest osiągalne tylko dla algorytmów niestabilnych Sortowanie bez porównań ma istotne praktyczne ograniczenia (co do złożoności klucza sortowania oraz rozkładu wartości kluczy) oraz zwykle większą złożoność pamięciową, co najmniej O(n)
Sortowanie bąbelkowe Sortowanie bąbelkowe jest chyba najprostszym pojęciowo oraz najłatwiejszym w implementacji algorytmem sortowania Polega na porównywaniu sąsiednich elementów i w razie potrzeby ich zamianie; Przy N elementach trzeba N-1 porównań, a cały proces trzeba powtórzyć co najwyżej N-1 razy FOR n=0, 1,, N-2 FOR i=0, 1,, N-2 IF t i > t i+1 t i t i+1
Sortowanie bąbelkowe Udoskonalenia (1) Algorytm można zakończyć, jeżeli wewnętrzna pętla nie wykona ani jednej zamiany (wtedy zewnętrzna pętla może być nieskończona) FOR Z = false FOR i=0, 1,, N-2 IF t i > t i+1 t i t i+1 Z = true IF NOT(Z) BREAK
Sortowanie bąbelkowe Udoskonalenia (2) Po pierwszym wykonaniu pętli wewnętrznej element największy znajdzie się na samym końcu ("wygra" wszystkie porównania) liczba porównań pętli wewnętrznej może być zmniejszana Można połączyć z (1) FOR n=0, 1,, N-2 FOR i=0, 1,, N-n-2 IF t i > t i+1 t i t i+1 FOR n=n-2, N-1,, 2 FOR i=0, 1,, n IF t i > t i+1 t i t i+1
Sortowanie bąbelkowe Udoskonalenia (3) Jeżeli zbiór będzie prawie posortowany, z wyjątkiem ostatniego elementu (np. { 2, 3, 4, 5, 6, 1 }), to algorytm będzie miał złożoność O(n 2 ), chociaż mógłby mieć O(n) gdyby odwrócić kierunek sortowania aby takiemu efektowi zapobiec, wewnętrzna pętla powinna na zmianę biec w górę i w dół zbioru, po coraz krótszym jego fragmencie FOR l = 1 r = N-2 FOR i = l, l+1,, r IF t i > t i+1 => t i t i+1 r r-1 FOR i = r, r-1,, l IF t i > t i+1 => t i t i+1 l l+1
Sortowanie gnoma Ciekawy wariant sortowania bąbelkowego, przedstawiany w formie dykteryjki: gnom (niezbyt inteligentny) ma w ogrodzie poukładać doniczki z kwiatami wg wysokości kwiatów; zaczyna od jednej strony (np. od lewej) i porównuje kwiat, przy którym stoi z następnym. Jeżeli są we właściwej kolejności, to przechodzi dalej, jeżeli nie, to zamienia je miejscami i cofa się (chyba że już jest na samym początku) 3 4 5 1 6 2
Sortowanie gnoma Ulepszenie Wadą algorytmu jest jałowe powtarzanie porównań podczas poruszania się w prawo, zaraz po cofnięciu się w lewo: 3 4 5 1 2 6
Sortowanie gnoma Ulepszenie Wadą algorytmu jest jałowe powtarzanie porównań podczas poruszania się w prawo, po cofnięciu się w lewo Wiadomo, że od bieżącego miejsca aż do miejsca, gdzie dotarł poprzednio, kwiaty są posortowane => powinien to miejsce zaznaczyć (powiesić tam swoją czapkę?) i od razu tam wrócić 1 3 4 5 2 6
Sortowanie gnoma Wersja bez ulepszenia p = 0 WHILE p<n-1 IF t p > t p+1 t p t p+1 IF p>0 p p-1 ELSE p p+1 3 4 5 1 6 2
Sortowanie przez wstawianie Przypomina układanie kart w ręku gracza: dzielimy elementy zbioru na część posortowaną (z lewej) i nieposortowaną (z prawej); należy pobrać pierwszy element z części nieposortowanej i przesuwając się w lewo znaleźć miejsce, gdzie powinien się znaleźć. Elementy posortowane, od tego miejsca aż do części nieposortowanej, należy przesunąć w prawo, żeby zrobić miejsce dla elementu wstawianego FOR n = 1, 2,, N-1 temp = t n i = n 1 WHILE i>=0 AND t i >temp t i+1 t i i i-1 t i+1 = temp
Sortowanie przez wybieranie Koncepcyjnie zbliżony do sortowania przez wstawianie, ale niestabilny. Najprostsza wersja polega na szukaniu minimum dla nieposortowanej przez kolejne porównania i (ewentualnie) zamiany elementu z początku tej części ze wszystkimi kolejnymi elementami: FOR n = 0, 1,, N-2 FOR i = n+1, n+2, N-1 IF t i <t n t i t n metoda jest niewydajna, ponieważ elementy w nieposortowanej części mają tendencję do układania się w porządku malejącym
Sortowanie przez wybieranie Najlepszy wariant sortowania przez wybieranie polega na szukaniu minimum w nieposortowanej (prawej) części zbioru i jednorazowej zamianie miejscami minimum oraz pierwszego elementu tej części: FOR n = 0, 1,, N-2 min = t n poz = n FOR i = n+1, n+2, N-1 IF t i < min min t i poz = i t n t poz
Sortowanie złożoność obliczeniowa Wszystkie proste algorytmy mają złożoność średnią O(n 2 ). Np. dla sortowania przez wybieranie, dla N elementów jest (N/2)(N-1) porównań: E E E E E P P P P P P P P P N-1 P N-1
Sortowanie medianowe Koncepcja z pozoru słuszna, w praktyce zupełnie nieprzydatna, leżąca u podstaw innych wydajnych algorytmów, a w tym bardzo popularnych (np. quicksort), o złożoności O(nlogn)
Sortowanie medianowe Skoro proste sortowanie ma złożoność O(n 2 ) to może zróbmy tak: podzielmy zbiór na dwie połowy i posortujmy je oddzielnie czas powinien być 2 x mniejszy: 2 x (n/2) 2 = ½ n 2 Skoro tak, to czemu tego nie powtórzyć dzieląc połowy tablicy znów na połowy, znowu zyskamy na czasie: 4 x (n/4) 2 = ¼ n 2 Gdyby powtórzyć to parę razy (aż do fragmentu tablicy o rozmiarze 1), to takich kroków będzie log 2 n, a złożoność obliczeniowa n logn
Sortowanie medianowe Tak może wyglądać pierwszy etap sortowania: 2 14 7 15 3 19 5 11 2 14 7 15 3 19 5 11 2 7 14 15 3 5 11 19 Niestety powstaje pewien drobny problem posortowane oddzielnie tablice po połączeniu wcale nie tworzą tablicy posortowanej
Sortowanie medianowe Tak może wyglądać pierwszy etap sortowania: 2 14 7 15 3 19 5 11 2 14 7 15 3 19 5 11 2 7 14 15 3 5 11 19 Niestety powstaje pewien drobny problem posortowane oddzielnie tablice po połączeniu wcale nie tworzą tablicy posortowanej Rozwiązanie: załóżmy, że M jest medianą tablicy (tj. dokładnie połowa elementów jest od M mniejsza, a połowa większa) trzeba przerzucić te mniejsze na lewo, a większe na prawo, a dopiero potem sortować
Sortowanie medianowe Sortowanie medianowe (pierwszy etap): 2 14 7 15 3 19 5 11 2 14 7 15 3 19 5 11 2 5 7 3 15 19 14 11 Wygląda na to, że błąd został poprawiony. Skoro tak, to czynności można powtórzyć, dzieląc tablice znowu na połowy
Sortowanie medianowe Sortowanie medianowe (całość): 2 14 7 15 3 19 5 11 2 14 7 15 3 19 5 11 2 5 7 3 15 19 14 11 2 5 7 3 15 19 14 11 2 3 7 5 11 14 19 15 2 3 7 5 11 14 19 15 2 3 5 7 11 14 15 19
Sortowanie medianowe Sortowanie medianowe (całość): 2 14 7 15 3 19 5 11 2 5 7 3 15 19 14 11 2 3 7 5 11 14 19 15 2 3 5 7 11 14 15 19 W sortowaniu medianowym sortowanie polega wyłącznie na przerzucaniu elementów pomiędzy połówkami zbioru nie porównuje się elementów z innymi elementami, a wyłącznie z medianą, złożoność obliczeniowa jest najlepsza możliwa, O(nlogn), jest jednak pewien problem
Sortowanie medianowe Sortowanie medianowe (całość): 2 14 7 15 3 19 5 11 2 5 7 3 15 19 14 11 2 3 7 5 11 14 19 15 2 3 5 7 11 14 15 19 W sortowaniu medianowym sortowanie polega wyłącznie na przerzucaniu elementów pomiędzy połówkami zbioru nie porównuje się elementów z innymi elementami, a wyłącznie z medianą, złożoność obliczeniowa jest najlepsza możliwa, O(nlogn), jest jednak pewien problem złożoność szukania mediany jest O(n 2 )
Sortowanie medianowe Sortowanie medianowe, przez złożoność szukania mediany O(n 2 ), jest nieopłacalne, jednak sama koncepcja podziału zbioru na połowy leży u podstaw dwóch wydajnych algorytmów sortowania (i pewnej liczby ich odmian i wariacji): Sortowanie przez scalanie (merge sort) "posortujmy te połówki oddzielnie, potem coś wykombinujemy" Szybkie sortowanie (quicksort) "weźmy coś prostszego zamiast tej całej mediany, potem zobaczymy co wyszło"
Sortowanie złożoność obliczeniowa Algorytmy bazujące na medianowym mają złożoność średnią O(nlogn). Np. dla sortowania medianowego jest N log 2 N porównań: E E E E E P P P P P P P P log 2 N N-1 Dla sortowania medianowego, sortowania przez scalanie i szybkiego jest log 2 N podziałów zbioru na połowy. Oznacza to log 2 N scaleń (każde N porównań) w merge sort, podobnie log 2 N poziomów rekurencji i przerzucania (małe na lewo duże na prawo, N porównań) w quicksort
Sortowanie przez scalanie Sortowanie przez scalanie (merge sort) polega na oddzielnym sortowaniu (hipotetycznie) połówek zbioru, a następnie ich scaleniu, prze wybieranie z lewej i prawej części kolejnych najmniejszych elementów; pomysł jest powielany na kolejnych poziomach podziału zbioru na połowy 2 14 7 15 3 19 5 11 2 14 7 15 3 19 5 11 2 7 14 15 3 5 11 19 2 3
Szybkie sortowanie Szybkie sortowanie (quicksort) jest najbliższe sortowaniu medianowemu, tyle że zamiast mediany jest brana wartość ją zastępująca zależnie od implementacji: wartość z arytmetycznego środka zbioru (najczęściej) wartość z losowo wybranej pozycji średnia arytmetyczna z kilku pozycji Optymistyczna złożoność obliczeniowa O(nlogn), pesymistyczna O(n 2 ) (bardzo mało prawdopodobna) Zaimplementowana starannie pod kątem wydajności jest 2-3 x szybsza niż merge sort i heap sort; Jest wykonywany "w miejscu", jednak rekurencyjnie - zużywa stos, co w komputerach o małych zasobach jest kłopotliwe Jest niestabilny (istnieją też warianty stabilne)
Szybkie sortowanie Szybkie sortowanie (quicksort) implementacja Implementacja pozornie jest prosta: QUICSORT (t, p, k) s = PodzielTablice(t, p, k) QUICKSORT(t, p, s) QUICKSORT(t, s+1, k) W praktyce podział (z przerzuceniem elementów mniejszych od "pseudo-mediany" na prawo i mniejszych od niej na lewo) jest kłopotliwy, ponieważ części tablicy nie muszą być równe (zależą od wartości "pseudo-mediany") Istnieją implementacje wykorzystujące 3 pętle while, dość trudne: WHILE (pp <> kk) WHILE (pp<kk AND t pp <quasimed) pp++ WHILE (kk>pp AND t kk >quasimed) kk t pp t kk
Szybkie sortowanie Szybkie sortowanie (quicksort) implementacja Najprostsza implementacja używa jednej pętli for i sprytnej sztuczki: ustawia "pseudomedianę" pomiędzy lewą i prawą częścią tablicy, dzięki czemu dalsze sortowanie tego elementu już nie dotyczy: QUICSORT (t, p, k) IF (p >= k) return; pivotpoint = p + (k-p)/2 pivotvalue = t[pivotpoint] t[pivotpoint] t[k] splitpoint = p FOR i=p, p+1,, k-1 IF t[i]<pivotvalue t[i] t[splitpoint] splitpoint++ t[splipoint] t[k] // koniec rekurencji // środek tablicy // pseudomediana // "środek" na koniec // punkt podziału tabl. // bez "środka"!!! // "środek" na środek QUICSORT (t, p, splitpoint-1) // rekurencja QUICSORT (t, splitpoint+1, k)
Sortowanie bez porównywania Sortowanie bez porównywania ma ograniczone zastosowania dla zbiorów o równomiernym rozkładzie kluczy, należących do skończonego zbioru, najlepiej o kluczach całkowitych, przy niewielkiej liczbie kluczy "pustych" wówczas osiąga złożoność O(n) Wadą tych algorytmów jest złożoność pamięciowa przynajmniej O(n)
Sortowanie przez zliczanie Sortowanie przez zliczanie (counting sort) Może być stosowana dla zbiorów o równomiernym rozkładzie kluczy należących do skończonego zbioru wówczas osiąga złożoność O(n+k) oraz złożoność pamięciową O(n+k), gdzie n oznacza liczebność zbioru, k rozpiętość wartości kluczy Polega na utworzeniu histogramu, tj. policzeniu liczby wystąpień kluczy o tej samej wartości. Później wartości są wkładane wprost na pozycję wynikającą z histogramu Jest stabilny W skrajnym przypadku (kiedy klucze nie powtarzają się) ma złożoność obliczeniową O(n) i pamięciową O(k) Nie nadaje się do sortowania złożonych danych (np. dane osobowe, alfabetycznie) co prawda klucze da się wyznaczyć, ale rozpiętość jest zbyt duża i złożoność pamięciowa jest nie do przyjęcia
Sortowanie kubełkowe Sortowanie kubełkowe (bucket sort) Wariant koncepcji poprzedniej należy podzielić zakres rozpiętości kluczy na k przedziałów (kubełków), następnie przeglądając elementy zbioru przypisywać je do odpowiedniego kubełka na podstawie wartości klucza (złożoność O(n)). Niepuste kubełki trzeba posortować (stosując mniejsze kubełki) itd. Przy równomiernym rozkładzie kluczy całkowitych osiąga złożoność O(n+k) oraz złożoność pamięciową O(n+k) i jest stabilny
Bogosort Pseudosortowanie (bogus sort) Jeden z algorytmów zaprojektowanych tak, aby były jak najwolniejsze. Polega na losowym ustawianiu elementów zbioru przy każdej iteracji i sprawdzaniu, czy przypadkiem kolejność nie jest właściwa (niemalejąca). Możliwych permutacji zbioru n elementów jest n!, stąd złożoność obliczeniowa O(n!) Już przy kilkudziesięciu elementach czas sortowania trzeba liczyć w miliardach lat (np. 25! 1,5 10 25, 50! 3 10 64 ) FOR gotowe = true FOR i=0, 1,, N-1 IF t i >t i+1 gotowe = false; BREAK IF gotowe BREEAK FOR i=n-1, N-2,, 1 t i t random(i)