Wstęp do programowaia Wykład 13 Algorytmy i ich aaliza Jausz Szwabiński Pla wykładu: Co to jest algorytm? Aaliza algorytmów Notacja dużego O Przykład: aagramy Struktury daych w Pythoie i ich wydajość Literatura uzupełiająca: http://iteractivepytho.org/ruestoe/static/pythods/algorithmaalysis/toctree.html (http://iteractivepytho.org/ruestoe/static/pythods/algorithmaalysis/toctree.html) Co to jest algorytm?
Algorytm to skończoy ciąg jaso zdefiiowaych czyości, koieczych do wykoaia pewego rodzaju zadań. Iymi słowy to przepis a rozwiązaie problemu lub osiągięcie jakiegoś celu. Na wcześiejszych wykładach mieliśmy już doczyieia z wieloma algorytmami, m.i.: sortowaie bąbelkowe wyliczaie fukcji silia geerowaie wielomiaów Hermite'a sprawdzaie podzielości liczby całkowitej przez 3 Istieje kilka różych metod zapisu algorytmu. Załóżmy, że aszym zadaiem jest obliczeie fukcji f(0) = 0 przy założeiu, że. f(x) = x x Słowy opis algorytmu 1. dla liczb ujemych, więc, 2. dla liczb dodatich, więc, x = 0 3. jeśli, to z defiicji $f(0)=0. x = x f(x) = x/( x) = 1 x = x f(x) = x/x = 1 Opis słowy czasami da się w prosty sposób wyrazić wzorem matematyczym: f(x) = 1, 0, 1, x < 0 x = 0 x > 0 Lista kroków 1. Wczytaj wartość daej. 2. Jeśli, to. Zakończ algorytm. 3. Jeśli, to. Zakończ algorytm. x x > 0 f(x) = 1 x = 0 f(x) = 0 x < 0 f(x) = 1 4. Jeśli, to. Zakończ algorytm. Schemat blokowy
Poszczególe elemety a powyższym schemacie mają astępujące zaczeie: Drzewo algorytmu
I [1]: x = float(iput("podaj x: ")) if x > 0: prit("f(x)=1") elif x < 0: prit("f(x)=-1") else: prit("f(x)=0") Podaj x: 3 f(x)=1 Aaliza algorytmów Często bywa tak, że te sam algorytm zaimplemetoway jest a wiele różych sposobów. Nasuwa się wtedy pytaie, która z implemetacji jest lepsza od pozostałych. Dla przykładu porówajmy dwa programy: I [2]: def sumofn(): thesum = 0 for i i rage(1,+1): thesum = thesum + i retur thesum prit(sumofn(10)) 55 I [3]: def foo(tom): fred = 0 for bill i rage(1,tom+1): barey = bill fred = fred + barey retur fred prit(foo(10)) 55
Mimo, że a pierwszy rzut oka tego ie widać, oba robią to samo sumują liczby od do, i a dodatek robią to w te sam sposób. Mimo to powiemy, że pierwszy program jest lepszy ze względu a czytelość. Ogólie rzecz biorąc, aby dokoać porówaia między programami, musimy zdefiiować odpowiedie kryteria. Oprócz czytelości mogą to być: liczba operacji iezbędych do wykoaia "zasobożerość" wydajość czas wykoaia Aaliza algorytmów zajmuje się właśie ich porówywaiem pod względem akładów obliczeiowych iezbędych do uzyskaia rozwiązaia. Wróćmy jeszcze raz do pierwszego z powyższych programów i zmodyfikujmy go tak, aby wyliczał jeszcze czas sumowaia: I [4]: import time def sumofn2(): start = time.time() thesum = 0 for i i rage(1,+1): thesum = thesum + i ed = time.time() retur thesum,ed-start Wyiki pięciu wywołań fukcji sumofn2dla = 10000 są astępujące: 1 I [5]: for i i rage(5): prit("suma wyosi %d, czas wykoaia: %10.7f sekud"%sumofn2(10000)) Suma wyosi 50005000, czas wykoaia: 0.0075047 sekud Suma wyosi 50005000, czas wykoaia: 0.0018022 sekud Suma wyosi 50005000, czas wykoaia: 0.0013335 sekud Suma wyosi 50005000, czas wykoaia: 0.0012643 sekud Suma wyosi 50005000, czas wykoaia: 0.0014412 sekud Czasy wykoaia poszczególych wywołań różią się iezaczie od siebie (zależą od chwilowego obciążeia komputera), jedak rząd wielkości pozostaje te sam. Zobaczmy, co staie się, jeżeli zwiększymy o jede rząd: I [6]: for i i rage(5): prit("suma wyosi %d, czas wykoaia: %10.7f sekud"%sumofn2(100000)) Suma wyosi 5000050000, czas wykoaia: 0.0199480 sekud Suma wyosi 5000050000, czas wykoaia: 0.0153518 sekud Suma wyosi 5000050000, czas wykoaia: 0.0166814 sekud Suma wyosi 5000050000, czas wykoaia: 0.0098536 sekud Suma wyosi 5000050000, czas wykoaia: 0.0087562 sekud
I zowu, czasy wykoaia są dość podobe do siebie, jedak w porówaiu z poprzedim przykładem wzrosły miej więcej dziesięciokrotie. Dla jeszcze większego otrzymamy: I [7]: for i i rage(5): prit("suma wyosi %d, czas wykoaia: %10.7f sekud"%sumofn2(1000000)) Suma wyosi 500000500000, czas wykoaia: 0.0854867 sekud Suma wyosi 500000500000, czas wykoaia: 0.0557914 sekud Suma wyosi 500000500000, czas wykoaia: 0.0546665 sekud Suma wyosi 500000500000, czas wykoaia: 0.0569293 sekud Suma wyosi 500000500000, czas wykoaia: 0.0577178 sekud Widzimy, że czas wykoaia poowie wzrósł. Zauważmy teraz, że program sumofn2wylicza sumę częściową ciągu arytmetyczego o różicy i wyrazie początkowym 1. Korzystając z własości ciągu sumę tę możemy wyliczyć przy pomocy wyrażeia: i = i=1 ( + 1) 2 r = 1 I [8]: def sumofn3(): start = time.time() thesum = *(+1)/2 ed = time.time() retur thesum,ed-start I [9]: for i (10000,100000,1000000,10000000): prit("suma wyosi %d, czas wykoaia: %10.7f sekud"%sumofn3()) Suma wyosi 50005000, czas wykoaia: 0.0000036 sekud Suma wyosi 5000050000, czas wykoaia: 0.0000019 sekud Suma wyosi 500000500000, czas wykoaia: 0.0000014 sekud Suma wyosi 50000005000000, czas wykoaia: 0.0000012 sekud Aalizując te wyiki dochodzimy do dwóch wiosków: 1. sumofn3wykouje się dużo szybciej iż sumofn2 2. w przypadku sumofn3czas wykoaia fukcji iezaczie zależy od
Notacja dużego O Aby sformalizować powyższe wioski, potrzebujemy miary, która pozwoli a scharakteryzować czas wykoywaia algorytmu w zależości od wielkości daych wejściowych, i iezależie of typu komputera i użytego języka programowaia. Do scharakteryzowaia czasu wykoaia algorytmu waże jest określeie liczby operacji i/lub kroków iezbędych do jego zakończeia. Jeżeli a każdą z tych operacji potraktujemy jako podstawową jedostkę obliczeiową, wówczas możemy wyrazić czas wykoaia algorytmu poprzez liczbę iezbędych operacji. Wybór podstawowej jedostki ie jest łatwy i zależy od tego, jak algorytm jest zaimplemetoway. W przypadku powyższych fukcji dobrym kadydatem a jedostę podstawową jest liczba przypisań iezbędych do wyliczeia sumy. I tak w przypadku fukcji sumofn2mamy przypisań, atomiast w przypadku sumofn3 tylko jedo. Moża pójść krok dalej i stwierdzić, że dokłada liczba operacji ie jest waża, a to, co się liczy, to domiująca składowa, bo dla dużych pozostałe składiki i tak są zaiedbywale. Iymi słowy, iteresuje as tylko asymptotycze tempo wzrostu czasu wykoywaia algorytmów. Do zapisu tego tempa służy tzw. otacja dużego O: f g T() T() = + 1 f() = O(g()) C f() C g() Niech i będą ciągami liczb rzeczywistych. Piszemy wtedy, gdy istieje stała dodatia taka, że dla dostateczie dużych wartości. służy do zapisu szybkości wzrostu azywaa jest oa czasami złożoością teoretyczą algorytmu defiicja dopuszcza, aby ierówość ie była spełioa dla pewej liczby małych wartości w praktyce ozacza ciąg, którym właśie się zajmujemy (p. góre ograiczeie czasu f() działaia algorytmu), a jest prostym ciągiem o zaej szybkości wzrostu otacja ie podaje dokładego czasu wykoaia algorytmu pozwala odpowiedzieć a pytaie, jak te czas będzie rósł z rozmiarem daych wejściowych Przydate własości: g() 1 log 2 W hierarchii ciągów,,, 4, 3,,, l,,,,,,, każdy z ich jest O od wszystkich ciągów a prawo od iego Jeśli i jest stałą, to W praktyce stwierdzeie, że algorytm jest klasy 2 3 2! f() = O(g()) c c f() = O(g()) Jeśli f() = O(g()) i h() = O(g()), to f() + h() = O(g()) Jeśli f() = O(a()) i g() = O(b()), to f() g() = O(a() b()) Jeśli a() = O(b()) i b() = O(c()), to a() = O(c()) O(a()) + O(b()) = O(max{ a(), b() }) O(a()) O(b()) = O(a() b()) O( 3 ) ozacza, że dla daych wejściowych o wielkości 3 (p. układ rówań liiowych zmieych) czas wykoaia algorytmu jest proporcjoaly do.
Zobaczmy, jak wypada porówaie czasów wykoaia algorytmów różych typów przy założeiu, że dla = 1 każdy z ich wykouje się s: O( ) O ( 2 ) O ( 3 ) O ( 2 ) 10 6 10 20 30 40 50 60 0,00001s 0,00002s 0,00003s 0,00004s 0,00005s 0,00006s 0,0001s 0,0004s 0,0009s 0,0016s 0,0025s 0,0036 0,001s 0,008s 0,027s 0,064s 0,125s 0,216s 0,001s 1,048s 17,9mi 12,7di 35,7lat 366w O ( 3 ) 10 6 10 13 O(!) 0,059s 58mi 6,5lat 3855w 227* w 1,3* w 10 16 10 32 10 47 10 66 3,6s 771w 8,4* w 2,6* w 9,6* w 2,6* w
Przykład aagramy Aagram ozacza wyraz, wyrażeie lub całe zdaie powstałe przez przestawieie liter bądź sylab iego wyrazu lub zdaia, wykorzystujące wszystkie litery (głoski bądź sylaby) materiału wyjściowego: kebab babek Gregory House Huge ego, sorry "Quid est veritas?" (Co to jest prawda?, Piłat) Jezus) "Vir est qui adest" (Człowiek, który stoi przed tobą, Testowaie, czy łańcuchy zaków są aagramami, to przykład zagadieia, które moża rozwiązać algorytmami o różym tempie asymptotyczego wzrostu, a jedocześie a tyle prostego, aby moża było o tym opowiedzieć w ramach kursu ze wstępu do programowaia. Rozwiązaie 1: "odhaczaie" liter Jedym z możliwych rozwiązań aszego problemu jest sprawdzeie, czy litera z jedego łańcucha zajduje się w drugim. Jeżeli tak, "odhaczamy" ją i powtarzamy czyość dla pozostałych liter. Jeżeli po zakończeiu tej operacji wszystkie litery w drugim łańcuchu zostaą "odhaczoe", łańcuchy muszą być aagramami. Odhaczaie możemy zrealizować poprzez zastąpieie odalezioej litery wartością specjalą Noe. I [10]: def aagramsolutio1(s1,s2): alist = list(s2) #zamień drugi łańcuch a listę pos1 = 0 stillok = True while pos1 < le(s1) ad stillok: pos2 = 0 foud = False while pos2 < le(alist) ad ot foud: if s1[pos1] == alist[pos2]: #sprawdzamy literę s1[pos1] foud = True else: pos2 = pos2 + 1 if foud: alist[pos2] = Noe else: stillok = False #przerwij, jeśli litery ie ma pos1 = pos1 + 1 #pozycja astępej litery w łańcuchu s1 retur stillok prit(aagramsolutio1('abcd','dcba')) True
s1 s2 Dla każdego zaku z łańcucha musimy wykoać iterację po maksymalie elemetach listy. Każda pozycja w liście s2 musi zostać odwiedzoa raz w celu odhaczeia. Dlatego całkowita liczba wizyt elemetów listy s2 jest sumą liczb aturalych od 1 do : ( + 1) 1 1 i = = + 2 2 2 2 i=1 1 2 2 1 2 O( 2 ) Dla dużych wyraz będzie domiował ad. Dlatego algorytm jest klasy. Rozwiązaie 2: sortowaie i porówywaie s1 s2 Zauważmy, że jeżeli i są aagramami, muszą składać się z tych samych liter, występujących taką samą liczbę razy. Jeżeli więc posortujemy każdy z łańcuchów alfabetyczie od 'a' do 'z', powiiśmy otrzymać dwa takie same łańcuchy. Do posortowaia wykorzystamy metodę sort: I [11]: def aagramsolutio2(s1,s2): alist1 = list(s1) #koiecze, żeby skorzystać z sort alist2 = list(s2) alist1.sort() alist2.sort() pos = 0 matches = True while pos < le(s1) ad matches: if alist1[pos]==alist2[pos]: pos = pos + 1 else: matches = False retur matches prit(aagramsolutio2('abcde','edcba')) True Na pierwszy rzut oka może się wydawać, że algorytm jest klasy, poieważ wykoujemy tylko jedą iterację po elemetach łańcuchów. Jedak wywołaie metody sortrówież "kosztuje", ajczęściej lub O( log ). Dlatego czas wykoaia będzie zdomioway przez operację sortowaia. O( 2 )
Rozwiązaie 3: algorytm siłowy (ag. brute force) Metoda siłowa rozwiązaia jakiegoś zadaia polega a wyczerpaiu wszystkich możliwości. Dla aagramów ozacza to wygeerowaie listy wszystkich możliwych łańcuchów ze zaków łańcucha s1 i sprawdzeie, czy s2 zajduje się a tej liście. Nie jest to jedak zalecae podejście, przyajmiej w tym przypadku. Zauważmy miaowicie, że dla ciągu zaków s1 o długości mamy wyborów pierwszego zaku, ( 1) możliwości dla zaku a drugiej pozycji, ( 2) a trzeciej pozycji itd. Musimy zatem wygeerować! łańcuchów zaków. Dla ustaleia uwagi przyjmijmy, że s1 składa się z 20 zaków. Ozacza to koieczość wygeerowaia I [12]: import math math.factorial(20) Out[12]: 2432902008176640000 łańcuchów zaków, a astępie odszukaie wśród ich ciągu. Widzieliśmy już wcześiej, ile czasu zajmuje algorytm klasy, dlatego ie jest to polecae podejście do zagadieia aagramów. O(!) s2 Rozwiązaie 4: zliczaj i porówuj s1 s2 Jeżeli i są aagramami, będą miały tą samą liczbę wystąpień litery 'a', tą samą litery 'b' itd. Dlatego możemy zliczyć liczbę wystąpień poszczególych zaków w łańcuchach i porówać te liczby ze sobą:
I [18]: def aagramsolutio4(s1,s2): c1 = [0]*26 #dla ułatwieia ograiczamy się do języka agielskiego c2 = [0]*26 for i i rage(le(s1)): pos = ord(s1[i])-ord('a') #pozycja zaku w alfabecie c1[pos] = c1[pos] + 1 for i i rage(le(s2)): pos = ord(s2[i])-ord('a') c2[pos] = c2[pos] + 1 j = 0 stillok = True while j<26 ad stillok: if c1[j]==c2[j]: j = j + 1 else: stillok = False retur stillok prit(aagramsolutio4('apple','pleap')) True Rówież w przypadku tej metody mamy do czyieia z iteracjami, jedak teraz żada z ich ie jest zagieżdżoa. Dodając kroki koiecze do wykoaia w tych iteracjach do siebie otrzymamy Jest to zatem algorytm klasy T() = 2 + 26, czyli ajszybszy ze wszystkich prezetowaych. Zauważmy jedak, że c1 c2 lepszą wydajość uzyskaliśmy kosztem większego obciążeia pamięci (dwie dodatkowe listy i ). To sytuacja bardzo często spotykaa w praktyce. Dlatego programując, ie raz staiemy przed koieczością wyboru, który z zasobów (czas procesora czy pamięć) ależy poświęcić. Struktury daych w Pythoie i ich wydajość Złożoe struktury daych dostępe w Pythoie oszczędzają programiście sporo pracy. Dobrze jest wiedzieć, jakie są zarówo ich moce stroy jak i ograiczeia, poieważ pozwoli to w przyszłości pisać lepsze programy. Listy Przyjrzyjmy się ajpierw różym sposobom geerowaia list:
I [13]: def test1(): #łączeie list l = [] for i i rage(1000): l = l + [i] def test2(): #dodawaie elemetu l = [] for i i rage(1000): l.apped(i) def test3(): #list comprehesio l = [i for i i rage(1000)] def test4(): #z zakresu l = list(rage(1000)) I [14]: %%timeit test1() The slowest ru took 4.51 times loger tha the fastest. This could mea that a itermediate result is beig cached. 1000 loops, best of 3: 1.34 ms per loop I [15]: %%timeit test2() The slowest ru took 7.11 times loger tha the fastest. This could mea that a itermediate result is beig cached. 10000 loops, best of 3: 81.3 µs per loop I [16]: %%timeit test3() The slowest ru took 5.44 times loger tha the fastest. This could mea that a itermediate result is beig cached. 10000 loops, best of 3: 33.3 µs per loop
I [17]: %%timeit test4() The slowest ru took 4.18 times loger tha the fastest. This could mea that a itermediate result is beig cached. 10000 loops, best of 3: 14.1 µs per loop Z powyższego eksperymetu wyika, że ajszybszą metodą jest tworzeie listy z zakresu, atomiast ajdłużej trwa łączeie list. Jeśli chodzi o róże operacje a listach, ich złożoość jest astępująca: Operacja idex [] Złożoość O(1) zmiaa wartości elemetu O(1) apped pop() pop(i) isert(i,item) del iterowaie zawiera (i) odczyt wycika [x:y] usuwaie wycika przypisaie wycika reverse łączeie O(1) O(1) O(k) O(+k) O(k) sort O( log ) iloczy O(k) Dla przykładu zbadajmy wydajość metody pop: I [1]: import timeit popzero = timeit.timer("x.pop(0)", "from mai import x") poped = timeit.timer("x.pop()", "from mai import x")
I [2]: x = list(rage(2000000)) popzero.timeit(umber=1000) Out[2]: 1.8516352830001779 I [3]: x = list(rage(2000000)) poped.timeit(umber=1000) Out[3]: 0.00010356900020269677 Eksperymet te moża powtórzyć dla różych : I [15]: popzero = timeit.timer("x.pop(0)", "from mai import x") poped = timeit.timer("x.pop()", "from mai import x") pi = [] pe = [] for i i rage(1000000,10000001,1000000): x = list(rage(i)) pt = poped.timeit(umber=1000) pe.apped(pt) x = list(rage(i)) pz = popzero.timeit(umber=1000) pi.apped(pz) I [16]: %matplotlib ilie I [17]: import matplotlib import matplotlib.pyplot as plt I [22]: sizes = list(rage(1000000,10000001,1000000))
I [26]: plt.xlabel("rozmiar listy") plt.ylabel("czas wykoaia") plt.plot(sizes,pi,'ro',label="pop(0)") plt.plot(sizes,pe,'bs',label="pop()") Out[26]: [<matplotlib.lies.lie2d at 0x7f8a4a370400>] Słowiki Słowiki to drugi główy typ daych w Pythoie. Wydajości wybraych operacji są astępujące: Operacja copy get item set item delete item Złożoość O(1) O(1) O(1) cotais (i) O(1) iteratio