• Nie Znaleziono Wyników

Złożoność, efektywność algorytmów

N/A
N/A
Protected

Academic year: 2021

Share "Złożoność, efektywność algorytmów"

Copied!
15
0
0

Pełen tekst

(1)

Złożoność algorytmów

Literatura: 1. The Euclidean Algorithm, Michael Monagan, monagan@inf.ethz.ch; 2. W.M. Turski, Propedeutyka

1. Złożoność obliczeń

Podamy kilka przykładów algorytmów i omówimy ich złożoność, efektywność, w sensie czasu wykonania.

1.1. Algorytm

Turski: Algorytm – informatyczny opis rozwiązania.

Nazwa algorytm pochodzi od nazwiska arabskiego matematyka Muhammeda ibn Musy al-Chorezmi (z Chorezmu), który opisał system dziesiętnego kodowania liczb i sztukę liczenia w tym systemie. Było to ok. roku 820 n. e.

Obecnie przez algorytm rozumie się opis obiektów wraz z opisem czynności, które należy wykonać z tymi obiektami, aby osiągnąć określony cel.

Opisy obiektów występujących w algorytmie będziemy nazywać deklaracjami, a czynności – instrukcjami.

1.2. Algorytm Euklidesa. Co to jest złożoność algorytmu

Cel jaki stawiamy przed sobą to poznanie algorytmu Euklidesa znajdowania n

¯ajwiększego w

¯spólnego d¯zielnika (nwd) dwóch liczb całkowitych dodatnich i szczegółowe opra-cowanie tego prostego algorytmu.

Co to jest nwd? Jeśli zapiszemy dwa ułamki 64/20i 16/5to różnica między nimi

polega na tym, że pierwszy ma licznik i mianownik większe od drugiego o czynnik 4. Oba są jednak takie same. Ułamek drugi jest skróconą wersją pierwszego. Wspól-nym czynnikiem przez które podzieliliśmy licznik i mianownik jest liczba 4. Jest to największa z liczb, przez które obie liczby 64 i 20 dzielą się bez reszty.

Można rozłożyć obie liczby na czynniki pierwsze i napisać 64 = (2)6 20 = (2)2(5).

Widzimy, że maksymalny czynnik wspólny wynosi (2)2= 4. Metoda rozkładu liczb

na czynniki pierwsze nie jest najlepszą z metod znajdowania nwd. Załóżmy bowiem, że liczby, których nwd szukamy są liczbami o ok. 100 cyfrach. Kiedy znajdziemy tą metodą wszystkie pierwsze czynniki obu liczb?

Około 300 lat p.n.e. Euklides, który nie znał żadnego języka programowania, zapisał w języku greckim algorytm znajdowania nwd. (I hope that what follows is not a Greek to you. Monagan, M., ETHZ). Algorytm Euklidesa jest prosty. Załóżmy, że mamy dwie dodatnie, całkowite liczby a i b. nwd posiada następujące własności 1. nwd(a, 0) = a,

2. nwd(a, b) = nwd(b, a), 3. nwd(a, b) = nwd(a − b, b).

(2)

(Trzecia własność: Jeżeli g = nwd(a, b) i h = nwd(a−b, b) to g = h. Najpierw pokażemy, że g dzieli h (co zapiszemy g|h), a następnie, że h|g. W ten sposób h = g. 1. (pokażemy, że g|h) Mamy g = nwd(a, b), a więc g|a i g|b. Stąd a − b = g ∗ (a/g) − g∗ (b/g) = g ∗ (a′− b), tzn. g|(a − b). Stąd g|h = nwd(a − b, b). 2.

(pokażemy, że h|g) Mamy h = nwd(a − b, b). Stąd h|(a − b) i h|b, a więc h|a i dlatego h = nwd(a, b) → h|g.)

Algorytm Euklidesa oparty jest na fakcie, że nwd(a, b) = nwd(a − b, b) Jeżeli a≥ b to korzystamy z własności nwd(a,b)=nwd(b,a) i przezywamy liczby a ↔ b, a w przypadku b = 0 mamy nwd(a, 0) = a.

Algorytm obliczania nwd wygląda więc następująco.1

START

w razie, gdy b=0 -> a

w razie, gdy a<b -> nwd(b,a) -> nwd(a-b,b)

KONIEC

Algorytm jest rzeczywiście prosty. A oto wyniki pośrednie otrzymane na drodze zastosowania tego algorytmu dla liczb a=64, b=20:

(64,20) -> (44,20) -> (24,20) -> (4,20) -> (20,4) -> (16,4) -> (12,4) -> (8,4) -> (4,4) -> (0,4) -> (4,0) => 4.

Zapiszemy jeszcze inną wersję słowną algorytmu Euklidesa: START

dopóki a <> b, dopóty wykonuj jeśli a > b to a-b -> a w przeciwnym wypadku b-a -> a; wypisz(a)

KONIEC

1.3. Złożoność algorytmu

Czy algorytm Euklidesa w przedstawionej postaci jest efektywny? Pomyślmy co się stanie gdy a = 1010a b = 10. W tym przypadku będą wykonywane kolejne

odejmowania, aż otrzymamy zero. Odejmowań będzie 1010, czyli 1010kroków albo

elementarnych instrukcji. Licząc, że w czasie 1 sec wykonywanych będzie 1 milion takich operacji na wynik obliczeń przyjdzie czekać kilka godzin. To co wykonuje algorytm to odejmowanie b od a, aż do chwili, gdy mamy a < b. Inaczej mówiąc algorytm jest algorytmem obliczania reszty z dzielenia a przez b. Oznaczmy ją przez mod(a, b). Zamiast korzystać z nwd(a, b) = nwd(a−b, b) można więc wykorzystać związek nwd(a, b) = nwd(mod(a, b), b). Najczęściej jest tak, że różne funkcje, takie jak mod, są częścią języka wyższego poziomu (np. Pascala).

