Syllabus Wprowadzenie Poprawność algorytmów (analiza algorytmów) Sortowanie Elementarne struktury danych Wyszukiwanie Zaawansowane struktury danych Programowanie dynamiczne 1
Literatura T. Cormen, Ch. Lieserson, R. Rivest, Wprowadzenie do Algorytmów, WNT, 1997 R. Sedgewick, Algorytmy w C++, RM, 1999 N. Wirth, Algorytmy + struktury danych = programy, WNT, 2001 2
O co w tym wszystkim chodzi? Rozwiązywanie problemów: Układanie planu zajęć Balansowanie własnego budżet Symulacja lotu samolotem Prognoza pogody Dla rozwiązania problemów potrzebujemy procedur, recept, przepisów inaczej mówiąc algorytmów 3
Historia Nazwa pochodzi od perskiego matematyka Muhammeda ibn Musa Alchwarizmiego (w łacińskiej wersji Algorismus) IX w n.e. Pierwszy dobrze opisany algorytm algorytm Euklidesa znajdowania największego wspólnego podzielnika, 400-300 p.n.e. XIX w. Charles Babbage, Ada Lovelace. XX w. Alan Turing, Alonzo Church, John von Neumann 4
Struktury danych i algorytmy Algorytm metoda, zestaw działań (instrukcji) potrzebnych do rozwiązania problemu Program implementacja algorytmu w jakimś języku programowania Struktura danych organizacja danych niezbędna dla rozwiązania problemu (metody dostępu etc.) 5
Ogólne spojrzenie Wykorzystanie komputera: Projektowanie programów (algorytmy, struktury danych) Pisanie programów (kodowanie) Weryfikacja programów (testowanie) Cele algorytmiczne: - poprawność, - efektywność, Cele implementacji: - zwięzłość - możliwość powtórnego wykorzystania 6
Problemy algorytmiczne Specyfikacja wejścia? Specyfikacja wyjścia, jako funkcji wejścia Ilość instancji danych spełniających specyfikację wejścia może być nieskończona, np.: posortowana niemalejąco sekwencja liczb naturalnych, o skończonej długości: 1, 20, 908, 909, 100000, 1000000000. 3, 44, 211, 222, 433. 3. 7
Rozwiązanie problemu Instancja wejściowa (dane), odpowiadająca specyfikacji algorytm Wyniki odpowiadające danym wejściowym Algorytm opisuje działania, które mają zostać przeprowadzone na danych Może istnieć wiele algorytmów rozwiązujących ten sam problem 8
Definicja algorytmu Algorytmem nazywamy skończoną sekwencję jednoznacznych instrukcji pozwalających na rozwiązanie problemu, tj. na uzyskanie pożądanego wyjścia dla każdego legalnego wejścia. Własności algorytmów: określoność skończoność poprawność ogólność dokładność 9
Przykład 1: poszukiwanie Wejście: uporządkowany niemalejąco ciąg n (n >0) liczb liczba a 1, a 2, a 3,.,a n ; q Wyjście: indeks (pozycja) odnalezionej wartości lub NIL j 2 5 4 10 11; 5 2 2 5 4 10 11; 9 NIL 10
Przykład 1: poszukiwanie liniowe INPUT: INPUT: A[1..n] A[1..n] tablica tablica liczb, liczb, q liczba liczba całkowita. OUTPUT: indeks indeksj j taki, taki, że żea[j] = q. q. NIL, NIL, jeśli jeśli j j (1 j n): A[j] A[j] q j 1 j 1 while while j n nand A[j] A[j] q q do doj++ if if j n nthen thenreturn j else else return NIL NIL Algorytm wykorzystuje metodę siłową (brute-force) przegląda kolejno elementy tablicy. Kod napisany jest w jednoznacznym pseudojęzyku (pseudokodzie). Wejście (INPUT) i wyjście (OUTPUT) zostały jasno określone. 11
Pseudokod Zbliżony do Ady, C, Javy czy innego języka programowania: struktury sterujące (if then else, pętle while i for) przypisanie ( ) dostęp do elementów tablicy: A[i] dla typów złożonych (record lub object) dostęp do pól: A.b zmienna reprezentująca tablicę czy obiekt jest traktowana jak wskaźnik do tej struktury (podobnie, jak w C). 12
Warunki początkowe i końcowe (precondition, postcondition) Ważne jest sprecyzowanie warunków początkowego i końcowego dla algorytmu: INPUT: określenie jakie dane algorytm powinien dostać na wejściu OUTPUT: określenie co algorytm powinien wyprodukować. Powinna zostać przewidziana obsługa specjalnych przypadków danych wejściowych 13
Przykład 2: sortowanie Wejście ciąg n liczb Wyjście Permutacja wejściowego ciągu a 1, a 2, a 3,.,a n Sort b 1,b 2,b 3,.,b n 2 5 4 10 7 2 4 5 7 10 poprawność wyjścia: Dla Dla każdego wejścia algorytm po po zakończeniu działania powinien dać daćjako wynik b 1, 1, b 2, 2,, b n takie, n że: że: b 1 < 1 b 2 < 2 b 3 < 3.. < b n n b 1, 1, b 2, 2, b 3, 3,., b n jest n jestpermutacją a 1, 1, a 2, 2, a 3,.,a 3 n n 14
Sortowanie przez wstawianie (Insertion Sort) A Strategia zaczynamy od od pustej pustej ręki ręki wkładamy kartę kartęwe we właściwe miejsce kart kart poprzednio już już posortowane kontynuujemy takie takie postępowanie aż ażwszystkie karty karty zostaną wstawione 3 4 6 8 9 7 2 5 1 1 j n i INPUT: INPUT: A[1..n] A[1..n] tablica tablica liczb liczb całkowitych OUTPUT: permutacja A taka, taka, że że A[1] A[1] A[2] A[n] for forj 2 to to n do dokey A[j] wstaw wstawa[j] A[j] do do posortowanej sekwencji A[1..j-1] i j-1 while while i>0 i>0 and and A[i]>key do do A[i+1] A[i] i-- i-- A[i+1] key 15
Analiza algorytmów Efektywność: Czas działania Wykorzystanie pamięci Efektywność jako funkcja rozmiaru wejścia: Ilość danych wejściowych (liczb, punktów, itp.) Ilość bitów w danych wejściowych 16
Analiza sortowania przez wstawianie Określany czas wykonania jako funkcję rozmiaru wejścia for j 2 to n do key A[j] wstaw A[j] do posortowanej sekwencji A[1..j-1] i j-1 while i>0 and A[i]>key do A[i+1] A[i] i-- A[i+1]:=key czas c 1 c 2 0 c 3 c 4 c 5 c 6 c 7 ile razy n n-1 n-1 n-1 n t j 2 j n ( t 1) j 2 j n = ( t 1) j 2 j = n-1 = 17
Przypadki: najlepszy/najgorszy/średni Najlepszy przypadek: elementy już są posortowane t j =1, czas wykonania liniowy (Cn). Najgorszy przypadek: elementy posortowane nierosnąco (odwrotnie posortowane) t j =j, czas wykonania kwadratowy (Cn 2 ) Przypadek średni : t j =j/2, czas wykonania kwadratowy (Cn 2 ) 18
Przypadki: najlepszy/najgorszy/średni Dla ustalonego n czas wykonania dla poszczególnych instancji: 6n 5n 4n 3n 2n 1n 19
Przypadki: najlepszy/najgorszy/średni Dla różnych n: najgorszy przypadek Czas działania 6n 5n 4n 3n 2n 1n średni przypadek najlepszy przypadek 1 2 3 4 5 6 7 8 9 10 11 12.. Rozmiar wejścia 20
Przypadki: najlepszy/najgorszy/średni Analizę najgorszego przypadku stosuje się zwykle wtedy, kiedy czas działania jest czynnikiem krytycznym (kontrola lotów, sterowanie podawaniem leków itp.) Dla pewnych zadań najgorsze przypadki mogą występować dość często. Określenie przypadku średniego (analiza probabilistyczna) jest często bardzo kłopotliwe 21
Różnice w podejściu? Czy sortowanie przez wstawianie jest najlepszą strategią dla zadania sortowania? Rozważmy alternatywną strategię opartą o zasadę dziel i zwyciężaj : Sortowanie przez łączenie (MergeSort): ciąg <4, 1, 3, 9> dzielimy na dwa podciągi Sortujemy te podciągi: <4, 1> i <3, 9> łączymy wyniki Czas wykonania rzędu n log n 22
Analiza wyszukiwania INPUT: INPUT: A[1..n] A[1..n] tablica tablica liczb liczb całkowitych, q liczba liczba całkowita OUTPUT: indeks indeksj j taki, taki, że że A[j] A[j] = q. q. NIL, NIL, jeśli jeśli j j (1 j n): A[j] A[j] q j 1 j 1 while while j n nand A[j] A[j] q q do doj++ if if j n nthen thenreturn j else else return NIL NIL Najgorszy przypadek: C n Średni przypadek: C n/2 23
Poszukiwanie binarne Pomysł: dziel i zwyciężaj INPUT: INPUT: A[1..n] A[1..n] posortowana tablica tablica liczb liczb całkowitych, q liczba liczba całkowita. OUTPUT: indeks indeksj j taki, taki, że żea[j] = q. q. NIL, NIL, jeśli jeśli j j (1 j n): A[j] A[j] q left 1 right n do do j (left+right)/2 if if A[j]=q then thenreturn j else else if if A[j]>q then then right j-1 else else left=j+1 while while left<=right return NIL NIL 24
Poszukiwanie binarne - analiza Ile razy wykonywana jest pętla: Po każdym przebiegu różnica międzyleft aright zmniejsza się o połowę początkowo n pętla kończy się kiedy różnica wynosi 1 lub 0 Ile razy trzeba połowić n żeby dostać 1? lg n lepiej niż poprzedni algorytm (n) 25
Poprawność algorytmów 26
Przegląd Poprawność algorytmów Podstawy matematyczne: Przyrost funkcji i notacje asymptotyczne Sumowanie szeregów Indukcja matematyczna 27
Poprawność algorytmów Algorytm jest poprawny jeżeli dla każdego legalnego wejścia kończy swoje działanie i tworzy pożądany wynik. Automatyczne dowiedzenie poprawności nie możliwe Istnieją jednak techniki i formalizmy pozwalające na dowodzenie poprawności algorytmów 28
Poprawność praktyczna i całkowita Praktyczna Jeśli ten punkt został osiągnięty to otrzymaliśmy poprawny wynik Poprawne dane algorytm Wynik Całkowita poprawność Zawsze ten punkt zostanie osiągnięty i otrzymamy poprawny wynik Poprawne dane algorytm Wynik 29
Dowodzenie W celu dowiedzenia poprawności algorytmu wiążemy ze specyficznymi miejscami algorytmu stwierdzenia (dotyczące stanu wykonania). np., A[1],, A[k] są posortowane niemalejąco Warunki początkowe (Precondition) stwierdzenia, których prawdziwość zakładamy przed wykonaniem algorytmu lub podprogramu (INPUT) Warunki końcowe (Postcondition) stwierdzenia, które muszą być prawdziwe po wykonaniu algorytmu lub podprogramu (OUTPUT) 30
Niezmienniki pętli Niezmienniki stwierdzenia prawdziwe za każdym razem kiedy osiągany jest pewien punkt algorytmu (może to zdarzać się wielokrotnie w czasie wykonania algorytmu, np. w pętli) Dla niezmienników pętli należy pokazać : Inicjalizację prawdziwość przed pierwszą iteracją Zachowanie jeśli stwierdzenie jest prawdziwe przed iteracją to pozostaje prawdziwe przed następną iteracją Zakończenie kiedy pętla kończy działanie niezmiennik daje własność przydatną do wykazania poprawności algorytmu 31
Przykład: poszukiwanie binarne (1) Chcemy mieć pewność, że jeżeli zwracany jest NIL to wartości q nie ma w tablicy A niezmiennik: na początku każdego wykonania pętli while A[i] < q dla każdego i [1..left-1] oraz A[i] > q dla każdego i [right+1..n] left 1 right n do do j (left+right)/2 if if A[j]=q then thenreturn j else else if if A[j]>q then then right j-1 else else left=j+1 while while left<=right return NIL NIL inicjalizacja: left = 1, right = n niezmiennik jest prawdziwy (nie ma elementów w [1..left-1] i [right+1..n] ) 32
Przykład: poszukiwanie binarne (2) niezmiennik: na początku każdego wykonania pętli while A[i] < q dla każdego i [1..left-1] oraz A[i] > q dla każdego i [right+1..n] left 1 right n do do j (left+right)/2 if if A[j]=q then thenreturn j else else if if A[j]>q then then right j-1 else else left=j+1 while while left<=right return NIL NIL zachowanie: jeśli A[j]>q, to A[i] > q dla wszystkich i [j..n], ponieważ tablica jest posortowana. Wtedy przypisano j-1 do right. Stąd, druga część niezmiennika również zachodzi. Analogicznie pokazuje się pierwszą część. 33
Przykład: poszukiwanie binarne (3) niezmiennik: na początku każdego wykonania pętli while A[i] < q dla każdego i [1..left-1] oraz A[i] > q dla każdego i [right+1..n] left 1 right n do do j (left+right)/2 if if A[j]=q then thenreturn j else else if if A[j]>q then then right j-1 else else left=j+1 while while left<=right return NIL NIL Zakończenie: kiedy pętla kończy działanie, mamy left > right. Niezmiennik oznacza, że q jest mniejsze od wszystkich elementów A na lewo od left oraz większy od wszystkich elementów A na prawo od right. To wyczerpuje wszystkie elementy A. 34
Przykład: sortowanie przez wstawianie niezmiennik: na początku każdego wykonania pętli for, A[1 j-1] składa się z posortowanych elementów for forj=2 to to length(a) do dokey key A[j] i j-1 j-1 while while i>0 i>0 and and A[i]>key do do A[i+1] A[i] A[i] i-- i-- A[i+1] key key inicjalizacja: j = 2, niezmiennik jest trywialny, A[1] jest zawsze posortowana zachowanie: wewnątrz pętli while przestawia się elementy A[j-1], A[j-2],, A[j-k] o jedną pozycję bez zmiany ich kolejności. Element A[j] jest wstawiany na k-tą pozycję, tak że A[k-1] A[k] A[k+1]. Stąd A[1..j-1] jest posortowane. zakończenie: kiedy pętla się kończy (j=n+1) niezmiennik oznacza, że cała tablica została posortowana. 35
Notacje asymptotyczne Cel: uproszczenie analizy czasy wykonania, zaniedbywanie szczegółów, które mogą wynikać ze specyficznej implementacji czy sprzętu zaokrąglanie dla liczb: 1,000,001 1,000,000 zaokrąglanie dla funkcji: 3n 2 n 2 Główna idea: jak zwiększa się czas wykonania algorytmu wraz ze wzrostem rozmiaru wejścia (w granicy). Algorytm asymptotycznie lepszy będzie bardziej efektywny dla prawie wszystkich rozmiarów wejść (z wyjątkiem być może małych ) 36
Notacje asymptotyczne Notacja O (duże O) Asymptotyczne ograniczenie górne f(n) = O(g(n)), jeżeli istnieje stała c i n 0, takie, że f(n) c g(n) dla n n 0 f(n) i g(n) są nieujemnymi funkcjami całkowitymi Korzysta się z niej przy analizie najgorszego przypadku. Czas działania c g( n) f (n) n0 Rozmiar wejścia 37
Notacje asymptotyczne Notacja Ω (duża Ω) Asymptotyczne ograniczenie dolne f(n) = Ω(g(n)) jeśli istnieje stała c i n 0, takie, że c g(n) f(n) dla n n 0 Opisuje najlepsze możliwe zachowanie się algorytmu Czas działania f (n ) c g( n) n 0 Rozmiar wejścia 38
Notacje asymptotyczne Prosta zasada: odrzucamy mniej istotne dla czasu składniki i czynniki stałe. 50 n log n jest O(n log n) 7n - 3 jest O(n) 8n 2 log n + 5n 2 + n jest O(n 2 log n) O jest ograniczeniem górnym więc np. (50 n log n) jest typu O(n 5 ), ale interesuje nas najlepsze możliwe oszacowanie w tym przypadku jest to O(n log n) 39
Notacje asymptotyczne Notacja Θ (duża Θ ) Dokładne oszacowanie asymptotyczne f(n) = Θ(g(n)) jeżeli istnieją stałe c 1, c 2, i n 0, takie, że c 1 g(n) f(n) c 2 g(n) dla n n 0 f(n) = Θ(g(n)) wtedy i tylko wtedy, gdy f(n) = Ο(g(n)) i f(n) = Ω(g(n)) Czas działania c g (n) 2 f (n ) c g (n) 1 n 0 Rozmiar wejścia 40
Notacje asymptotyczne Istnieją dwie inne notacje asymptotyczne: małe o" f(n)=o(g(n)) mocniejsze ograniczenie analogiczne do O Dla każdego c, musi istnieć n 0, takie, że f(n) c g(n) dla n n 0 mała omega" f(n)=ω(g(n)) analogicznie dla Ω 41
Notacje asymptotyczne Analogie do zależności pomiędzy liczbami: f(n) = O(g(n)) f g f(n) = Ω(g(n)) f g f(n) = Θ(g(n)) f = g f(n) = o(g(n)) f < g f(n) = ω(g(n)) f > g Zwykle zapisujemy: f(n) = O(g(n)), co formalnie powinno być rozumiane jako f(n) O(g(n)) 42
Porównanie czasów wykonania Maksymalny rozmiar problemu (n) 1 sekunda 1 minuta 1 godzina 400n 2500 150000 9000000 20n log n 4096 166666 7826087 2n 2 707 5477 42426 n 4 31 88 244 2 n 19 25 31 43
Szeregi Szereg geometryczny Dana jest liczba całkowita n 0 i rzeczywiste 0< a 1 n i= 0 1 a = + + + + = 1 a i 2 n a 1 a a... a n+ 1 Szereg geometryczny reprezentuje przyrost wykładniczy Szereg arytmetyczny n i= 0 i = 1+ 2 + 3 +... + n = n(1 + n) 2 Przyrost kwadratowy 44
Sumowanie Czas działania sortowania przez wstawianie jest zdeterminowany przez zagnieżdżone pętlę for j 2 to n do key A[j] wstaw A[j] do posortowanej sekwencji A[1..j-1] i j-1 while i>0 and A[i]>key do A[i+1] A[i] i-- A[i+1]:=key Czas wykonania pętli reprezentuje szereg n ( 1) ( 2 j = O n ) j= 2 czas c 1 c 2 0 c 3 c 4 c 5 c 6 c 7 ile razy n n-1 n-1 n-1 n t j 2 j n ( t 1) j 2 j n = ( t 1) j 2 j = n-1 = 45
Dowody indukcyjne Chcemy pokazać prawdziwość własności P dla wszystkich liczb całkowitych n n 0 Założenie indukcyjne: dowodzimy prawdziwości P dla n 0 Krok indukcyjny: dowodzimy, że z prawdziwości P dla wszystkich k, n 0 k n 1 wynika prawdziwość P dla n Przykład: n S( n ) = i= 0 i = n( n + 1), 2 dla n 1 Założenie ind. S(1) 1 = i = i= 0 1(1 + 1) 2 46
Dowody indukcyjne Krok indukcyjny k k( k + 1) S( k) = i = for dla 1 k n 1 2 i= 0 n n 1 S( n) = i = i + n = S( n 1) + n = i= 0 i= 0 2 ( n 1+ 1) ( n n + 2 n) = ( n 1) 2 + n = 2 = n( n + 1) = 2 47
Metoda dziel i zwyciężaj 48
Wprowadzenie Technika konstrukcji algorytmów dziel i zwyciężaj. przykładowe problemy: Wypełnianie planszy Poszukiwanie (binarne) Sortowanie (sortowanie przez łączenie - merge sort). 49
Wypełnianie planszy Zadanie: dysponując klockami oraz planszą 2 n x2 n z brakującym polem: Wypełnić plansze w całości: 50
Wypełnianie planszy: przypadki trywialne (n = 1) Przypadek trywialny (n = 1): wypełniamy plansze jednym klockiem: Idea dla rozwiązania problemu doprowadzić rozmiar zadania do przypadku trywialnego, który umiemy rozwiązać 51
Wypełnianie planszy : podział zadania Oryginalną planszę dzielimy na 4 części Dostajemy problemy o rozmiarze 2 n-1 x2 n-1 Ale: trzy z nich nie są podobne do oryginalnego (plansze nie mają brakującego pola)! 52
Wypełnianie planszy : podział zadania pomysł: umieszczamy jeden klocek w środku planszy i dokonujemy podziału na 4 części Teraz otrzymujemy 4 plansze o rozmiarach 2 n-1 x2 n-1. Każda z planszy ma brakujące pole 53
Wypełnianie planszy : algorytm INPUT: INPUT: n plansza 2 n n x2 x2 n n,, L pozycja brakującego pola. pola. OUTPUT: wypełniona plansza Tile(n, L) L) if if n = 1 then then przypadek trywialny wypełnij jednym klockiem return umieść jeden jeden klocek w środku planszy podziel planszę na na 4 równe równe części Niech NiechL1, L2, L2, L3, L3, L4 L4 oznaczają pozycje 4 brakujących pól pól Tile(n-1, L1) L1) Tile(n-1, L2) L2) Tile(n-1, L3) L3) Tile(n-1, L4) L4) 54
Dziel i zwyciężaj Metoda konstrukcji algorytmów Dziel i zwyciężaj : Jeśli problem jest na tyle mały, że umiesz go rozwiązać - zrób to. Jeśli nie to: Podział: Podziel problem na dwa lub więcej rozdzielnych podproblemów Rozwiązanie: Wykorzystaj metodę rekurencyjnie dla rozwiązania tych podproblemów Łączenie: połącz rozwiązania podproblemów tak, aby rozwiązać oryginalny problem 55
Wypełnianie planszy : Dziel i zwyciężaj Wypełnianie jest przykładem algorytmu dziel i zwyciężaj : w wypadku trywialnym (2x2) po prostu wypełniamy planszę, lub: Dzielimy planszę na 4 mniejsze części (wprowadzając wypełnione miejsce w rogu, przez umieszczenie centralnie jednego klocka) Rozwiązujemy problem rekursywnie stosując tą samą metodę Łączymy części umieszczając klocek w środku planszy 56
Poszukiwanie binarne Odnaleźć liczbę w posortowanej tablicy: Przypadek trywialny tablica jest jednoelementowa Albo dzielimy tablice na dwie równe części i rozwiązujemy zadanie osobno dla każdej z nich INPUT: INPUT: A[1..n] A[1..n] posortowana niemalejąco tablica tablica liczb, liczb, s liczba. liczba. OUTPUT: indeks indeksj j taki, taki, że żea[j] = s. s. NIL, NIL, jeśli jeśli j j (1 j n): A[j] A[j] s Binary-search(A, p, p, r, r, s): s): if if p = r then then if if A[p] A[p] = s then then return p else else return NIL NIL q (p+r)/2 ret ret Binary-search(A, p, p, q, q, s) s) if if ret ret = NIL NILthen return Binary-search(A, q+1, q+1, r, r, s) s) else else return ret ret 57
Rekurencja Czas działania algorytmu z odwołaniami rekursywnymi można opisać poprzez rekurencję Równanie/nierówność opisująca funkcję poprzez jej wartości dla mniejszego argumentu Przykład: poszukiwanie binarne T ( n) Θ (1) if n = 1 = 2 T ( n / 2) + Θ (1) if n > 1 Po rozwiązaniu daje to złożoność O(n)! taką samą jak dla metody naiwnej 58
Poszukiwanie binarne (poprawione) T(n) = Θ(n) nie lepiej niż dla metody siłowej! Poprawa: rozwiązywać zadanie tylko dla jednej połowy tablicy INPUT: INPUT: A[1..n] A[1..n] posortowana niemalejąco tablica tablica liczb, liczb, s liczba. liczba. OUTPUT: indeks indeksj j taki, taki, że żea[j] = s. s. NIL, NIL, jeśli jeśli j j (1 j n): A[j] A[j] s s Binary-search(A, p, p, r, r, s): s): if if p = r then then if if A[p] A[p] = s then then return p else else return NIL NIL q (p+r)/2 if if A[q] A[q] s then then return Binary-search(A, p, p, q, q, s) s) else else return Binary-search(A, q+1, q+1, r, r, s) s) 59
Czas działania metody T ( n) Θ (1) if n = 1 = T ( n / 2) + Θ (1) if n > 1 T(n) = Θ(lg n)! 60
Sortowanie przez łączenie (merge sort) Podziel: Jeśli S posiada przynajmniej dwa elementy (1 lub 0 elementów przypadek trywialny), podziel S na dwie równe (z dokładnością do 1 elementu) części S 1 i S 2. (tj. S 1 zawiera pierwsze n/2 elementów, a S 2 kolejne n/2 ). Zwyciężaj: posortuj sekwencje S 1 i S 2 stosując Merge Sort. Połącz: Połącz elementy z dwóch posortowanych sekwencji S 1 i S 2 w sekwencję S zachowaniem porządku 61
Algorytm Merge Sort Merge-Sort(A, p, p, r) r) if if p < r then then q (p+r)/2 Merge-Sort(A, p, p, q) q) Merge-Sort(A, q+1, q+1, r) r) Merge(A, p, p, q, q, r) r) Merge(A, p, p, q, q, r) r) wybieramy mniejszy z dwóch dwóch elementów na na początku sekwencji A[p..q] oraz oraza[q+1..r] i wkładamy go go do do sekwencji wynikowej, przestawiamy odpowiedni znacznik. Powtarzamy to to aż ażdo do wyczerpania się sięelementów. Rezultat kopiujemy do do A[p..r]. 62
Sortowanie przez łączenie - 1 63
Sortowanie przez łączenie - 2 64
Sortowanie przez łączenie - 3 65
Sortowanie przez łączenie - 4 66
Sortowanie przez łączenie - 5 67
Sortowanie przez łączenie - 6 68
Sortowanie przez łączenie - 7 69
Sortowanie przez łączenie - 8 70
Sortowanie przez łączenie - 9 71
Sortowanie przez łączenie - 10 72
Sortowanie przez łączenie - 11 73
Sortowanie przez łączenie - 12 74
Sortowanie przez łączenie - 13 75
Sortowanie przez łączenie - 14 76
Sortowanie przez łączenie - 15 77
Sortowanie przez łączenie - 16 78
Sortowanie przez łączenie - 17 79
Sortowanie przez łączenie - 18 80
Sortowanie przez łączenie - 19 81
Sortowanie przez łączenie - 20 82
Sortowanie przez łączenie - 21 83
Sortowanie przez łączenie - 22 84
Sortowanie przez łączenie podsumowanie Sortowanie n liczb jeśli n=1 trywialne rekursywnie sortujemy 2 ciągi n/2 i n/2 liczb łączymy dwa ciągi w czasie Θ(n) Strategia Podział problemu na mniejsze, ale analogiczne podproblemy Rekursywne rozwiązywanie podproblemów Łączenie otrzymanych rozwiązań 85
Sortowanie przez łączenie czas działania Czas działania algorytmu może być reprezentowany przez następującą zależność rekurencyjną: T ( n) Θ (1) if n = 1 = 2 T ( n / 2) + Θ ( n) if n > 1 Po rozwiązaniu dostajemy: T ( n) = Θ( nlg n) 86
Wieże Hanoi Mamy 3 wieże oraz stos 64 dysków o zmniejszających sięśrednicach umieszczonych na pierwszej wieży Potrzebujemy przenieść wszystkie dyski na inną wieżę Zabronione jest położenie dysku większego na mniejszym W każdym kroku wolno mam przenieść tylko jeden dysk 87
Wieże Hanoi 88
Rozwiązanie rekursywne 89
Algorytm rekursywny INPUT: INPUT: n ilość ilośćdysków,, a, a, b, b, c wieże, wieże, wieża wieża a zawiera wszystkie dyski. dyski. OUTPUT: a, a, b, b, c wieże, wieże, wieża wieża b zawiera wszystkie dyski dyski Hanoi(n, a, a, b, b, c) c) if if n = 1 then then Move(a,b); else else Hanoi(n-1,a,c,b); Move(a,b); Hanoi(n-1,c,b,a); Poprawność algorytmu łatwo pokazać przez indukcję względem n. 90
Ilość kroków n M(n) Ilość kroków M(n) potrzebnych do rozwiązania problemu dla n dysków spełnia zależność rekurencyjną M(1) = 1 M(n) = 2M(n-1) + 1 1 2 3 1 3 7 4 15 5 31 91
Ilość kroków Rozwijając tę zależność dostajemy M(n) = 2M(n-1) + 1 = 2*[2*M(n-2)+1] + 1 = 2 2 * M(n-2) + 1+2 = 2 2 * [2*M(n-3)+1] + 1 + 2 = 2 3 * M(n-3) + 1+2 + 2 2 = Po k krokach M(n) = 2 k * M(n-k) + 1+2 + 2 2 + + 2 n-k-1 Dla k = n-1 M(n) = 2 n-1 * M(1) + 1+2 + 2 2 + + 2 n-2 = 1 + 2 + + 2 n-1 = 2 n -1 92
Sortowanie 93
Sortowanie - zadanie Definicja (dla liczb): wejście: ciąg n liczb A = (a 1, a 2,, a n ) wyjście: permutacja (a 1,, a n ) taka, że a 1 a n Po co sortować? Podstawowy problem dla algorytmiki Wiele algorytmów wykorzystuje sortowanie jako procedurę pomocniczą Pozwala pokazać wiele technik Dobrze zbadane (czas) 94
Sortowanie taksonomia Wewnętrzne i zewnętrzne Zależnie od miejsca przechowywania zbioru: (RAM czy dysk) Sortowanie tablic i sortowanie list łączonych zależnie od struktury danych (pliku); różny sposób dostępu (bezpośredni dla tablicy, sekwencyjny dla listy). W miejscu lub nie Nie wymaga dodatkowej pamięci Stabilne i niestabilne Kolejność elementów o tych samych wartościach klucza nie zmienia się. Inaczej kolejne sortowanie dla złożonych obiektów nie psuje efektów poprzedniego sortowania. Bezpośrednie i pośrednie zależnie od tego przemieszczamy całe obiekty, czy tylko wskaźniki (indeksy) do nich 95
Zestawienie czasów działania Przez wybór: O(N 2 ) zawsze Bąbelkowe: O(N 2 ) najgorszy przypadek; O(N) najlepszy przyp. Wstawianie: O(N 2 ) średnio; O(N) najlepszy przypadek Shellsort: O(N 4/3 ) Heapsort: O(NlogN) zawsze Mergesort: O(NlogN) zawsze Quicksort: O(NlogN) średnio; O(N 2 ) najgorszy przypadek Zliczanie: O(N) zawsze Radix sort: O(N) zawsze zewnętrzne: O(b logb)) dla pliku o b stronach. 96
Sortowanie przez wybór pomysł Znajdujemy najmniejszy element ciągu i zamieniamy go z pierwszym elementem. Powtarzamy to dla podciągu bez pierwszego elementu, itd. X X Znajdź minimum i zamień z pierwszym elementem 97
Sortowanie przez wybór pseudokod Selection_Sort(int A) 1 for i 1 to length[a] 2 do min i; 3 for j i+1 to length[a] 4 do if A[j] < A[min] then min j; 5 Exchange A[min] A[i] 98
Sortowanie przez wybór przykład iteracja 1 2 3 4 5 6 7 8 9 10 11 ciąg: EASYQUESTION - rozmiar 12 znaków #porównań #zamian EASYQUESTION AESYQUESTION 11 1 AESYQUESTION 10 1 AEEYQUSSTION 9 1 AEEIQUSSTYON 8 1 AEEINUSSTYOQ 7 1 AEEINOSSTYUQ 6 1 AEEINOQSTYUS 5 1 AEEINOQSTYUS 4 1 AEEINOQSSYUT 3 1 AEEINOQSSTUY 2 1 AEEINOQSSTUY 1 1 Razem 66 11 99
Sortowanie przez wybór czas działania Zależność od danych wejściowych: Ilość przebiegów: nie (zawsze N-1) Ilość porównań: nie Ilość zamian: nie O(N 2 ) zawsze (bez znaczenia jaki jest układ elementów w danych ważna tylko ilość) 100
Sortowanie bąbelkowe (przez zamianę) Przechodzimy przez ciąg od jego końca, porównując sąsiadujące elementy i ewentualnie zamieniając je miejscami. Powtarzamy te procedurę aż do uporządkowania całego ciągu. Po pierwszym przejściu element minimalny znajduje się na początku a[0], po drugim na drugim miejscu znajduje się drugi co do wielkości a[1], po itd. Porównanie do wypływających bąbelków stąd nazwa. 101
Sortowanie bąbelkowe pseudokod BUBBLE_SORT(A) 1 for i 1 to length[a] 2 do for j length[a] downto i + 1 3 do if A[j] < A[j - 1] 4 then exchange A[j] A[j - 1] 102
Sortowanie bąbelkowe przykład iteracja 1 2 3 4 5 6 7 8 9 Ciąg: EASYQUESTION, (12 znaków) porównań zamian EASYQUESTION (najgorszy przyp) AEESYQUISTNO 11 (11) 8 (11) AEEISYQUNSTO 10 (10) 6 (10) AEEINSYQUOST 9 (9) 6 (9) AEEINOSYQUST 8 (8) 4 (8) AEEINOQSYSUT 7 (7) 3 (7) AEEINOQSSYTU 6 (6) 2 (6) AEEINOQSSTYU 5 (5) 1 (5) AEEINOQSSTUY 4 (4) 1 (4) AEEINOQSSTUY 3 (3) 0 (3) (2) (2) (1) (1) razem 63 (66) 31 (66) 103
Sortowanie bąbelkowe czas wykonania Zależność od danych wejściowych: Ilość potrzebnych przejść: tak Ilość porównań w jednym przejściu: nie Ilość zamian: tak Najlepszy przypadek: O(N) Jeśli elementy są już posortowane, np.: ABCDEFGHIJ. Tylko jedno przejście. Stąd mamy N-1 porównań i 0 zamian. Najgorszy przypadek: O(N 2 ) Dane odwrotnie posortowane, np.: JIHGFEDCBA. N-1 przejść (N-1)N/2 porównań i (N-1)N/2 zamian 104
Sortowanie przez wstawianie pomysł Dla każdego elementu ciągu (od lewej do prawej), wstawiamy go we właściwe miejsce ciągu elementów poprzedzających go (już posortowanych). 105
Sortowanie przez wstawianie pseudokod INSERTION_SORT(A) 1 for j 2 to length[a] 2 do key A[j] 3 i j-1 4 while i>0 and A[i]>key 5 do A[i+1] A[i] 6 i i-1 7 A[i+1] key 106
Sortowanie przez wstawianie przykład iteracja 1 2 3 4 5 6 7 8 9 10 11 Ciąg: EASYQUESTION, (12 znaków) porównań zamian (najgorszy przyp.) EASYQUESTION AESYQUESTION 1 (1) 1 (1) AESYQUESTION 1 (2) 0 (2) AESYQUESTION 1 (3) 0 (3) AEQSYUESTION 3 (4) 2 (4) AEQSUYESTION 2 (5) 1 (5) AEEQSUYSTION 5 (6) 4 (6) AEEQSSUYTION 3 (7) 2 (7) AEEQSSTUYION 3 (8) 2 (8) AEEIQSSTUYON 7 (9) 6 (9) AEEIOQSSTUYN 7 (10) 6 (10) AEEINOQSSTUY 8 (11) 7 (11) razem 41 (66) 31 (66) 107
Sortowanie przez wstawianie czas działania Zależność od danych wejściowych: Ilość iteracji: nie (zawsze N-1) Ilość porównań: tak Ilość zamian: tak Najgorszy przypadek: O(N 2 ) Elementy odwrotnie posortowane np.: JIHGFEDCBA. (N-1)N/2 porównań i (N-1)N/2 zamian. Najlepszy przypadek: O(N) Elementy już posortowane np.: ABCDEFGHIJ. Jedno porównanie i 0 zamian w każdej iteracji. Razem, N-1 porównań i brak zamian. 108
Shellsort pomysł Modyfikacja (rozszerzona wersja) sortowania przez wstawianie Dążymy do zmniejszenia ilości zamian albo ciągi krótkie, albo lepsze ( bliższe posortowania). Shellsort wykonuje sortowanie podciągów: Wybieramy ciąg liczb (zwany ciągiem przyrostów) h t,, h 2, h 1 ; h 1 =1; h t > > h 2 >h 1 ; Sortujemy ciągi elementów odległych o h t, h t-1, h t-2,,h 1. 109
Shellsort kod w C void shellsort (int[ ] a, int n) { int i, j, k, h, v; int[ ] cols = {1391376, 463792, 198768, 86961, 33936, 13776, 4592, 1968, 861, 336, 112, 48, 21, 7, 3, 1} for (k=0; k<16; k++) { h=cols[k]; for (i=h; i<n; i++) { v=a[i]; j=i; while (j>=h && a[j-h]>v) { a[j]=a[j-h]; j=j-h; } a[j]=v; } } } 110
Shellsort przykład ciąg: EASYQUESTION (12 znaków) ciąg przyrostów 4, 1. faza 1: przyrost = 4 porównań zamian EASYQUESTION EASYQUESTION 2 0 EASYQIESTUON 3 1 EAEYQIOSTUSN 3 2 EAENQIOSTUSY 3 3 Razem w tej fazie 11 6 111
Shellsort przykład faza 2: przyrost= 1 porównań zamian EAENQIOSTUSY AEENQIOSTUSY 1 1 AEENQIOSTUSY 1 0 AEENQIOSTUSY 1 0 AEENQIOSTUSY 1 0 AEEINQOSTUSY 3 2 AEEINOQSTUSY 2 1 AEEINOQSTUSY 1 0 AEEINOQSTUSY 1 0 AEEINOQSTUSY 1 0 AEEINOQSSTUY 3 2 AEEINOQSSTUY 1 0 Razem w tej fazie 16 6 112
Shellsort przykład Razem 27 porównań i 12 zamian. Dla sortowania przez wstawiania odpowiednio 41 i 31!!! Polepszenie dostajemy przez wstępne posortowanie, krótkich podciągów Zwykle stosuje się ciągi przyrostów o więcej niż 2 elementach. 113
Shellsort czas działania Nie ma możliwości przeprowadzenie dokładnej analizy dla przypadki ogólnego (wyniki są oparte o badania empiryczne). Wybór ciągu przyrostów ma zasadniczy wpływ na czas sortowania. Dla ciągu podanego przez Shell a: O(N 2 ) I max = Floor(N/2), I k = Floor(I k+1 /2). np N=64:1, 2, 4, 8, 16, 32 Dla ciągu podanego przez Knuth a: O(N 3/2 ) I 1 =1, I k+1 = 1+3*I k. 1, 4, 13, 40, 121, 364, 114
Mergesort pomysł Dzielimy ciąg na podciągi, sortujemy te podciągi, a następnie łączymy zachowując porządek. Przykład algorytmu typu dziel i zwyciężaj. Potrzeba dodatkowego miejsca dla tych podciągów nie jest to sortowanie w miejscu. Można realizować ten proces w miejscu, ale rośnie stopień komplikacji. 115
Mergesort przykład ciąg: EASYQUESTION (12 znaków) podział EASYQUESTION EASYQU ESTION EAS YQU EST ION E AS Y QU E ST I ON A S Q U S T O N 116
Mergesort przykład łaczenie AEEINOQSSTUY A E S AEQSUY EINOST C1 AES QUY EST INO E AS Y QU E ST I NO A S Q U S T O N Q U Y C2 C3 117
Mergesort - pseudokod MERGE-SORT(A, p, r) 1 if p < r 2 then q (p + r)/2 3 MERGE-SORT(A, p, q) 4 MERGE-SORT(A, q + 1, r) 5 MERGE(A, p, q, r) 118
Mergesort - pseudokod MERGE(A, p, q, r) 1 n1 q - p + 1 2 n2 r - q 3 create arrays L[1..n1 + 1] and R[1..n2 + 1] 4 for i 1 to n1 5 do L[i] A[p + i - 1] 6 for j 1 to n2 7 do R[j] A[q + j] 8 L[n1 + 1] 9 R[n2 + 1] 10 i 1 11 j 1 12 for k p to r 13 do if L[i] R[j] 14 then A[k] L[i] 15 i i + 1 16 else A[k] R[j] 17 j j + 1 119
Sortowanie w czasie liniowym 120
Przegląd Czy możliwe jest sortowanie w czasie lepszym niż dla metod porównujących elementy (poprzednio najlepsze algorytmy dawały czas O(NlogN))? Algorytmy o liniowym czasie działania: Przez zliczanie (Counting-Sort) Radix-Sort Kubełkowe (Bucket-sort) Potrzeba dodatkowych założeń! 121
Sortowanie o czasie liniowym Możliwe przy dodatkowych informacjach (założeniach) o danych wejściowych. Przykłady takich założeń: Dane są liczbami całkowitymi z przedziału [0..k] i k = O(n). Dane są liczbami wymiernymi z przedziału [0,1) o rozkładzie jednostajnym na tym przedziale Trzy algorytmy: Counting-Sort Radix-Sort Bucket-Sort 122
Zliczanie (Counting sort) wejście: n liczb całkowitych z przedziału [0..k], dla k = O(n). pomysł: dla każdego elementu wejścia x określamy jego pozycje (rank): ilość elementów mniejszych od x. jeśli znamy pozycję elementu umieszczamy go na r+1 miejscu ciągu przykład: jeśli wiemy, że w ciągu jest 6 elementów mniejszych od 17, to 17 znajdzie się na 7 miejscu w ciągu wynikowym. powtórzenia: jeśli mamy kilka równych elementów umieszczamy je kolejno poczynając od indeksu pozycja 123
Zliczanie (Counting sort) A = Rank = 4 2 1 3 5 1 2 3 4 5 Dla każdego A[i], liczymy elementy od niego. Daje to rank (pozycję) elementu B = 1 2 3 4 5 Jeśli nie ma powtórzeń i n = k, Rank[A[i]] = A[i] i B[Rank[A[i]] A[i] 124
Zliczanie (Counting sort) A = 5 2 1 3 Jeśli nie ma powtórzeń i n < k, Rank = 1 2 3 4 B = 1 2 3 5 Niektóre komórki tablicy rank pozostają niewykorzystane, ale algorytm działa. 125
Zliczanie (Counting sort) A = 4 2 1 2 3 Jeśli n > k i mamy powtórzenia, Rank = 1 32 4 5 B = 1 2 2 3 4 umieszczamy na wyjściu powtarzające się elementy w takiej kolejności, w jakiej występowały w oryginalnym ciągu (stabilność) 126
Zliczanie (Counting sort) A[1..n] tablica wejściowa B [1..n] tablica wyjściowa C [0..k] pomocnicza tablica (do zliczania) Counting-Sort(A, B, k) 1. for i 0 to k 2. do C[i] 0 3. for j 1 to length[a] 4. do C[A[j]] C[A[j]] +1 5. /* C zawiera ilości elementów równych i 6. for i 1 to k 7. do C[i] C[i] + C[i 1] 8. /* C zawiera ilości elementów i 9. for j length[a] downto 1 10. do B[C[A[j]]] A[j] 11. C[A[j]] C[A[j]] 1 127
Sortowanie przez zliczanie przykład (1) A = C = C = 1 2 3 4 5 6 7 8 2 5 3 0 2 3 0 3 0 1 2 3 4 5 2 0 2 3 0 1 0 1 2 3 4 5 2 2 4 7 7 8 1 2 3 4 5 6 7 8 B = 3 C = 0 1 2 3 4 5 2 2 4 76 7 8 n = 8 k = 6 C[A[j]] C[A[j]] +1 po p.4 C[i] C[i] + C[i 1] po p. 7 B[C[A[j]]] A[j] C[A[j]] C[A[j]] 1 po p. 11 128
Sortowanie przez zliczanie przykład (2) A = C = 1 2 3 4 5 6 7 8 2 5 3 0 2 3 0 3 0 1 2 3 4 5 2 2 4 6 7 8 1 2 3 4 5 6 7 8 B = 0 3 0 1 2 3 4 5 C = 21 2 4 6 7 8 129
Sortowanie przez zliczanie przykład (3) A = C = B = C = 1 2 3 4 5 6 7 8 2 5 3 0 2 3 0 3 0 1 2 3 4 5 2 2 4 6 7 8 1 2 3 4 5 6 7 8 0 3 3 0 1 2 3 4 5 1 2 4 65 7 8 130
Counting sort czas działania Pętla for w p.1-2 zajmuje czas Θ(k) Pętla for w p.3-4 zajmuje czasθ(n) Pętla for w p.6-7 zajmuje czasθ(k) Pętla for w p.9-11 zajmuje czasθ(n) Stąd dostajemy łączny czas Θ(n+k) Ponieważ k = O(n), T(n) = Θ(n) algorytm jest optymalny!! Konieczne jest założenie k = O(n). Jeśli k >> n to potrzeba to potrzeba dużej ilości pamięci. Nie jest to sortowanie w miejscu. 131
Radix sort sortowanie pozycyjne wejście: n liczb całkowitych, d-cyfrowych, łańcuchów o d-pozycjach pomysł: zajmować się tylko jedną z cyfr (sortować względem kolejnych pozycji cyfr/znaków). Zaczynamy od najmniej znaczącej cyfry/znaku, potem kolejne pozycje (cyfry/znaki), aż do najbardziej znaczącej. Musimy stosować metodą stabilną. Ponieważ zbiór możliwych wartości jest mały (cyfry 0-9, znaki a - z ) możemy zastosować metodę zliczania, o czasie O(n) Po zakończeniu ciąg będzie posortowany!! 132
Radix sort przykład 3 2 9 7 2 0 7 2 0 3 2 9 4 5 7 3 5 5 3 2 9 3 5 5 6 5 7 4 3 6 4 3 6 4 3 6 8 3 9 4 5 7 8 3 9 4 5 7 4 3 6 6 5 7 3 5 5 6 5 7 7 2 0 3 2 9 4 5 7 7 2 0 3 5 5 8 3 9 6 5 7 8 3 9 133
Radix-Sort pseudokod Radix-Sort(A, d) 1. for i 1 to d 2. do zastosuj stabilną metodę sortowania do cyfry d dla tablicy A uwagi: złożoność: T(n) = Θ(d(n+k)) Θ(n) dla stałego d i k = O(1) wartości cyfr/znaków są z zakresu [0..k 1] dla k = O(1) Metoda stosowana dla poszczególnych pozycji musi być stabilna! 134
Sortowanie kubełkowe Bucket sort wejście: n liczb rzeczywistych z przedziału [0..1) ważne jest, aby były równomiernie rozłożone (każda wartość równie prawdopodobna) pomysł: dzielimy przedział [0..1) na n podprzedziałów ( kubełków ):0, 1/n, 2/n. (n 1)/n. Elementy do odpowiednich kubełków, a i : 1/i a i 1/(i+1). Ponieważ rozkład jest równomierny to w żadnym z przedziałów nie powinno znaleźć się zbyt wiele wartości. Jeśli wkładamy je do kubełków zachowując porządek (np. przez wstawianie Insertion-Sort), dostaniemy posortowany ciąg. 135
Bucket sort przykład.78 0.12.17 1.12.17.17.39 2.21.23.26.21.26 3.39.23.72 4.26.94 5.39. 21 6.68.68.12 7.72.78.72.23 8.78.68 9.94.94 136
Bucket-Sort A[i] tablica wejściowa B[0], B[1], B[n 1] lista kubełków Bucket-Sort(A) 1. n length(a) 2. for i 0 to n 3. do wstaw A[i] do listy B[floor(nA[i])] 4. for i 0 to n 1 5. do Insertion-Sort(B[i]) 6. Połącz listy B[0], B[1], B[n 1] 137
Bucket-Sort złożoność czasowa Wszystkie instrukcje z wyjątkiem 5 (Insertion-Sort) wymagają czasu O(n), w przypadku pesymistycznym. W przypadku pesymistycznym, O(n) liczb trafi do jednego kubełka czyli ich sortowanie zajmie czas O(n 2 ). Jednak w średnim przypadku stała ilość elementów wpada do jednego przedziału stąd czas średni wyniesie O(n). 138
Sortowanie Quicksort i Heapsort 139
Sortowanie - zadanie Definicja (dla liczb): wejście: ciąg n liczb A = (a 1, a 2,, a n ) wyjście: permutacja (a 1,, a n ) taka, że a 1 a n 140
Zestawienie czasów działania Przez wybór: O(N 2 ) zawsze Bąbelkowe: O(N 2 ) najgorszy przypadek; O(N) najlepszy przyp. Wstawianie: O(N 2 ) średnio; O(N) najlepszy przypadek Shellsort: O(N 4/3 ) Heapsort: O(NlogN) zawsze Mergesort: O(NlogN) zawsze Quicksort: O(NlogN) średnio; O(N 2 ) najgorszy przypadek Zliczanie: O(N) zawsze Radix sort: O(N) zawsze zewnętrzne: O(b logb)) dla pliku o b stronach. 141
Dzisiaj: Dwa algorytmy sortowania: Quicksort Bardzo popularny algorytm, bardzo szybki w średnim przypadku Heapsort Wykorzystuje strukturę kopca (heap) 142
Sortowanie szybkie (Quick Sort) - pomysł Jest to najszybszy w praktyce algorytm sortowania, pozwala na efektywne implementacje. średnio: O(NlogN) najgorzej O(N 2 ), przypadek bardzo mało prawdopodobny. Procedura: Wybieramy element osiowy (pivot ). Dzielimy ciąg na dwa podciągi: elementów mniejszych lub równych od osiowego oraz elementów większych od osiowego. Powtarzamy takie postępowanie, aż osiągniemy ciąg o długości 1. Algorytm typu dziel i zwyciężaj. Jest to metoda sortowania w miejscu (podobnie jak Insert-sort, przeciwnie do np. Merge-sort), czyli nie wymaga dodatkowej pamięci 143
Quicksort algorytm QUICKSORT(A, p, r) 1 if p < r 2 then q PARTITION(A, p, r) 3 QUICKSORT(A, p, q - 1) 4 QUICKSORT(A, q + 1, r) Problemy: 1. Wybór elementu osiowego; 2. Podział (partition). 144
Quicksort podział Funkcja partition dzieli ciąg na dwa podciągi: elementów mniejszych (bądź równych) od osiowego i większych od niego Po podziale: {a[j] a[j] <= a[i] dla j [left, i-1]} a[i] El. osiowy {a[k] a[k] > a[i] dla k [i+1,right]} wynik quicksort(a, left, i-1) wynik quicksort(a, i+1, right) 145
Quicksort przykład podziału ciąg: EASYQUESTION (12 znaków). El. osiowy: N Przeglądaj aż: a[i] > a[right] Przeglądaj aż: a[j] <= a[right] E A S Y Q U E S T I O N i E A I Y Q U E S T S O N i j E A I E Q U Y S T S O N i j j Swap(a[i], a[j]) Swap(a[i], a[j]) (indeksy i oraz j minęły się) Swap(a[i], a[right]) E A I E N U Y S T S O Q Lewy podciąg Prawy podciąg 146
Quicksort wybór elementu osiowego opcja 1: zawsze wybierać skrajny element (pierwszy lub ostatni). Zalety: szybkość; Wady: jeśli trafimy na najmniejszy (największy) element podział nie redukuje istotnie problemu. opcja 2: wybieramy losowo. Zalety: średnio powinno działać dobrze (podział na podciągi o zbliżonej długości); Wady: czasochłonne i nie gwarantuje sukcesu. opcja 3: wybieramy medianę z pierwszych/ostatnich/środkowych 3/5/7 elementów. gwarantuje, że nie będzie zdegenerowanych podciągów (pustych). kompromis pomiędzy opcją 1 i 2 147
Podział pseudokod (opcja 1) Partition(A, Left, Right) 1. Pivot A[Right] 2. i Left 1 3. for j Left to Right 1 4. do if (A[j] Pivot) 5. then i i + 1 6. Exchange(A[i], A[j]) 7. Exchange (A[i+1], A[Right]) 8. return i +1 148
Randomizowany Quicksort (opcja 2) Zakładamy że nie ma powtórzeń Jako element osiowy wybieramy losowy element ciągu (opcja 2) Powtarzamy procedurę, wszystkie podziały są równie prawdopodobne (1:n-1, 2:n-2,..., n-1:1), z prawdopodobieństwem 1/n Randomizacja jest drogą do unikania najgorszego przypadku 149
Randomizowany Quicksort Randomized-Partition(A,p,r) 01 i Random(p,r) 02 exchange A[r] A[i] 03 return Partition(A,p,r) Randomized-Quicksort(A,p,r) 01 if p<r then 02 q Randomized-Partition(A,p,r) 03 Randomized-Quicksort(A,p,q) 04 Randomized-Quicksort(A,q+1,r) 150
Quicksort czas działania Najgorszy przypadek: O(N 2 ) Podciągi zawsze mają długości 0 i N-1 (el. Osiowy jest zawsze najmniejszy/największy). Np. dla posortowanego ciągu i pierwszej opcji wyboru el. osiowego. Najlepszy przypadek: O(NlogN) Podział jest zawsze najlepszy (N/2). El. osiowy zawsze jest medianą. Średnio: O(NlogN) 151
Quicksort najlepszy przypadek Podciągi otrzymane w wyniku podziału są równe T ( n) = 2 T ( n / 2) + Θ( n) 152
Quicksort najgorszy przypadek 153
Quicksort- czas działania T(N) = T(i) + T(N-i-1) + N for N > 1 T(0) = T(1) = 1 T(i) i T(N-i-1) dla podziału i/n-i-1. N dla podziału 1/N-1(liniowe przeglądamy wszystkie elementy). 154
Quicksort czas działania najgorzej: T(N) = T(0) + T(N-1) + N = T(N-1) + N = O(N 2 ) najlepiej: T(N) = 2T(N/2) + N = O(NlogN) średnio : T(N) = (1/N) i=0 N-1 T(i) + (1/N) i=0 N-1 T(N-i-1) + N = (2/N) j=0 N-1 T(j) + N = O(NlogN) 155
Quicksort - uwagi Małe ciągi Quicksort zachowuje sięźle dla krótkich ciągów. Poprawa jeśli podciąg jest mały zastosować sortowanie przez wstawianie (zwykle dla ciągów o długości 5 ~ 20) Porównanie z mergesort: Oba zbudowane na zasadzie dziel i zwyciężaj. Mergesort wykonuje sortowanie w fazie łączenia. Quicksort wykonuje prace w fazie podziału. 156
Heap Sort pojęcie kopca Struktura kopca binarnego Drzewo binarne (bliskie zrównoważenia) Wszystkie poziomy, z wyjątkiem co najwyżej ostatniego, kompletnie zapełnione Wartość klucza w węźle jest większa lub równa od wartości kluczy wszystkich dzieci; własność taka jest zachowana dla lewego i prawego poddrzewa (zawsze) 157
Heap Sort reprezentacja tablicowa kopca Parent (i) return i/2 Left (i) return 2i Right (i) return 2i+1 Własność kopca: 1 2 3 4 5 6 7 8 9 10 A[Parent(i)] A[i] 16 15 10 8 7 9 3 2 4 1 poziomy: 3 2 1 0 158
Heap Sort reprezentacja kopca w tablicy Zauważmy połączenia w drzewie dzieci węzła i występują na pozycjach 2i oraz 2i+1 Czemu to jest wygodne? Dla reprezentacji binarnej, dzieleniu/mnożeniu przez 2 odpowiada przesuwanie (szybka operacja) Dodawanie jedynki oznacza zmianę najmłodszego bitu (po przesunięciu) 159
Kopcowanie (Heapify) Niech i będzie indeksem w tablicy A Niech drzewa binarne Left(i) i Right(i) będą kopcami Ale, A[i] może być mniejsze od swoich dzieci co powoduje złamanie własności kopca Metoda Kopcowania (Heapify) przywraca własności kopca dla A poprzez przesuwanie A[i] w dół kopca aż do momentu, kiedy własność kopca jest już spełniona 160
Kopcowanie (Heapify) 161
Kopcowanie (Heapify) przykład 162
Kopcowanie czas działania Czas działania procedury Heapify dla poddrzewa o n węzłach i korzeniu w i: Ustalenie relacji pomiędzy elementami: Θ(1) dodajemy czas działania Heapify dla poddrzewa o korzeniu w jednym z potomków i, gdzie rozmiar tego poddrzewa 2n/3 jest najgorszym przypadkiem. T ( n) T (2 n /3) + Θ(1) T ( n) = O(log n) Inaczej mówiąc Czas działania dla drzewa o wysokości h: O(h) 163
Budowa kopca Konwertujemy tablicę A[1...n], gdzie n = length[a], na kopiec Zauważmy, że elementy w A[( n/2 + 1)...n] są już zbiorem kopców - jednoelementowych! 164
Budowanie kopca 1 165
Budowanie kopca 2 166
Budowanie kopca 3 167
Budowa kopca analiza Poprawność: indukcja po i, (wszystkie drzewa o korzeniach m > i są kopcami) Czas działania: n wywołań kopcowania (Heapify) = n O(lg n) = O(n lg n) Wystarczająco dobre ograniczenie O(n lg n) dla zadania sortowanie (Heapsort), ale czasem kopiec budujemy dla innych celów 168
Sortowanie za pomocą kopca Heap Sort O( n) Czas działania O(n lg n) + czas budowy kopca (O(n)) 169
Heap Sort 1 170
Heap Sort 2 171
Heap Sort podsumowanie Heap sort wykorzystuje strukturę kopca przez co dostajemy asymptotycznie optymalny czas sortowania Czas działania O(n log n) podobnie do merge sort, i lepiej niż wybór, wstawianie czy bąbelkowe Sortowanie w miejscu podobnie do sortowania przez wybór, wstawianie czy bąbelkowego 172
Dynamiczne struktury danych 173
Plan wykładu Wprowadzenie Popularne dynamiczne struktury danych (ADT) stosy, kolejki, listy opis abstrakcyjny Listy liniowe Implementacja tablicowa stosu i kolejki Drzewa Możliwe implementacje 174
Wprowadzenie Do tej pory najczęściej zajmowaliśmy się jedną strukturą danych tablicą. Struktura taka ma charakter statyczny jej rozmiar jest niezmienny. Powoduje to konieczność poznania wymaganego rozmiaru przed rozpoczęciem działań (ewentualnie straty miejsca deklarujemy wystarczająco dużą tablicę). W wielu zadaniach wygodniejsza jest struktura o zmiennym rozmiarze (w zależności od aktualnych potrzeb) struktura dynamiczna. Potrzebujemy struktury pozwalającej na przechowywanie elementów niezależnie od ich fizycznego położenia. logicznie fizycznie 2 0 5 3 4 1 3 2 4 1 0 5 175
Wprowadzenie Przykładowe operacje dla struktur danych: Insert(S, k): wstawianie nowego elementu Delete(S, k): usuwanie elementu Min(S), Max(S): odnajdowanie najmniejszego/największego elementu Successor(S,x), Predecessor(S,x): odnajdowanie następnego/poprzedniego elementu Zwykle przynajmniej jedna z tych operacji jest kosztowna czasowo (zajmuje czas O(n)). Czy można lepiej? 176
Abstrakcyjne typy danych (Abstract Data Types ADT ) Abstrakcyjnym typem danych nazywany formalną specyfikację sposobu przechowywania obiektów oraz zbiór dobrze opisanych operacji na tych obiektach. Jaka jest różnica pomiędzy strukturą danych a ADT? struktura danych (klasa) jest implementacją ADT dla specyficznego komputera i systemu operacyjnego. 177
Popularne dynamiczne ADT Listy łączone Stosy, kolejki Drzewa z korzeniem (rooted trees), binarne, BST, czerwonoczarne, AVL itd. Kopce i kolejki priorytetowe (późniejsze wykłady) Tablice z haszowaniem (późniejsze wykłady) 178
Listy Lista L jest liniową sekwencją elementów. Pierwszy element listy jest nazywany head, ostatni tail. Jeśli obydwa są równe null, to lista jest pusta Każdy element ma poprzednik i następnik (za wyjątkiem head i tail) Operacje na liście: Successor(L,x), Predecessor(L,x) List-Insert(L,x) List-Delete(L,x) List-Search(L,k) 2 0 2 3 0 1 head x tail 179
Listy łączone Rozmieszczenie fizyczne obiektów w pamięci nie musi odpowiadać ich logicznej kolejności; wykorzystujemy wskaźniki do obiektów (do następnego/poprzedniego obiektu) Manipulując wskaźnikami możemy dodawać, usuwać elementy do listy bez przemieszczania pozostałych elementów listy Lista taka może być pojedynczo lub podwójnie łączona. head a1 a2 a3 an tail null null 180
Węzły i wskaźniki Węzłem nazywać będziemy obiekt przechowujący daną oraz wskaźnik do następnej danej i (opcjonalnie dla listy podwójnie łączonej) wskaźnik do poprzedniej danej. Jeśli nie istnieje następny obiekt to wartość wskaźnika będzie null Wskaźnik oznacza adres obiektu w pamięci Węzły zajmują zwykle przestrzeń: Θ(1) struct node { } key_type key; data_type data; struct node *next; struct node *prev; key data next prev 181
Wstawianie do listy (przykład operacji na liście) wstawianie nowego węzła q pomiędzy węzły p i r: p r p q r a1 a3 a1 a2 a3 a2 next[q] r next[p] q 182
Usuwanie z listy usuwanie węzła q p q r p r a1 a2 a3 a1 a3 next[p] r next[q] null a2 q null 183
Operacje na liście łączonej List-Search(L, k) 1. x head[l] 2. while x null and key[x] k 3. do x next[x] 4. return x List-Insert(L, x) 1. next[x] head[l] 2. if head[l] null 3. then prev[head[l]] x 4. head[l] x 5. prev[x] null List-Delete(L, x) 1. if prev[l] null 2. then next[prev[x]] next[x] 3. else head[l] next[x] 4. if next[l] null 5. then prev[next[x]] prev[x] 184
Listy podwójnie łączone head x a1 a2 a3 a4 tail null null Listy cykliczne: łączymy element pierwszy z ostatnim 185
Stosy Stosem S nazywany liniową sekwencję elementów do której nowy element x może zostać wstawiony jedynie na początek, analogicznie element może zostać usunięty jedynie z początku tej sekwencji. Stos rządzi się zasadą Last-In-First-Out (LIFO). Operacje dla stosu: Stack-Empty(S) Pop(S) Push(S,x) Push 2 0 1 5 Pop head null 186
Kolejki Kolejka Q jest to liniowa sekwencja elementów do której nowe elementy wstawiane są na końcu sekwencji, natomiast elementy usuwane są z jej początku. Zasada First-In-First-Out (FIFO). Operacje dla kolejki: Queue-Empty(Q) EnQueue(Q, x) DeQueue(Q) DeQueue EnQueue 2 0 2 3 0 1 head tail 187
Implementacja stosu i kolejki Tablicowa Wykorzystujemy tablicę A o n elementach A[i], gdzie n jest maksymalną ilością elementów stosu/kolejki. Top(A), Head(A) i Tail(A) są indeksami tablicy Operacje na stosie/w kolejce odnoszą się do indeksów tablicy i elementów tablicy Implementacja tablicowa nie jest efektywna Listy łączone Nowe węzły tworzone są w miarę potrzeby Nie musimy znać maksymalnej ilości elementów z góry Operacje są manipulacjami na wskaźnikach 188
Implementacja tablicowa stosu Push(S, x) 1. if top[s] = length[s] 2. then error overflow 3. top[s] top[s] + 1 4. S[top[S]] x 0 1 2 3 4 5 6 1 5 2 3 Pop(S) 1. if top[s] = -1 2. then error underflow 3. else top[s] top[s] 1 4. return S[top[S] +1] top Kierunek wstawiania Stack-Empty(S) 1. if top[s] = -1 2. then return true 3. else return false 189
Implementacja tablicowa kolejki Dequeue(Q) 1. x Q[head[Q]] 2. if head[q] = length[q] 3. then head[q] 1 4. else head[q] (head[q]+1) mod n 5. return x 1 5 2 3 0 tail head Enqueue(Q, x) 1. Q[tail[Q]] x 2. if tail[q] = length[q] 3. then tail[q] x 4. else tail[q] (tail[q]+1) mod n 190
Drzewa z korzeniem Drzewem z korzeniem T nazywamy ADT dla którego elementy są zorganizowane w strukturę drzewiastą. Drzewo składa się z węzłów przechowujących obiekt oraz krawędzi reprezentujących zależności pomiędzy węzłami. W drzewie występują trzy typy węzłów: korzeń (root), węzły wewnętrzne, liście Własności drzew: Istnieje droga z korzenia do każdego węzła (połączenia) Droga taka jest dokładnie jedna (brak cykli) Każdy węzeł z wyjątkiem korzenia posiada rodzica (przodka) Liście nie mają potomków Węzły wewnętrzne mają jednego lub więcej potomków (= 2 binarne) 191
Drzewa z korzeniem A 0 B C D 1 E F G H I J 2 K L M N 3 192
Terminologia Rodzice (przodkowie) i dzieci (potomkowie) Rodzeństwo (sibling) potomkowie tego samego węzła Relacja jest dzieckiem/rodzicem. Poziom węzła Ścieżka (path): sekwencja węzłów n 1, n 2,,n k takich, że n i jest przodkiem n i+1. Długościąścieżki nazywamy liczbę k. Wysokość drzewa: maksymalna długośćścieżki w drzewie od korzenia do liścia. Głębokość węzła: długośćścieżki od korzenia do tego węzła. 193
Drzewa binarne Drzewem binarnym T nazywamy drzewo z korzeniem, dla którego każdy węzeł ma co najwyżej 2 potomków. A B C A B C D E F D E F G Porządek węzłów jest istotny!!! G 194
Drzewa pełne i drzewa kompletne Drzewo binarne jest pełne jeśli każdy węzeł wewnętrzny ma dokładnie dwóch potomków. Drzewo jest kompletne jeśli każdy liść ma tę samą głębokość. A A B C B C D E D E F G F G pełne kompletne 195
Własności drzew binarnych Ilość węzłów na poziomie d w kompletnym drzewie binarnym wynosi 2 d Ilość węzłów wewnętrznych w takim drzewie: 1+2+4+ +2 d 1 = 2 d 1 (mniej niż połowa!) Ilość wszystkich węzłów: 1+2+4+ +2 d = 2 d+1 1 Jak wysokie może być drzewo binarne o n liściach: (n 1)/2 Wysokość drzewa: 2 d+1 1= n log (n+1) 1 log (n) 196
Tablicowa implementacja drzewa binarnego 1 A 2 3 B C 4 5 6 7 D E F G Poziom 0 1 2 Na każdym poziomie d mamy 2 d elementów 1 2 3 4 5 6 7 A B C D E F G 2 0 2 1 2 2 Kompletne drzewo: parent(i) = floor(i/2) left-child(i) = 2i right-child(i) = 2i +1 197
Listowa implementacja drzewa binarnego root(t) A B C Każdy węzeł zawiera Dane oraz 3 wskaźniki: przodek lewy potomek prawy potomek D E F G H data 198
Listowa implementacja drzewa binarnego (najprostsza) root(t) A B C Każdy węzeł zawiera Dane oraz 2 wskaźniki: lewy potomek prawy potomek D E F G H data 199
Listowa implementacja drzewa (n-drzewa) root(t) A D B E C D F G H I Każdy węzeł zawiera Dane oraz 3 wskaźniki: przodek lewy potomek prawe rodzeństwo J K 200
Drzewa poszukiwań binarnych (BST) 201
O czym będziemy mówić Definicja Operacje na drzewach BST: Search Minimum, Maximum Predecessor, Successor Insert, Delete Struktura losowo budowanych drzew BST 202
Wprowadzenie Poszukujemy dynamicznego ADT, który efektywnie będzie obsługiwał następujące operacje: Wyszukiwanie elementu (Search) Znajdowanie Minimum/Maximum Znajdowanie poprzednika/następnika (Predecessor/Successor) Wstawianie/usuwanie elementu (Insert/Delete) Wykorzystamy drzewo binarne! Wszystkie operacje powinny zajmować czasθ(lg n) Drzewo powinno być zawsze zbalansowane inaczej czas będzie proporcjonalny do wysokości drzewa (gorszy od O(lg n))! 203
Drzewo poszukiwań binarnych (binary search tree) Struktura drzewa z korzeniem Każdy węzeł x posiada pola left(x), right(x), parent(x), oraz key(x). Własność drzewa BST: Niech x będzie dowolnym węzłem drzewa, natomiast niech y będzie należał do poddrzewa o korzeniu w x wtedy:. Jeżeli należy do lewego poddrzewa to: key(y) key(x) Jeżeli należy do prawego poddrzewa to : key(y) > key(x) 204
Przykład BST 5 2 3 7 3 2 5 8 7 5 8 5 Metody przechodzenia przez drzewo : In-order, pre-order, post-order 205
Poszukiwanie w drzewie BST Tree-Search(x,k) if x = null or k = key[x] then return x if k < key[x] then return Tree-Search(left[x],k) else return Tree-Search(right[x],k) rekurencyjnie Tree-Search(x,k) while x null and k key[x] do if k < key[x] then x left[x] else x right[x] return x iteracyjnie złożoność: O(h) 206
Przykład Poszukiwany klucz: 13 207
Przechodzenie przez wszystkie węzły drzewa Inorder-Tree-Walk(x) if x null then Inorder-Tree-Walk(left[x]) print key[x] Inorder-Tree-Walk(right[x]) czas wykonania: T(0) = Θ(1) T(n)=T(k) + T(n k 1) + Θ(1) złożoność: Θ(n) 208
Odnajdowanie minimum i maksimum Tree-Minimum(x) while left[x] null do x left[x] return x Tree-Maximum(x) while right[x] null do x right[x] return x złożoność: O(h) 209
Przykład minimum 210
Odnajdowanie następnika Następnikiem x nazywamy najmniejszy element y wśród elementów większych od x Następnik może zostać odnaleziony bez porównywania kluczy. Jest to : 1. null jeśli x jest największym z węzłów. 2. minimum w prawym poddrzewie t jeśli ono istnieje. 3. najmniejszy z przodków x, dla których lewy potomek jest przodkiem x. 211
Odnajdowanie następnika x z y minimum w prawym poddrzewie t x najmniejszy z przodków x, dla których lewy potomek jest przodkiem x 212
Odnajdowanie następnika Tree-Successor(x) if right[x] null // przypadek 2 then return Tree-Minimum(right[x]) y parent[x] while y null and x = right[y] // przypadek 3 do x y y parent[y] return y 213
Przykład Poszukajmy następników dla 15 (przyp. 2), 13 (przyp. 3) 214
Wstawianie elementów Wstawianie jest bardzo zbliżone do odnajdowania elementu: Odnajdujemy właściwe miejsce w drzewie, w które chcemy wstawić nowy węzeł z. Dodawany węzeł z zawsze staje się liściem. Zakładamy, że początkowo left(z) oraz right(z) mają wartość null. 215
Wstawanie przykład Wstawiamy 13 do drzewa 12 5 18 2 9 15 19 z 13 17 216
Wstawianie pseudokod Tree-Insert(T,z) y null x root[t] while x null do y x if key[z] < key[x] then x left[x] else x right[x] parent[z] y // dla pustego drzewa if y = null then root[t] z else if key[z] < key[y] then left[y] z else right[y] z 217
Usuwanie z drzewa BST Usuwanie elementu jest bardziej skomplikowane niż wstawianie. Można rozważać trzy przypadki usuwania węzła z: 1. z nie ma potomków 2. z ma jednego potomka 3. z ma 2 potomków przypadek 1: usuwamy z i zmieniamy u rodzica wskazanie na null. przypadek 2: usuwamy z a jego dziecko staje się dzieckiem rodzica. przypadek 3:najbardziej złożony; nie można po prostu usunąć węzła i przenieść dzieci do rodzica drzewo przestałoby być binarne! 218
Usuwanie z drzewa BST - przypadek 1 usuwamy delete 219
Usuwanie z drzewa BST przypadek 2 Usuwany węzeł 220
Usuwanie z drzewa BST przypadek 3 Rozwiązanie polega na zastąpieniu węzła jego następnikiem. założenie: jeśli węzeł ma dwóch potomków, jego następnik ma co najwyżej jednego potomka. dowód: jeśli węzeł ma 2 potomków to jego następnikiem jest minimum z prawego poddrzewa. Minimum nie może posiadać lewego potomka (inaczej nie byłoby to minimum) 221
Usuwanie z drzewa BST przypadek 3 Usuwamy z z δ y δ α β α β y w w 222
Usuwanie z drzewa BST przypadek 3 usuwamy następnik 223