20.11.2014 Views

Lekcija 16 - FESB

Lekcija 16 - FESB

Lekcija 16 - FESB

SHOW MORE
SHOW LESS

You also want an ePaper? Increase the reach of your titles

YUMPU automatically turns print PDFs into web optimized ePapers that Google loves.

<strong>Lekcija</strong> 17. Rekurzija, složenost algoritama i sortiranje<br />

Rekurzija nastaje kada funkcija poziva samu sebe direktno ili indirektno.<br />

Indirektna rekurzija nastaje kada jedna funkcija poziva drugu funkciju, a ova ponovo poziva funkciju iz koje je<br />

pozvana.<br />

U matematici se često proračun funkcija f(n) definira pomoću rekurzije:<br />

Koristi se pravilo:<br />

1. postavi f(0) "temeljni slučaj"<br />

2. računaj f(n) pomoću f(k) za k < n "pravilo rekurzije"<br />

Primjerice, rekurzivno se može izračunati n! (n-faktorijela), jer vrijedi<br />

za n=0: 0! = 1; (temeljno rekurzije)<br />

za n>0: n! = n * (n-1)! (rekurzivno pravilo)<br />

pa se sve vrijednosti mogu izračunatu pomoću gornjeg rekurzivnog pravila:<br />

1! = 1 * 0! = 1<br />

2! = 2 * 1! = 2<br />

3! = 3 * 2! = 6 ... itd.<br />

int factorial(int n)<br />

{<br />

if (n == 0)<br />

return 1;<br />

else<br />

return factorial(n-1) * n;<br />

}<br />

1


Usporedba rekurzivnih i iterativnih programa<br />

rekurzivni proračun sume<br />

1,2,..n<br />

int sum( int n)<br />

{<br />

if (n ==1)<br />

return 1;<br />

else<br />

return sum(n – 1) + n;<br />

}<br />

iterativni proračun sume 1,2,..n<br />

int sum( int n)<br />

{<br />

int s=0;<br />

while (n)<br />

sum += n--;<br />

return s;<br />

}<br />

redoslijed poziva funkcije<br />

sum(4)<br />

sum(4)<br />

sum(3)<br />

sum(2)<br />

sum(1)<br />

return 1<br />

return 1+2=3<br />

return 3+3 =6<br />

return 6+4 = 10<br />

stanje memorije koja se koristi<br />

za prijenos argumenata funkcije<br />

(stog memorija)<br />

arg. 4<br />

arg. 4 3<br />

arg. 4 3 2<br />

arg. 4 3 2 1<br />

arg. 4 3 2<br />

arg. 4 3<br />

arg. 4<br />

arg. -<br />

Rekurzija je korisna u mnogim slučajevima, posebno kada je njome prirodno definiran problem.<br />

2


Metoda - podijeli pa vladaj (Divide and Conquer)<br />

Opći princip metode ja da se problem podijeli u više manjih problema, pa rješenje potraži u manjim cjelinama<br />

Primjer: Binarno pretraživanje niza<br />

Zadan je sortirani niz cijelih brojeva a[n]<br />

a[i-1]< a[i], za i=1,..n-1<br />

Problem: da li se ovom nizu, izmežu indeksa d i g, nalazi element vrijednosti x.<br />

U tu svrhu koristit ćemo funkciju<br />

int bsearch( int a[],int x, int d, int g);<br />

Problem definiramo rekurzivno na slijedeći način:<br />

1. Temeljne pretpostavke:<br />

Ako u nizu a[i] postoji element jednak traženoj vrijednosti x, njegov indeks je iz intervala [d,g], gdje<br />

mora biti istinito g >= d. Trivijalni slučaj je za d=0, g=n-1, koji obuhvaća cijeli niz.<br />

Razmatramo element niza indeksa i = (g+d)/2. (dijelimo niz na dva podniza)<br />

Ako je a[i]==x, pronađen je traženi element niza i funkcija vraća indeks i.<br />

2. Pravilo rekurzije:<br />

Ako je a[i]


Ilustrirajmo proces traženja vrijednosti x=23 u nizu od 14 elemenata<br />