Algorytm obliczania nwd można teraz przepisać do postaci

1 W zapisie jaki tu stosujemy konstrukcja w razie gdy b=0 -> a oznacza, że ten fragment programu daje wynik a jeżeli b=0.

(3)

START

w razie, gdy b=0 -> a

w razie, gdy a<b -> nwd(b,a) -> nwd(mod(a,b),b)

KONIEC

Inną wersją tego algorytmu, która wykorzystuje instrukcję "dopóki W, dopóty R"jest

START

c:=a, d:=b

dopóki d>0, dopóty wykonuj (* pętla dd *) r:=mod(c,d)

c:=d d:=r koniec pętli dd KONIEC

Ile operacji wykonuje algorytm w czasie jego realizacji dla liczb a i b mniejszych od n? Parametr ten można też nazwać złożonością algorytmu. Algorytm Euklidesa ma złożoność rzędu n2. Będziemy zapisywać to w postaci Θ(n2). Oznacza to, że

liczba wykonanych operacji jest równa

LE= Cn2, c =constans

Jeśli więc podwoimy liczby a, b to czas obliczeń, który jest proporcjonalny do ilości operacji, a więc do złożoności algorytmu, staje się czterokrotnie dłuższy.

Efektywność przedstawionego algorytmu Euklidesa jest taka sama jak efektywność mnożenia liczb całkowitych. (W sprawie LApatrz: Knuth, The art of computer

pro-gramming, t II).

Z najgorszym przypadkiem spotykamy się wtedy, gdy liczby a i b są liczbami Fibonacciego.2 Są to liczby ciągu 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, . . . Zadane są one

związkiem rekurencyjnym

f1= 0, f2= 1, fn= fn−1+ fn−2, n = 3, 4, . . . .

Chcąc policzyć nwd(fn, fn−1)musimy policzyć nwd(fn−1, fn−2), . . . , czyli musimy

policzyć wszystkie liczby Fibonacciego w odwrotnym porządku. Nasz algorytm za-stosowany do liczb a = 55 i b = 34 daje

c d r 55 34 21 34 21 13 21 13 8 13 8 5 8 5 3 5 3 2 3 2 1 2 1 0 1 0 → 1

Czy można wyznaczyć nwd(a, b) za pomocą mniejszej liczby operacji niż Θ(n2)?

Okazuje się, że tak, ale nie można zrobić tego tak szybko jak w przypadku dodawania liczb całkowitych dla którego złożoność jest Θ(n).

Zakończmy ten fragment uwagą o tzw. "Binarnym algorytmie nwd". Ten nadaje się szczególnie dobrze dla maszyn cyfrowych. Jest on też rzędu n2. Jego przewaga

2 Fibonacci, właściwie Filius Bonacci czyli syn Bonacciego. Studiował Al Chorezmiego. W roku 1202 opublikował Liber abaci, w której to książce rozpatrywał m. in. problem rozmnaża-nia się królików, prowadzący do liczb jego imierozmnaża-nia.

(4)

nad poprzednim polega na tym, że jeżeli a i b zapisać w systemie dwójkowym to pewne operacje można wykonywać szybciej. Liczba operacji może spaść o połowę. Zaobserwujmy mianowicie, że nwd(2a,2b)=2nwd(a,b) oraz, że nwd liczb nieparzy-stych jest liczbą nieparzystą, a różnica liczb nieparzynieparzy-stych jest parzysta. Algorytm zaczyna się od zapisania

a = 2ia′ b = 2jb,

gdzie a′, bsą nieparzyste. Stąd nwd(a, b) = 2min(i,j)nwd(a, b). Wyznaczanie

i, j i dzielenie przez 2ilub 2jjest bardzo proste gdy liczby całkowite zapisze się w

postaci dwójkowej. A oto cały algorytm. nwd(a,b):=

START

b = 0 -> a

a = 0 -> b

mod(a,2)=0 i mod(b,2)=0 -> 2*nwd(a/2,b/2)

mod(a,2)=0 -> nwd(a/2,b)

mod(b,2)=0 -> nwd(a,b/2)

a < b -> nwd(b,a)

-> nwd(a-b,b) KONIEC

Procedura ta jest szybsza niż oryginalna wersja algorytmu Euklidesa ponieważ w dwu krokach dzieli przynajmniej jedną z liczb przez dwa. Dzieje się tak nawet wtedy gdy a i b są nieparzyste gdyż odejmując je od siebie dostajemy wynik podzielny przez dwa.

1.4. Złożoność algorytmu podwajania

Literatura: M. Davies, Czym jest obliczanie, w Matematyka Współczesna, Dwana-ście esejów, red. L. A. Steen, WNT, Warszawa, 1983.

Przypomnijmy jak wyglądał algorytm programu Turinga-Posta podwajania liczby jedynek umieszczonych na tasiemce papierowej lub magnetycznej. Jak pracuje pro-gram podwajania?

1 DRUKUJ 0 (* jeśli jest 1 *) 2 IDŹ W LEWO

3 IDŹ DO KROKU 2 JEŚLI PRZECZYTAŁEŚ 1 4 DRUKUJ 1

5 IDŹ W PRAWO

6 IDŹ DO KROKU 5 JEŚLI PRZECZYTAŁEŚ 1 7 DRUKUJ 1

8 IDZ W PRAWO

