Lekcija 16 - FESB
Lekcija 16 - FESB
Lekcija 16 - FESB
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