0 1 2 3 4 5 6 7 8 9 10 11 12 13<br />

1 2 3 5 6 8 9 12 23 26 27 31 34 42<br />

d i g<br />

1 2 3 5 6 8 9 12 23 26 27 31 34 42<br />

d i g<br />

1 2 3 5 6 8 9 12 23 26 27 31 34 42<br />

d i g<br />

1.korak: d=0, g=13, i=(d+g)/2= 6, a[6]23<br />

3.korak: d=7, g=i-1=9, i=8, a[8]==23<br />

int bsearch( int a[],int x, int d, int g)<br />

{<br />

int i;<br />

if (d > g) return –1; /* ako x nije u nizu*/<br />

i = (d + g)/ 2;<br />

if (a[i] == x)<br />

return i;<br />

if (a[i] < x)<br />

return bsearch( a, x, i + 1, g);<br />

else<br />

return bsearch( a, x, d, i - 1);<br />

}<br />

4


Iteracija kao specijalni slučaj rekurzije<br />

Ekvivalentni iterativni i rekurzivni zapis funkcije petlja()<br />

“tail” rekurzija iteracija 1. ili iteracija 2.<br />

void petlja()<br />

{<br />

iskaz<br />

if (e)<br />

petlja();<br />

}<br />

void petlja()<br />

{<br />

start:<br />

iskaz<br />

if (e) goto start;<br />

}<br />

void petlja()<br />

{<br />

do<br />

iskaz<br />

while (e);<br />

}<br />

Rekurzivni poziv se vrši na kraju (ili na repu) tijela funkcije, stoga se ovaj tip rekurzije naziva "tail" rekurzija ili<br />

"rekurzija na repu".<br />

Na ovaj se način dobija efikasnija funkcija, jer se ne gubi vrijeme i prostor na stogu potreban za poziv funkcije.<br />

Funkciju bsearch, može se lako transformirati u "tail" rekurzivnu funkciju:<br />

int bsearch( int a[],int x, int d, int g)<br />

{<br />

int i;<br />

start: if (d > g) return –1;<br />

i = (d + g)/ 2;<br />

if (a[i] == x) return i;<br />

if (a[i] < x) d=i+1;<br />

else g=i-1<br />

goto start;<br />

}<br />

5


Kule Hanoia<br />

Najpoznatiji rekurzivni problem u kompjuterskoj literaturi - Kule Hanoia.<br />

A B C<br />

Zadatak:<br />

Premjestiti sve diskove s štapa A na štap B u redoslijedu kako se nalaze na štapu A.<br />

Pravila:<br />

1. Odjednom se smije pomicati samo jedan disk.<br />

2. Ne smije se stavljati veći disk povrh manjeg diska.<br />

3. Može se koristiti štap C za smještaj diskova, ali uz poštovanje prethodna dva pravila.<br />

Temeljni slučaj rekurzije:<br />

1. Ako kula A sadrži samo jedan disk, prebaci taj disk na ciljni štap B.<br />

Rekurzivno pravilo :<br />

Ako kula sadrži N diskova, pomicanje diskova se može izvesti u tri koraka<br />

1. Pomaki gornjih N-1 diskova na pomoćni štap C.<br />

2. Preostali donji disk s štapa A pomaki na ciljni štap B.<br />

3. Zatim kulu od N-1 diska s pomoćnog štapa C prebaci na ciljni štap B.<br />

6


Kako napisati funkciju koji izvršava gornje pravilo. Nazvat ćemo je move_tower,<br />

void move_tower(int n, char A, char B, char C);<br />

a argumente koji će nam biti potrebni su:<br />

n- broj diskova koje treba pomaknuti,<br />

A - ime početnog štapa ,<br />

B - ime ciljnog štapa,<br />

C - ime pomoćnog štapa.<br />

void move_disk(char from, char to)<br />

{<br />

printf("%c -> %c\n", from, to);<br />

}<br />

void move_tower(int n, char A, char B, char C)<br />