9 IDZ DO KROKU 1 JESLI PRZECZYTAŁEŚ 1 10 ZATRZYMAJ SIE

Liczbę jedynek podwaja się poprzez ich kopiowanie jedna za drugą. Każda je-dynka jest chwilowo zastępowana przez zero, które odgrywa role znacznika miejsca (krok 1). Dalej obliczenia przesuwaja sie na lewo przez wszystkie jedynki (wraz z tymi, które w procesie obliczeń zostały wydrukowane na nowo), aż do nieuży-wanego (pustego) kwadracika (powtarzane kroki 2, 3). W kroku 4 jedynka zostaje skopiowana. Nastepnie obliczenia idą na prawo aż do napotkania zera, które zajmuje miejsce właśnie skopiowanej jedynki (kroki 5, 6 - powtarzane). Skopiowana jedynka jest ponownie zapisana (krok 7). Obliczenia idą dalej w prawo w poszukiwaniu

(5)

nowej jedynki, którą trzeba skopiowac (krok 8). Jesli istnieje nowa jedynka to obli-czenia przenosza sie do kroku 1. W przeciwnym razie idą do kroku 10 i zatrzymują sie (kroki 9, 10).

Algorytm ten jest wygodny dla celów dyskucji któą chcemy teraz przeprowadzić. Zauważmy, że algorytm ten i każdy jego fragmenty, można powtarzać dowolną liczbę razy dzięki instrukcji IDŹ DO, która powoduje powrót do wcześniej wykona-nego polecenia. Taka sama instrukcja IDŹ DO pozwala też przeskoczyć do przodu grupę poleceń.

Widzieliśmy, że kod zero-jedynkowy tego programu (tablica kodowania jest po-dana poniżej) ma postać 1 000 010 110 110 001 011 110111110 001 011 11010 100 111i pomijając znaki przestankowe: 1 na początku i 111 na końcu oraz niezna-czące znaki puste, które zostawiliśmy dla przejrzystości zapisu, liczy 41 znaków.

Kod Instrukcja 000 DRUKUJ 0 001 DRUKUJ 1 010 IDZ w LEWO 011 IDZ w PRAWO 101 0...0 |{z} i

1 IDZ do KROKU i JESLI PRZECZYTALES 0 110 1...1

|{z}

i

0 IDZ do KROKU i JESLI PRZECZYTALES 1 100 ZATRZYMAJ SIE (stop=100p)

Zastanowimy się teraz nad problemem jak złożony musi być program Turinga-Posta, aby w wyniku jego działania otrzymać dany wynik.

Rozpatrzymy tylko przypadek, gdy na taśmie w chwili zakończenia obliczeń znaj-dują się co najmniej dwie jedynki. Wynik składa się wtedy z ciągu zer i ciągu jedynek położonych pomiędzy skrajnymi jedynkami na taśmie (znakami przestankowymi). Załóżmy, że jako wynik chcemy otrzymać ciąg 1022 jedynek. Jeśli dodamy do tego dwie jedynki ograniczające to taśma w chwili zakończenia obliczeń ma zawierać 1024 jedynki. Reszta taśmy ma być pusta. Możemy wprost zapisać te 1024 jedynki i uważać, że zrobiliśmy co należy. Możemy jednak postąpić lepiej. Wystarczy napisać na taśmie 512 jedynek i uruchomić program podwajania opisany przed chwilą. Jak wiemy, program ten składa się z 41 bitów (bit oznacza z ang. binary digit czyli cyfrę dwójkową). Mamy zatem opis ciągu 1022 jedynek, który używa 41 + 512 = 553 bity. Można postąpić jeszcze lepiej. 1024 = 210, a więc można zastosować program

podwajania dziesięciokrotnie zaczynając od jednej jedynki.

Poniżej (Davies) przedstawiony jest 22 krokowy program, który to robi jeszcze inaczej. Pierwszych 9 kroków pokrywa się z programem podwajania. Proces obli-czeń rozpoczyna się od danych

1 |{z} ↑ 0 11 . . . 1 | {z } n .

Po wykonaniu obliczeń na taśmie znajdzie się blok 2n+1jedynek.

1 DRUKUJ 0 2 IDŹ W LEWO

3 IDŹ DO KROKU 2 JEŚLI PRZECZYTAŁEŚ 1 4 DRUKUJ 1

5 IDŹ W PRAWO

6 IDŹ DO KROKU 5 JEŚLI PRZECZYTAŁEŚ 1 7 DRUKUJ 1

(6)

9 IDŹ DO KROKU 1 JESLI PRZECZYTAŁEŚ 1 10 IDŹ W PRAWO

11 IDŹ DO KROKU 22 JEŚLI PRZECZYTAŁEŚ 0 12 IDŹ W PRAWO

13 IDŹ DO KROKU 12 JESLI PRZECZYTAŁEŚ 1 14 IDŹ W LEWO

15 DRUKUJ 0 16 IDŹ W LEWO

17 IDZ DO KROKU 16 JESLI PRZECZYTAŁEŚ 1 18 IDŹ W LEWO

19 IDZ DO KROKU 18 JESLI PRZECZYTAŁEŚ 1 20 IDŹ W PRAWO

21 IDZ DO KROKU 1 JESLI PRZECZYTAŁEŚ 1 22 ZATRZYMAJ SIE