{<br />

if (n == 1) { /* temeljni slučaj */<br />

move_disk(A, B);<br />

}<br />

else {<br />

move_tower (n - 1, A, C, B); /* 1. pravilo */<br />

move_disk (A, B); /* 2. pravilo */<br />

move_tower (n - 1, C, B, A); /* 3. pravilo */<br />

}<br />

}<br />

7


int main()<br />

{<br />

/* npr. za slučaj 3 diska*/<br />

}<br />

move_tower(3, 'A','B','C');<br />

return 0;<br />

Rezultat:<br />

A -> B,<br />

A -> C,<br />

B -> C,<br />

A -> B,<br />

C -> A,<br />

C -> B,<br />

A -> B<br />

8


SLOŽENOST ALGORITAMA<br />

Kvalitetan je onaj algoritam kojim se postiže efikasno korištenje memorijskog prostora<br />

M(n) -<br />

i prihvatljivo vrijeme obrade<br />

T(n) - vremenska složenost (complexity) (n – dimenzija problema)<br />

"Veliki-O" notacija<br />

Analizirajmo sada segmemt programa u kojem treba sumirati elemente kvadratne matrice, dimenzije n. Također,<br />

označimo broj ponavljanja naredbi (pri tome se podrazumjeva da su signifikantne one naredbe u kojima se vrši<br />

zbrajanje).<br />

NAREDBE PROGRAMA BROJ PONAVLJANJA<br />

-------------------------------------------<br />

S = 0 ; ........<br />

for(i=0; i


Ovaj se zaključak u računarskoj znanosti piše u obliku tzv. "veliki-O" notacije:<br />

T(n) = O [n²]<br />

i kaže se da je T(n) "veliki O od n² ". Funkcija f(n) = n² predstavlja red složenosti algoritma.<br />

Tip algoritma<br />

f(n)<br />

Konstantan<br />

const.<br />

Logaritamski<br />

log 2 n<br />

Linearan<br />

n<br />

Linearno-logaritamski nlog 2 n<br />

Kvadratni n 2<br />

Stupanjski<br />

n k (k>2)<br />

Eksponencijalni<br />

k n (k>1)<br />

Faktorijelni n!<br />

Binarno pretraživanje je LOGARITAMSKI ALGORITM<br />

Opća koncepcija logaritamskih algoritama je slijedeća:<br />

1. Obaviti postupak kojom se veličina problema prepolovi.<br />

2. Nastaviti razlaganje problema dok se ne dođe do veličine 1.<br />

3. Obaviti završnu obradu s problemom jedinične veličine.<br />

Ukupan broj razlaganja k dobije se iz uvjeta:<br />

n / 2k = 1 .<br />

odnosno k= log 2 n. Za binarno pretraživanje T(n) = O(log 2 n)<br />

10


LINEARNI ALGORITMI<br />

Linearni algoritmi se javljaju u svim slučajevima gdje je obradom obuhvaćeno n istovjetnih podataka i gdje<br />

udvostručenje količine radnji ima za posljedicu udvostručenje vremena obrade. Opći oblik linearnog algoritma<br />

može se prikazati u vidu jedne for petlje:<br />

for (i=0; i< n; i++)<br />

{obrada koja traje vrijeme t}<br />

Zanemarujući vrijeme opsluživanja for petlje, u ovom slučaju funkcija složenosti je T(n)=nt, pa je T(n) = O(n).<br />

LINEARNO-LOGARITAMSKI ALGORITMI<br />

Linearno-logaritamski algoritmi (O[n log(n)]) također spadaju u klasu veoma efikasnih algoritama jer im<br />

složenost raste sporije čak i od kvadratne funkcije. Primjer za ovakav algoritam je Quicksort, koji će biti detaljno<br />

opisan kasnije. Bitna osobina ovih algoritama je slijedeća<br />

(1) Obaviti pojedinačnu obradu kojom se veličina problema prepolovi.<br />

(2) Unutar svake polovine sekvencijalno obraditi sve postojeće podatke.<br />

(3) Nastaviti razlaganje problema dok se ne dođe do veličine 1.<br />

Slično kao i kod logaritamskih algoritama i ovdje je ukupan broj polovljenja log 2 n, ali kako se pri svakom<br />

polovljenju sekvencijalno obrade svi podaci, to je ukupan broj elementarnih obrada jednak nlog 2 n, i to predstavlja<br />

rezultantni red funkcije složenosti.<br />

KVADRATNI ALGORITMI<br />

Kvadratni algoritmi (O[n 2 ]) se najčešće dobijaju kada se koriste dvije for petlje jedna unutar druge. Primjer je dat<br />

na početku ovog poglavlja.<br />

STUPANJSKI ALGORITMI<br />

Stupanjski algoritmi se mogu dobiti poopćenjem kvadratnih algoritama, za algoritam s k umetnutih for petlji<br />

složenost je (O[n k ]).<br />

11


EKSPONENCIJALNI ALGORITAMSKI<br />

Eksponencijalni algoritmi (O[k n ]) spadaju u kategoriju problema za koje se suvremena računala ne mogu koristiti,<br />

izuzev u slučajevima kada su dimenzije takvog problema veoma male. Jedan od takovih primjera je algoritam<br />

koji rekurzivno rješava igru "Kule Hanoia", opisan u prethodnom poglavlju.<br />

FAKTORIJELNI ALGORITMI<br />

Kao primjer faktorijelnih algoritama najčešće se uzima problem trgovačkog putnika. Problem je formuliran na<br />

slijedeće način: zadano je n+1 točaka u prostoru i poznata je udaljenost između svake dvije točke j i k. Polazeći od<br />

jedne točke potrebno je formirati putanju kojom se obilaze sve točke i vraća opet u polaznu točku tako da je<br />

ukupni prijeđeni put minimalan.<br />

Trivijalni algoritam za rješavanje ovog problema mogao bi se temeljiti na uspoređivanju duljina svih mogućih<br />

putanja. Broj mogućih putanja iznosi n!. Polazeći iz početne točke postoji n putanja do n preostalih točaka. Kada<br />

odaberemo jednu od njih i dođemo u prvu točku onda nam preostaje n-1 moguća putanja do druge točke, n-2<br />

putanja do treće točke, itd., n+1-k putanja do k-te točke, i na kraju samo jedna putanja do n-te točke i natrag u<br />

polaznu točku. Naravno u literaturi postoje razne efikasnije varijante algoritma za rješavanje navedenog problema,<br />

ali u opisanom slučaju sa jednostavnim nabrajanjem i uspoređivanjem duljina n! različitih zatvorenih putanja<br />

dolazimo do algoritma čije je složenost O(n!).<br />

12


Sortiranje<br />

Ulaz: niz A[n] od n elemenata.<br />

Izlaz: Elementi raspoređeni prema vrijednostima<br />

A[0] min od A[4..5] zamijeni sa A[4]<br />

1 2 3 4 5 6 -> niz je sortiran<br />

13


void selectionSort(int *A, int n)<br />

{<br />

int i, j, imin; /* indeks najmanjeg elementa u A[i..n-1] */<br />

for (i = 0; i < n-1; i++)<br />

{<br />

/* Odredi najmanji element u A[i..n-1]. */<br />

imin = i; /* pretpostavi da je to A[i] */<br />

for (j = i+1; j < n; j++)<br />

if (A[j] < A[imin]) /* ako je A[j] najmanji */<br />

imin = j; /* zapamti njgov indeks */<br />

swap(&A[i], &A[imin]);<br />

}<br />

}<br />

/* Zamjena vrijednosti dva int */<br />

void swap(int *a, int *b)<br />

{<br />

int t = *a;<br />

*a = *b;<br />

*b = t;<br />

}<br />

14


Analiza selekcijskog sortiranja<br />

Uzet ćemo da se svaka naredba izvršava neko konstantno vrijeme. Svaka iteracija vanjske petlje (indeks i) traje<br />

konstantno vrijeme t 1 plus vrijeme izvršenja unutarnje petlje (indeks j). Svaka iteracija u unutarnjoj petlji traje<br />

konstantno vrijeme t 2 .<br />

Broj iteracija unutarnje petlje ovisi u kojoj se iteraciji nalazi vanjska petlja:<br />

Ukupno vrijeme je:<br />

Broj operacija u<br />

i<br />

unutarnjoj petlji<br />

0 n-1<br />

1 n-2<br />

2 n-3<br />

... ...<br />

n-2 1<br />

T(n) = [t 1 + (n-1) t 2 ] + [t 1 + (n-2) t 2 ] + [t 1 + (n-3) t 2 ] + ... + [t 1 + (1) t 2 ]<br />

odnosno, grupirajući članove u oblik t 1 ( …) + (...) t 2 dobije se<br />

T(n) = (n-1) t 1 + [ (n-1) + (n-2) + (n-3) + ... + 1 ] t 2<br />

Izraz u uglatim zagradama predstavlja sumu aritmetičkog niza<br />

1 + 2 + 3 + ... + (n-1) = (n-1)n/2 = (n 2 -n)/2,<br />

pa je ukupno vrijeme jednako:<br />

T(n) = (n-1) t 1 + [(n 2 -n)/2] t 2 = - t 1 + t 1 n - t 2 n/2 + t 2 n 2 /2<br />

Vidimo da dominira član sa n 2 , pa je složenost selekcijskog sortiranja jednaka O(n 2 )<br />

15


Sortiranje umetanjem<br />

Algoritam sortiranja umetanjem (eng. insertion sort), se zasniva na postupku koji je sličan načinu kako se slažu<br />

igraće karte. Ideju demonstrirajmo nizom od 6 brojeva. Algoritam se vrši u u n-1 korak. U svakom koraku se<br />

umeće i-ti element u dio niza koji mu prethodi (A[0..i-1]), tako taj niz bude sortiran.<br />

6 4 1 5 3 2 -> ako je A[1]< A[0], umetni A[1] u A[0..0]<br />

4 6 1 5 3 2 -> ako je A[2]< A[1], umetni A[2] u A[0..1]<br />

1 4 6 5 3 2 -> ako je A[3]< A[2], umetni A[3] u A[0..2]<br />

1 4 5 6 3 2 -> ako je A[4]< A[3], umetni A[4] u A[0..3]<br />

1 3 4 5 6 2 -> ako je A[5]< A[4], umetni A[5] u A[0..4]<br />

1 2 3 4 5 6 -> niz je sortiran<br />

Algoritam se može zapisati pseudokodom:<br />

for (i = 1; i < n; i++)<br />

{<br />

el = A[i];<br />

od indeksa k=i-1, do indeka k=0 analiziraj A[0 .. i-1]<br />

ako je el < A[k], pomakni element A[k] na mjesto A[k+1]<br />

inače prekini<br />

zatim umetni el na mjesto A[j+1]<br />

}<br />

<strong>16</strong>


Analiza složenosti sortiranja umetanjem<br />

Pokazat ćemo da je sortiranje umetanjem primjer algoritma u kojem prosječno vrijeme izvršenja nije puno kraće<br />

od vremena izvršenja koje se postiže u najgorem slučaju.<br />

Najgori slučaj<br />

Vanjska petla se izvršava u n-1 iteracija, što daje O(n) iteracija. U unutarnjoj petlji se vrši od 0 do i


17.8.3 Sortiranje spajanjem sortiranih podnizova (merge sort)<br />

Sortiranje metodom spajanja sortiranih podnizova (eng. merge sort) temelji se na ideji da se niz rekurzivno dijeli<br />

na dva sortirana niza, te da se zatim izvrši spajanje tih sortiranih nizova.<br />

Problem će biti riješen za slučaj da se sortira niz A[d..g], tj. od donjeg indeksa d do gornjeg indeksa g, funkcijom<br />

void mergeSort(int *A, int d, int g);<br />

Rekurzivnom se podjelom niza u dva podniza, A[d..s] i A[s+1,g], koji su otprilike podjednake veličine (indeks s<br />

se odredi kao srednja vrijednost s = (d+g)/2), dolazi se do temeljnog slučaja kada u svakom nizu ima samo jedan<br />

element. Taj jedno-elementni niz je već sortiran, pa se pri "izvlačenju" iz rekurzije može vršiti spajanje sortiranih<br />

podnizova. Ovaj postupak je ilustriran na slici 4.<br />

Za implementaciju ovog algoritma bitno je uočiti sljedeće:<br />

• Ulazni niz nije nužno "fizikalno" dijeliti na podnizove, jer se podnizovi ne preklapaju. Dovoljno je<br />

zapamtiti indekse ulaznog niza koji određuju neki podniz.<br />

• Spajanje podnizova se uvijek provodi s elementima koji su u ulaznom nizu poredani jedan do drugog; prvi<br />

podniz je A[d..s], a drugi podniz je A[s+1..g]. U tu svrhu koristit će se funkcija:<br />

18


void merge(int *A, int d, int s, int g)<br />

/* Ulaz: dva sortirana niza A[d..s] i A[s+1..g] */<br />

/* Izlaz: sortirani niz A[d..g] */<br />

Očito je da se radi o algoritmu tipa "podijeli pa vladaj":<br />

1. Podijeli: podijeli niz A[d,g], na način da dva podniza A[d,s] i A[s+1,g] sadrže otprilike pojednak broj<br />

elemenata. To se postiže izborom: s=(d+g)/2.<br />

2. Vladaj: rekurzivno nastavi dijeliti oba podniza sve dok njihova veličina ne postane manja od 2 elementa<br />

(niz koji sadrži nula ili jedan element je sortirani niz).<br />

3. Spoji: Nakon toga, pri "izvlačenju" iz rekurzije, izvrši spajanje sortiranih nizova koristeći funkciju<br />

merge(A,d,s,g).<br />

Implementacija ovog algoritma je jednostavna;<br />

void mergeSort(int *A, int d, int g)<br />

{<br />

if (d < r ) { /* temeljni slucaj - 1 element */<br />

int s = (d + g) / 2; /* s je indeks podjele niza */<br />

mergeSort(A, d, s); /* rekurzivno podijeli A[d..s] */<br />

mergeSort(A, s+1, g); /* rekurzivno podijeli A[s+1..g]*/<br />

merge(A, d, s, g); /* zatim spoji sortirane nizove */<br />

}<br />

}<br />

Još treba definirati funkciju merge(). Ona se može realizirati na način da se formiraju dva pomoćna niza, donji[] i<br />

gornji[], u koje se kopira sortirane nizove A[d..s] i A[s+1..g]. Zatim se iz tih pomoćnih sortiranih nizova formira<br />

jedan sortirani niz u području ulaznog niza A[d..g]. Postupak je ilustriran na slici 5.<br />

19


Slika 17.5. Spajanje sortiranih nizova<br />

Implementacija je sljedeća:<br />

/* Spajanje podnizove A[d..s] i A[s+1..g] u sortirani niz A[d..g]. */<br />

void merge(int *A, int d, int s, int g)<br />

{<br />

int m = s - d + 1; /* broj elemenata u A[d..s] */<br />

int n = g - s; /* broj elemenata u A[s+1..g] */<br />

int i; /* indeks u donji niz*/<br />

int j; /* indeks u gornji niz*/<br />

int k; /* indeks u orig. niz A */<br />

int *donji = malloc(sizeof(int) * m); /* niz A[d..s] */<br />

int *gornji = malloc(sizeof(int) * n); /* niz A[s+1..g] */<br />

/* Kopiraj A[d..s] u donji[0..m-1] i A[s+1..g] u gornji[0..n-1]. */<br />

for (i = 0, k = d; i < m; i++, k++) donji[i] = A[k];<br />

for (j = 0, k = s+1; j < n; j++, k++) gornji[j] = A[k];<br />

/* Usporedbom donji[0..m-1] i gornji[0..n-1], pomakni manji na sljedeću poziciju A[d..g].<br />

*/<br />

i = 0; j = 0; k = d;<br />

while(i < m && j < n; )<br />

if (donji[i] < gornji[j]) A[k++] = donji[i++];<br />

else A[k++] = gornji[j++];<br />

20


* Preostale elemente jednostavno kopiraj */<br />

/* Jedna od ove dvije petlje će imati nula iteracija! */<br />

while (i < m) A[k++] = donji[i++];<br />

while (j < n) A[k++] = gornji[j++];<br />

}<br />

/* Dealociraj memoriju koju zauzimaju donji i gornji. */<br />

free(donji); free(gornji);<br />

Operacija kopiranja iz pomoćnih nizova u sortirani niz A[d..g] provodi se jednostavnom usporedbom sadržaja<br />

donji[i] i gornji[j]. Kopira se manji element u A i inkrementira pozicija u nizu. Na taj način, ova se operacija vrši<br />

u linearnom vremenu O(g-d+1).<br />

Pomoćni nizovi su formirani alociranjem memorije, stoga se na kraju funkcije vrši oslobađanje memorije. U<br />

realnoj se primjeni može koristiti brži postupak, bez alociranja memorije, na način da se pomoćni nizovi<br />

deklariraju kao globalne varijable. Dimenzija ovih globalnih nizova mora biti veća od polovine dimenzije niza<br />

koji se sortira. Na sličan način se može provesti i sortiranje datoteka.<br />

Složenost metode spajanja podnizova<br />

Može se na jednostavan način pokazati da je vremenska složenost ovog algoritma jednaka O(n log 2 n), ukoliko se<br />

uzme da je veličina niza potencija broja 2, tj. da je n = 2 m . Pošto se pri svakom rekurzivnom pozivu niz dijeli na<br />

dva podniza, sve dok duljina podniza ne postane jednaka 1, proizlazi da je broj razina podijele niza jednak log 2 n.<br />

Na k-toj razini niz je podijeljen na 2 k podnizova duljine n/2 k . To znači da spajanje sortiranih nizova na k-toj razini<br />

ima složenost 2 k xO(n/2 k )= O(n), a pošto ima log 2 n razina, proizlazi da je ukupna složenost jednaka O(n log 2 n).<br />

Do istog rezultata se dolazi i uz znatno rigorozniju analizu složenosti.<br />

Može se pokazati da je ovo najbolji rezultat koji se može postići pri sortiranju nizova. Jedini problem ovog<br />

algoritma je što zahtijeva povećanu prostornu složenost.<br />

21


Quicksort<br />

Quicksort je rekurzivna metoda sortiranja kojom se u prosječno postiže O(n log 2 n), što je znatno bolje od O(n 2 )<br />

(koji nastaje samo u najgorem slučaju). Često se koristi u praksi. Temelji se na tehnici podijeli pa vadaj.<br />

Neka je problem sortirati dio niza A[p..r]. Koristi se algoritam:<br />

PODIJELI. Izaberi jedan element iz niza A[p..r] i zapamti njegovu vrijednost. Taj element ćemo nazavati<br />

pivot. Nakon toga podijeli A[p..r] u dva podniza A[p..q] i A[q+1..r] koji imaju slijedeća svojstva:<br />

Svaki element A[p..q] je manji ili jednak pivotu .<br />

Svaki element A[q+1..r] je veći ili jednak pivotu.<br />

Niti jedan podniz ne sadrži sve elemente (odnosno ne smije biti prazan).<br />

VLADAJ. Rekurzivno sortiraj oba podniza A[p..q] i A[q+1..r], i problem će biti riješen kada oba podniza<br />

budu imala manje od 2 elementa.<br />

Uvjet da nijedan podniz ne bude prazan je potreban jer kada bi to bilo ispunjeno, tada bi rekuzivni problem bio isti<br />

kao originalni problem, pa bi nastala bekonačna rekurzija.<br />

Pokažimo sada kako podijeliti podnizove da budu ispunjeni postavljeni uvjeti.<br />

Podjela se vrši funkcijom<br />

int partition(int *A, int p, int r)<br />

koja vraća indeks q (gdje je izvršena podjela na podnizove).<br />

22


Logika funkcije partition():<br />

Postupak podjele niza je<br />

ilustriran na nizu A[4..13].<br />

Označeni su indeksi (i,j).<br />

Potamnjeni elementi<br />

pokazuju koje elemente<br />

zamjenjujemo.<br />

Linija podjela je<br />

prikazana u slučaju kada i<br />

postane veći od j.<br />

Za pivot je odabran prvi<br />

element vrijednosti 6.<br />

Podjela se vrši pomoću dva indeksa (i, j) i pivota<br />

koji se odabire kao element A[p], prema pravilu:<br />

i pomičemo od početka prema kraju niza, dok<br />

ne nađemo element A[i] koji je veći ili<br />

jednak pivotu<br />

j pomičemo od kraja prema početku niza, dok<br />

ne nađemo element A[j] koji je manji ili<br />

jednak pivotu<br />

zatim zamjenjujemo vrijednosti A[i] i A[j],<br />

kako bi svaki svaki element A[p..i] bio manji<br />

ili jednak pivotu, a svaki element A[j..r] veći<br />

ili jednak pivotu.<br />

Ovaj se proces nastavlja dok se ne dobije da je<br />

i > j. Tada je podjela završena, a j označava<br />

indeks koji vraća funkcija partition. Ovaj<br />

uvjet ujedno osigurava da nijedan podniz<br />

neće biti prazan.<br />

23


* Podijeli A[p..r] u podnizove A[p..q] and A[q+1..r], gdje je p = j, raspored je OK, inače, zamijeni A[i] sa A[j] i nastavi. */<br />

if (i < j) swap(&A[i], &A[j]);<br />

else return j;<br />

}<br />

}<br />