Opiszmy pokrótce działanie tego programu. Kroki 1-9 to poznany wcześniej program podwajania. Część ta podwaja liczbę jedynek znajdujących się na lewo od zera. Kroki 10-21 wymazują jedną jedynkę znajdującą się na prawo od zera i następuje powrót do kroku 1. Gdy program wymaże wszystkie jedynki znajdujące się na prawo od zera to w kroku 11 zostanie przeczytane zero. Wtedy nastąpi skok do kroku 22 (do przodu) i program się zatrzyma. Liczba jedynek, które znajdują się początkowo na lewo od zera, jest podwajana tyle razy ile jedynek znajduje się po-czątkowo na prawo od tego zera. Kod całego programu zajmuje 155 bitów. (Zadanie: Sprawdzić to przez kodowanie zgodne z poprzednio zdefiniowanym.) Aby otrzymać żądany ciąg 1024 jedynek, potrzebny jest ciąg danych 10111111111. Prowadzi to do 155+11=166 bitów co stanowi znaczną poprawę w stosunku do 553 bitów.

Podamy teraz pewną definicję. Przez w oznaczymy dowolny ciąg bitów. Mó-wimy, że w posiada złożoność n (lub pojemność informacji n, co zapisujemy jako I(w) = n, jeżeli

1. Istnieje program P i ciąg v, takie, że długość kod(P) i długość v wynoszą ra-zem n, oraz takie, że jeśli program P rozpocznie obliczenia na v, to zatrzyma się ostatecznie z wynikiem w (tzn. z ciągiem 1w1) zajmującym niepustą część taśmy oraz

2. Nie istnieje liczba mniejsza od n, dla której to samo zachodzi.

Jeżeli w oznacza ciąg 1022 jedynek, to pokazaliśmy, że I(w) ≤ 166. Ogólnie można pokazać, że dla ciągu bitów (zer i jedynek) o długości n złożoność I(w) ≤ n + 9 W szczególności, jeśli program P składa się tylko z jednej instrukcji: Zatrzymaj sięto ponieważ nic on nie robi, więc rozpoczynając obliczenia z danymi 1w1 za-trzyma się natychmiast i pozostawi na taśmie niezmieniony ciąg 1w1. Ponieważ kod(P)=1100111, więc prawdą jest, że I(w) musi być mniejsze lub równe długości ciągu 11001111w1, tj. mniejsze lub równe n+9. Liczba 9 wynika z przyjętej metody kodowania i nie posiada żadnego teoretycznego znaczenia.

Policzymy teraz ciągi o długości n takie, że I(w) ≤ n−10? (Zakładamy n > 10.) Każdy taki ciąg jest związany z programem P i ciągiem danych v takich, że kod(P)v jest ciągiem bitów o długości mniejszej lub równej n − 1. Ponieważ łączna liczba ciągów bitów o długości i wynosi 2i, to istnieje tylko

2 + 4 + . . . + 2n−10

ciągów bitów o długości ≤ n − 10. Jak łatwo policzyć jest to liczba równa 2n−9− 2

(suma ciągu geometrycznego). Ponieważ istnieje 2n ciągów bitów o długości n to

stosunek liczby ciągów o długości n i o złożoności ≤ n − 10, do liczby wszystkich ciągów o długości n, nie przekracza liczby

2n−9 2n = 1 29 < 1 500.

(7)

Jest to mniej niż 0.2%, a więc więcej niż 99.8% wszystkich ciągów o długości n ma złożoność I(w) > n − 10.

Literatura: W. M. Turski, Propedeutyka informatyki, Warszawa, 1989

n - charakterystyczny rozmiar zadania. Często, zamiast trudnej do ustalenia dokładnej złożoności obliczeniowej wprowadza się oszacowanie w postaci pewnej funkcji f(n), takiej że ∃n0∀n > n0{LA(n) ≤ f(n)}. W roli f(n) występują takie

funkcje jak n, n3/2. . . . , log n, n log n, . . . , 2n,. . . , pomnożone czasami przez stałe

współczynniki. Ponieważ zależy nam na tym by jak najlepiej oszacować złożoność obliczeniową danego zadania, to spośród funkcji, które można przyjąć za oszacowa-nie wybieramy tę, której wzrost wraz z n jest najwoloszacowa-niejszy. Jeżeli od pewnego n0

zachodzi LA≤ n, a od pewnego n1mamy LA≤ n2to za f(n) bierzemy n.

Mówimy: złożoność obliczeniowa jest rzędu f(n) (np. mnożenie liczb posiada złożoność rzędu n2, a dodawanie rzędu n).

Znajomość złożoności obliczeniowej danego algorytmu pozwala oszacować czas wykonywania zadania.

Przykład. Jak już wiemy, ciągiem Fibonacciego nazywamy ciąg liczb zadany zależ-nościami:

f1= 0 , f2= 1 , fi= fi−1+ fi−2, dla i > 2. (1)

Problem polega na znalezieniu n-tego wyrazu tego ciągu. Za rozmiar charaktery-styczny można przyjąć numer n wyrazu.

Algorytm 1 Wykorzystujemy bezpośrednio związek rekurencyjny (!) (1). bliczamy f1, f2 i dalej kolejne wyrazy ciągu, aż do chwili gdy obliczymy interesujący nas

wyraz n-ty. Wykonanie jednego kroku polega na wykonaniu jednego dodawania. Złożoność algorytmu jest więc rzędu n.

Algorytm 2 (patrz: Turski) Można sprawdzić (przez indukcję), że

f2k = f2k+ f2k+1

f2k+1 = (2fk+ fk+1)fk+1. (2)

Rozwinięcie dwójkowe liczby n jest:

n = cl2l+ cl−12l−1+ . . . + c12 + c0, (3)

gdzie każde cijest równe 0 lub 1. Krok przygotowawczy algorytmu polega na

zna-lezieniu współczynników rozwinięcia (3) i ustaleniu bazy dalszych obliczeń b1= f1= 0 , b2= f2= 1 .

W kolejnych krokach (i = 1, . . . , l) postępujemy zgodnie z następującym schema-tem.

1. Z wzorów (2) obliczamy nową bazę

b1= f2k, b2= f2k+1,

gdzie numery k i k + 1 są numerami wyrazów ciągu Fibonacciego uznanych za ppoprzednią bazę.

2. Sprawdzamy jaki jest współczynnik cl−i rozwinięcia dwójkowego (3). Jeśli

cl−i= 0to kończymy krok i-ty, w przeciwnym wypadku obliczamy nową bazę

b1= f2k+1, b2= f2k+2,

wykorzystując związek

(8)

(patrz definicja ciągu Fibonacciego (1)). Znaleziona baza stanowi podstawę wy-konania następnego kroku ((i+1) -szego). Jeśli i = l to algorytm jest zakończony, a obliczony w nim pierwszy element bazy, b1jest poszukiwanym wyrazem ciągu

kn.

Przykład. Znajdziemy f23. Mamy 23 = 1 · 24+ 0· 23+ 1· 22+ 1· 21+ 1, a więc

c4= 1 , c3= 0 , c2= 1 , c1= 1 , c0= 1oraz l = 4. A oto kolejne kroki.

Krok 0 b1= f1= 0 , b2= f2= 1 Krok 1 b1= f2= f21+ f22= 1 , b2= f2= (2f1+ f2)f2= 1 c3= 0 . Krok 2 b1= f4= f22+ f23= 2 , b2= f5= (2f2+ f3)f3= 3 . c2= 1 , f6= f5+ f4= 5 , b1= f5= 3 , b2= f6= 5 . Krok 3 b1= f10= f25+ f26= 34 , b2= f11= (2f5+ f6)f6= 55 , c1= 1 , f12= f11+ f10= 89 , b1= f11= 55 , b2= f12= 89 . Krok 4 b1= f22= f211+ f212= 10946 , b2= f23= (2f11+ f12)f12= 17711 , c0= 1 , f24= f23+ f22= 28657 , b1= f23= 17711 , b2= f24= 28657 . Wynik f23(= b1) = 17711.

Zadanie 1. Złożonośc algorytmu 2

3

Pokazać metodą indukcji słuszność algorytmu 2 znajdowania wyrazów ciągu

Fibonacciego. [Spis]

Złożoność obliczeniowa algorytmu 2 jest rzędu log n, a więc jest dużo mniej-sza od złożoności obliczeniowej algorytmu 1. Wykonanie algorytmu 2 przebiega w l-krokach, przy czym l ≈ log2n, a na każdym kroku wykonuje się 4 mnożenia

i 3 dodawania. Czynności wstępne polegające na znalezieniu rozwinięcia liczby n wymagają również rzędu log2noperacji, bo wykonuje się je znajdując reszty z l + 1 dzieleń przez dwa liczby n. Stąd liczba wszystkich operacji jest rzędu 8 log2nDo tego trzeba dodać działania organizacyjne związane z przezywaniem bazy itd. Złożo-ność algorytmu drugiego jest więc znacznie mniejsza niż pierwszego. Jedyna kom-plikacja, która pojawia się w algorytmie 2, polega na zwiększeniu liczby obiektów, na których operuje algorytm. Jest to często koszt jaki należy ponieść przy zmniejszaniu liczby operacji.

(9)

1.4.1. Jeszcze o złożoności. Przeszukiwanie tablic.

Rozpatrzmy problem znajdowania numeru elementu X z tablicy A[1..N]. Sche-mat Nassi-Schneidermana algorytmu wygląda następująco:

+---+ | i:=1; | |---| | X <> A[i] | | | | +---| | | i:=i+1; | | | | +---| | Wypisz(i); | +---+

Ile razy wykonywana jest instrukcja dopóki-dopóty? Jeśli nic nie wiemy o ciągu elementów z A to pętla wykonuje się "jakąśłiczbę razy, która zawarta jest między zerem, a N − 1. Średnia liczba (przy dużej liczbie przebiegów programu) jest równa (N − 1)/2 = N/2 − 1/2 ≈ N/2. Algorytm, który tu zastosowaliśmy jest liniowy. Czas jego wykonania jest liniową funkcją N.

Załóżmy teraz, że elementy A są uporządkowane rosnąco. Algorytm przeszu-kiwania binarnego polega na tym, że proces przeszuprzeszu-kiwania rozpoczynamy od po-równania X z elementem środkowym tablicy. Jeśli są one jednakowe to proces koń-czy się. Jeśli X jest mniejszy od elementu środkowego powtarzamy przeszukiwanie w tablicy A[1..N/2]. Jeśli X jest większy od elementu środkowego przeszukujemy część górną: A[N/2 + 1..N]. Czynimy to aż znajdziemy numer X. Liczba połowień jest w przybliżeniu równa log2N. Algorytm przeszukiwania binarnego jest więc algorytmem logarytmicznym. Przedstawmy to w tablicy. N N/2 ⌈log2N 10 5 4 100 50 7 1 000 500 10 10 000 5 000 14 100 000 50 000 17 1 000 000 500 000 20

Wielkość ⌈log2N⌉ oznacza funkcję sufit, która daje wartość argumentu zaokrągloną

do najbliższej większej liczby całkowitej.

Widzimy, że dla małych N różnica czasów jest niewielka, lecz dla dużych rośnie ona bardzo szybko. Algorytm N/2 nie ma szans przy N rzędu miliona. Złożoność opisanych algorytmów oznaczamy przez Θ(N) oraz Θ(log N).

Zadanie 2. Grafiki złożoności

2

Narysować grafiki Θ(f(N)) dla algorytmów: stałego Θ(k), logarytmicznego Θ(log N), liniowego Θ(N), N log N, kwadratowego Θ(N2), sześciennego

Θ(N3)i eksponencjalnego Θ(2N)dla N=1. . . 200. [Spis]

Przykład. Załóżmy, że mamy wybrać algorytm sortowania. Mamy do dyspozycji algorytm A, którego czas porządkowania wynosi 5N2µsi algorytm B, którego czas

pracy jest dany formułą: 50N log Nµs. Czynnik 50N przed logarytmem wskazuje, że algorytm B jest bardziej skomplikowany niż A, który przed N2ma czynnik 5.

(10)

Tablica 5N2µs 50Nlog Nµs 10 0.0005 s 0.002 s 100 0.05 s 0.03 s 1 000 5 s 0.5 s 10 000 500 s 6.6 s 100 000 50 000 s=14 h 83 s 1 000 000 5× 106s = 58 d 993 s=17 min

Zadanie 3. Arytmetyka Θ

2

Można udowodnić słuszność następujących reguł arytmetyki Θ: Θ(N) + c = Θ(N) ,

Θ(cN) = Θ(N) ,

Θ(N) + Θ(M) = Θ(M) + Θ(N) , Θ(N) + Θ(M) = Θ(M) , M≤ N .

W wyrażeniach poniżej wstawić w miejscu znaku ? znak >, < lub =. 1. Θ(5) ? Θ(23) 2. Θ(N + M) ? Θ(N − 15/2) 3. Θ(log N) ? Θ(N) 4. Θ(N2+ N)? Θ(N3) 5. Θ(N log N + N3)? Θ(2N) 6. Θ(2N)? Θ(5N) 7. Θ(log N) ? Θ(log10N) [Spis]

1.4.2. I jeszcze . . . sortowanie przez wstawianie

Literatura: Cormen, Leiserson, Rivest, Wprowadzenie do algorytmów, WNT, War-szawa, 1998, wydanie II.

Rozważmy algorytm porządkowania ciągu liczb zwany sortowaniem przez wsta-wianie.

Problem.

Dane wejściowe: Ciąg n liczb: A =< a1, a2, a3, . . . an > .

Wynik: Spermutowany ciąg < a′

1, a2′, a3′, . . . an′ >otrzymany z A taki, że a1′ ≤

a′

2≤ a3′. . .≤ an′.

Sortowania dokonamy za pomocą następującego algorytmu. Sort_wstaw(A)

1 for j <- 2 to length[A] 2 do klucz <- A[j]

3 (* wstaw A[j] w posortowany ciąg A[1, 2, ... , n] *) 4 i <- j-1

5 while i>0 and A[i]>klucz 6 do A[i+1] <- A[i]

7 i <- i-1

8 A[i+1] <- klucz

Poniższy, konkretny przykład zilustruje działanie programu.

Przykład. Niech A =< 5, 2, 4, 6, 1, 3 >. Działanie algorytmu Sort_wstaw sprowa-dza się do wykonania następujących kroków.

(11)

5 2 ¯ 4 6 1 3 2 5 4 ¯ 6 1 3 2 4 5 6 1 3 2 4 5 6 ¯ 1 3 2 4 5 6 1 ¯ 3 1 2 4 5 6 3 ¯ 1 2 3 4 5 6

Pozycje o indeksie i zostały wyróżnione

¯ . W każdym następnym kroku algorytm powoduje przeniesienie wyróżnionej liczby w odpowiednie miejsce w ciągu.

Czas działania algorytmu zależy od liczby elementarnych operacji potrzebnych do jego wykonania. W celu oszacowania go założymy, że czas wykonania pojedyn-czego wiersza algorytmu jest zawsze jednakowy i wynosi ci. Przez tj oznaczymy

liczbę sprawdzeń warunku wejścia do pętli while. Możemy sporządzić następującą tablicę kosztów.

Instrukcja (wiersz) Koszt Liczba wykonań

1 for j <- 2 to length[A] c1 n

2 do klucz <- A[j] c2 n − 1

3 (* ... *) c3= 0 n − 1

4 i <- j-1 c4 n − 1

5 while i>0 and A[i]>klucz c5 Pnj=2tj

6 do A[i+1] <- A[i] c6 Pnj=2(tj− 1)

7 i <- i-1 c7 Pnj=2(tj− 1)

8 A[i+1] <- klucz c8 n − 1

Czas działania całego algorytmu jest sumą czasów cząstkowych. Θ(n) = c1n + c2(n − 1) + 0(n − 1) + c4(n − 1) + c5 n X j=2 tj +c6 n X j=2 (tj− 1) + c7 n X j=2 (tj− 1) + c8(n − 1) .

Oszacujmy minimalny czas działania algorytmu. Dzieje się to w przypadku posor-towanego ciągu A. Dla każdego j = 2, 3, . . . , n w wierszu 5 mamy wtedy A[i] ≤ kluczdla każdego początkowego i równego j − 1. Zatem tj= 1dla j = 2, 3, . . . , n

i minimalny czas działania jest równy

Θ(n) = c1n + c2(n − 1) + c4(n − 1) + c5(n − 1) + c8(n − 1)

= (c1+ c2+ c4+ c5+ c8)n − (c2+ c + c4+ c5+ c8)

= an + b ,

gdzie a i b są stałymi. Widzimy, że Θ(n) jest liniową funkcją n, liczby elementów ciągu.

Najgorsza sytuacja ma miejsce gdy tablica jest posortowana odwrotnie. Musimy wtedy porównać każdy element A[j] tablicy z elementami podtablicy A[1 . . . j − 1], a więc tj= jdla j = 2, 3, . . . , n. Ponieważ

n X j=2 j = n(n + 1)/2 − 1 , n X j=2 (j − 1) = n(n − 1)/2 ,

(12)

więc Θ(n) = c1n + c2(n − 1) + c4(n − 1) + c5[n(n + 1) − 1] +c6[n(n − 1)/2] + c7[n(n − 1)/2 + c8(n − 1) = 1/2(c5+ c6+ c7)n2 +(c1+ c2+ c4+ c5/2 − c6/2 + c7/2 + c8)n − (c2+ c4+ c5+ c8) = an2+ bn + c .

Tym razem otrzymaliśmy funkcję kwadratową względem n.

W realnych przypadkach czas wykonania algorytmu leży gdzieś pomiędzy cza-sem optymistycznym i pesymistycznym. Jest on najczęściej zbliżony do czasu pesy-mistycznego.

Odrzucając w Θ(n) wszystkie człony oprócz wiodącego otrzymamy to co nazy-wamy złożonością algorytmu. Dla naszego przypadku mamy więc złożoność Θ(n2).

1.4.3. . . . sortowanie przez scalanie

W przypadku sortowania przez wstawianie stosowaliśmy metodę przyrostową: mając posortowaną tablicę A[1 . . . j − 1] wstawialiśmy pojedynczy element A[j] we właściwe miejsce otrzymując większą tablicę A[1...j].

Rozpatrzymy teraz inną metodę zwaną dziel i zwyciężaj. Algorytmy, które sto-sują tę metodę mają najczęściej strukturę rekurencyjną. W metodzie tej problem dzielony jest na mniejsze problemy podobne do poprzedniego, problemy te są roz-wiązywane rekurencyjnie, a następnie ich rozwiązania są łączone w rozwiązanie pełnego problemu.

Opis algorytmu sortowania przez scalanie jest następujący.

— Dzielimy ciąg n elementowy na dwa podciągi o n/2 elementach każdy (DZIEL). — Sortujemy je rekurencyjnie przez scalanie (ZWYCIĘŻAJ).

— Łączymy posortowane podciągi w jeden posortowany ciąg (POŁĄCZ).

W kroku trzecim potrzebna będzie procedura, która łączy dwa podciągi posorto-wane A[p . . . q] i A[q+1 . . . r] w jeden A[p . . . r]. Nazwiemy ją Merge. Jej algorytm wygląda tak Merge(A, p, q, r) 1 i <- p; j <- q+1; (* wskaźniki do el A *) 2 while i<= q or j <= r 3 do 4 if C <= A[j] 5 i <- i+1 6 else przesuń(A, i, j) 7 A[i] <- C 8 i <- i+1 9 j <- j+1

gdzie przesun(A,i,j) przemieszcza elementy części tablicy A[i..j-1] w prawo o 1 miejsce. W powstałą lukę wstawiana jest następnie zapamiętana wcześniej wartość C (niszczona w procesie przesuwania). Np.

for l:=j downto i+1 do A[l]:=A[l-1] .

A oto dpowiedni program w Pascalu.

procedure Merge(var A: List; p, q, r: integer);

(*

Sklada dwie uporzadkowane czesci tablicy A w jedna uporzadkowana calosc;

A[p..q] -lewa; A[q+1..r] - prawa *)

(13)

var i, j, l: integer;

C: integer;

begin

i:=p; (* wskaznik do czesci lewej *) j:=q+1; (* wskaznik do czesci prawej *)

while (i <= p) or (j <= r) do begin

C := A[j];

if A[i] <= C then begin

i := i+1;

end else begin

(* przesun w prawo o 1 poczynajac od i *)

for l:=j downto i+1 do A[l]:=A[l-1];

A[i]:=C; i:=i+1; j:=j+1; end; end; end;

Procedura Merge działa w miejscu, tzn korzysta tylko z tablicy A. Dlatego po-trzebujemy przesuwania, które zajmuje sporo (!) czasu obliczeń. Efektywnywniej-szy czasowo, ale rozbudowany algorytm otrzymamy jeśli wyniki scalania będziemy umieszczać w posredniej tablicy B.

procedure Merge(var A, B: List; p, q, r: integer);

(*

Sklada dwie uporzadkowane czesci tablicy A w jedna uporzadkowana calosc B[p..r]; A[p..q] - lewa; A[q+1..r] - prawa *)

var i, j, k: integer; begin

i:=p; (* wskaznik do czesci lewej *) j:=q+1; (* wskaznik do czesci prawej *) k:=1; (* wskaznik do el. tablicy B *)

while (i <= q) and (j <= r ) do begin if A[i] < A[j] then begin

B[k]:=A[i]; i := i+1; end else begin B[k] := A[j]; j:=j+1; end; k:=k+1; end;

(* Kopiowanie uporzadkowanej reszty tablicy A *)

if i > q then (* wyczerpano liste z lewej; przepisuj prawa czesc *) repeat B[k] := A[j]; j := j+1; k := k+1; until k>r else

(14)

repeat B[k] := A[i]; i := i+1; k := k+1; until k>q; end;

Następną procedurą jest Merge_Sort(A, p, r). Procedura ta sortuje elementy w podtablicy A[p . . . q]. Kiedy p ≥ r podciąg jest posortowany. W przeciwnym razie znajdujemy q, które dzieli ją na dwie podtablice A[p . . . q] o ⌈n/2⌉ elementach i drugą A[q + 1 . . . r] o ⌊n/2⌋ elementach.3

Merge_Sort(A, p, r) 1 if p<r 2 then q -> floor(p+r)/2 3 Merge_Sort(A, p, q) 4 Merge_Sort(A, q+1, r) 5 Merge(A, p, q, r)

W celu posortowania ciągu A =< A[1], A[2], . . . , A[n] > należy wywołać pro-cedurę Merge_Sort(A, 1, length(A)) gdzie length(A) oznacza długość A i wynosi n.

Drzewo sortowań (przypadek n = 2m, gdzie m=3) pokazane jest na rysunku.

5 2 4 6 1 3 2 6 2 5 4 6 1 3 2 6 2 4 5 6 1 2 3 6 1 2 2 3 4 5 6 6 scalanie scalanie scalanie scalanie scalanie scalanie scalanie CIAG POCZATKOWY CIAG KONCOWY

1.4.4. Złożoność algorytmu sortowania przez scalanie

Algorytm sortowania przez scalanie jest algorytmem rekurencyjnym. Jego czas działania można więc zapisać jako rekurencyjną zależność zawierającą czas realizacji podproblemów mniejszych.

Jeśli problem został podzielony na a mniejszych podproblemów o rozmiarach nn/bkażdy to czas jego wykonania jest dany zależnością

T (n) =

Θ(1) dla n ≤ c

aT (n/b) + D(n) + S(n) dla n > c

Tutaj Θ(1) jest w przybliżeniu stałym czasem wykonania elementarnego podpro-blemu o rozmiarze n ≤ c, D(n) jest czasem dzielenia propodpro-blemu, a S(n) jest czsem scalania podproblemów.

Dla algorytmu Sort (przy założeniu, że n = 2m) mamy D(n) = Θ(1), S(n) =

Θ(n), a T(n) = Θ(1) dla n = 1 oraz T(n) = Θ(n/2) + Θ(n) dla n > 1. Można pokazać, że rozwiązaniem tego problemu rekurencji jest

T (n) = Θ(nlog2n) .

Ponieważ algorytm sortowania przez wstawianie ma czas pesymistyczny Θ(n2),

wiąc widzimy, że algorytm sortowania przez scalanie jest szybszy gdyż Θ(nlog2n) < Θ(n2) .

3 Wielkość ⌈x⌉ oznacza najmniejszą liczbę całkowitą nie mniejszą niż x (ceiling(x) lub sufit(x)), a ⌊x⌋ oznacza największą liczbę całkowitą nie większą niż x (floor(x) lub podłoga(x)).

(15)

Koniec.

Zadanie 4. Złożoność rekurencji

3

Pokazać stosując indukcję matematyczną, że rozwiązaniem równania rekuren-cyjnego

T (n) =

Θ(1) dla n = 2

2T (n/2) + n dla n = 2m, m > 1 ,

jest T(n) = n log2n. [Odp.][Spis]

Zadanie 5. Złożoność sortowania przez wstawianie

3

Napisz równanie rekurencyjne na czas sortowania przez wstawianie. [Spis]

Zadanie 6. Porównanie złożoności

1

Jaka jest najmniejsza wartość n, dla której algorytm wykonujący 100n2operacji

działa szybciej niż algorytm wykonujący 2n operacji na tym samym

kompute-rze? [Spis]

Problem.

Dla funkcji f(n) i czasu t z poniższej tabeli wyznacz największy rozmiar n pro-blemu, który może być rozwiązany w czasie t, zakładając, że algorytm działa w czasie f(n)µsekund.

f \ t 1 sec 1 min 1 godz 1 dzień 1 miesiąc 1 rok 1 wiek

log2n ? ? ? ? ? ? ? √ n ? ? ? ? ? ? ? n ? ? ? ? ? ? ? nlog2n ? ? ? ? ? ? ? n2 ? ? ? ? ? ? ? n3 ? ? ? ? ? ? ? 2n ? ? ? ? ? ? ? n! ? ? ? ? ? ? ?

Obraz

Tablica 5N 2 µs 50N log Nµs 10 0.0005 s 0.002 s 100 0.05 s 0.03 s 1 000 5 s 0.5 s 10 000 500 s 6.6 s 100 000 50 000 s=14 h 83 s 1 000 000 5 × 10 6 s = 58 d 993 s=17 min Zadanie 3

Cytaty

Powiązane dokumenty

Niech s n oznacza ilość ciągów binarnych długości n, które zawierają parzystą liczbę jedynek i każde dwie jedynki rozdzielone są przynajmniej jed- nym zerem.. Niech s n

SPŁYW - szybkie przemieszczanie się masy gruntowej bez wytworzenia wyraźnej powierzchni poślizgu przy współudziale wody np.. spływy

Ponieważ wyrazy szeregu dążą do zera, jego zbieżność (i sumę) można zbadać rozważając tylko co 101-szą sumę częściową. Wśród poniższych sześciu szeregów wskaż

[r]

Projekt jest to przedsięwzięcie, na które składa się zespół czynności, które charakteryzują się tym, że mają:.. 

Wybierz P, jeśli zdanie jest prawdziwe, albo F - jeśli jest fałszywe... Wybierz właściwa odpowiedź

Głównym celem mojej pracy doktorskiej jest zbadanie dynamiki kondensatu Bosego- Einsteina w temperaturze większej od zera bezwzględnego w układach, w których frak- cja

Zazwyczaj też konstruktor nie przyjmuje żadnych parametrów, co nie znaczy jednak, że nie może tego czynić. Dokładniej wyjaśnimy to w następnym paragrafie. Warto też wiedzieć,