Pomoću ove funkcije se sada iskazuje kompletni quicksort algoritam:<br />

void quicksort(int *A, int p, int r)<br />

{<br />

if (p < r) /* završava kada podniz ima manje od 2 elementa */<br />

{<br />

int q = partition(A, p, r); /* q je pivot */<br />

quicksort(A, p, q); /* rekurzivno sortiraj A[p..q] */<br />

quicksort(A, q+1, r); /* rekurzivno sortiraj A[q+1..r]*/<br />

}<br />

}<br />

24


Analiza složenosti quicksort algoritma<br />

Prvo uočimo da je vrijeme koje je potrebno za podjelu podnizova od n elemenata, proporcionalno broju<br />

elemenata cn (konstanta c > 0), tj. složenost je O(n). Analizirajmo zatim broj mogućih rekurzivnih poziva.<br />

Najbolji slučaj<br />

U najboljem slučaju nizovi se dijele na dvije jednake polovine. Pošto je tada ukupan broj razlaganja jednak log 2 n<br />

donije se da je složenost O(n)⋅O(log 2 n) = O(n log 2 n).<br />

Najgori slučaj<br />

U najgorem slučaju podjela na podnizove se vrši tako da je u jednom podnizu uvijek samo jedan element. To<br />

znači da bi tada imali n rekurzivnih poziva, pa bi ukupna složenost bila O(n) ⋅O(n)= O(n 2 ). Najgori slučaj nastupa<br />

kada je niz sortiran ili "skoro" sortiran.<br />

Prosječni slučaj<br />

Analiza prosječnog slučaja je dosta komplicirana. Ovdje je nećemo iznositi. Važan je zaključak da se u<br />

prosječnom slučaju dobija složenost koja je bliska najboljem slučaju O(n log 2 n).<br />

Kako odabrati pivot element<br />

U prethodnom algoritmu za pivota je odabran početni element A[p]. U praksi se pokazalo da je znatno povoljnije<br />

odabir pivota vršiti po nekom slučajnom uzorku. Koristi se postupak da se odaberu se tri indeksa po slučajnom<br />

uzorku, a zatim se za pivot odabere jedan od ova tri elementa koji je najbliži srednjoj vrijednosti (median). Ovim<br />

postupkom se još dobija vjerojatnost dobre podjele (50%). Dublje opravdanje ove metode ovdje neće biti dano.<br />

Pokazalo se, da uz primjenu ovakovog postupka, quicksort predstavlja najbrži poznati način sortiranja nizova, pa<br />

je implementiran u standardnoj biblioteci C jezika.<br />

25

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!