Algoritam sortiranja postavlja elemente neke liste u određeni redosled. Najčešće se sortira po numeričkom redosledu ili po abecedi (leksikografski redosled).
3. 2
1. Uvod
Kada radimo sa velikom količinom podataka važno je da su sortirani jer takav
sistem olakšava njihovo pretraživanje. Iz tog razloga je sortiranje uveliko prisutno u
informatici, a i u drugim područjima. Podaci su sortirani u telefonskim imenicima,
poreskim registrima, bibliotekama, skladištima odnosno svuda gde je potrebno podatke
pretražiti i preuzeti. Postoji mnogo algoritama za sortiranje i međusobno su vrlo različiti.
Algoritam sortiranja postavlja elemente neke liste u određeni redosled. Najčešće se
sortira po numeričkom redosledu ili po abecedi (leksikografski redosled). Efikasni
algoritmi sortiranja preduslov su uspešnosti nekih drugih algoritama npr. onih algoritama
pretraživanja koji zahtevaju sortiranu listu da bi u njoj mogli pronaći određeni član. Većina
algoritama sortiranja zasniva se na metodologiji "podeli i vladaj" tj. ponavlja se postupak
reduciranja složenog problema na više jednostavnijih (manjih) celina, odnosno veliki niz
se deli na više manjih nizova koji se zasebno sortiraju. Takvi zasebno sortirani segmenti
zatim se spajaju u konačno sortirani niz. Reduciranje niza se obično ponavlja na
rekurzivan način. Algoritmi sortiranja najviše se razlikuju u operacijama odnosno
načinima deljenja na podnizove i spajanja u konačno sortirani niz.
4. 3
2. Rekurzija
Rekurzija je važan koncept u računarskoj nauci jer se mnogi algoritmi mogu
pomoću nje najbolje prikazati. Rekurzija je zapravo i jedan način implementacije
potprograma - u nekim jezicima potprogram može pozvati samog sebe uzrokujući
zaustavljanje svog procesa izvržavanja za vreme dok traje drugi "ubačen" proces
izvršavanja istog potprograma.
Kao primer za bolje razumevanje rekurzije razmotrimo definiciju predaka određene
osobe:
roditelji neke osobe su njeni preci (osnovni slučaj)
roditelji bilo kojeg pretka su također preci osobe koju razmatramo (korak
rekurzije)
Rekurzivna procedura dakle poziva samu sebe ili sadrži poziv neke druge
procedure koja bi mogla rezultovati pozivom originalne procedure. Da bi se izbeglo
beskonačno izvođenje programa:
mora postojati određeni kriterijum (temeljni kriterijum) za koji procedura
ne poziva samu sebe
svaki put kad procedura pozove samu sebe (direktno ili indirektno) mora
biti bliže temeljnom kriterijumu
Neki jezici (npr. FORTRAN) ne podržavaju rekurziju. Rekurzivni programi su
kraći, ali izvođenje programa je duže. Za čuvanje rezultata i povratak iz rekurzije koristi se
struktura podataka stog. Većina programskih jezika, i programski jezik C, podržava
rekurziju.
Primer rekurzivno definisane funkcije je sledeća definicija funkcije faktorijela:
f(0) = 1
f(n) = n * f(n − 1) za svaki celi broj n > 0
5. 4
Prema datoj definiciji f(3) računa se kako sledi:
f(3) = 3 * f(3 − 1)
= 3 * f(2)
= 3 * 2 * f(2 − 1)
= 3 * 2 * f(1)
= 3 * 2 * 1 * f(1 − 1)
= 3 * 2 * 1 * f(0)
= 3 * 2 * 1 * 1
= 6
Sledeći primer rekurzivne funkcije u programskom jeziku C takođe izračunava
faktorijel celog broja n:
int fakt(int n){
if (n <= 1) {
return 1;
} else {
return n * fakt(n-1);
}
}
3. Sortiranje
Sortiranje je jedan od fundamentalnih zadataka u računarstvu. Sortiranje
podrazumeva uredivanje niza u odnosu na neko linearno uredenje (npr. uredenje niza
brojeva po veličini — rastuće ili opadajuće, uredivanje niza niski leksikografski ili po
dužini, uredivanje niza struktura na osnovu vrednosti nekog polja i slično). Mnogi zadaci
nad nizovima se mogu jednostavnije rešiti u slučaju da je niz sortiran (npr. Pretraživanje se
može vršiti binarnom pretragom).
Neki od algoritama za sortiranje rade u mestu (engl. in-place), tj. sortiraju zadate
elemente bez korišćenja dodatnog niza. Drugi algoritmi zahtevaju korišćenje pomoćnog
niza ili nekih drugih struktura podataka.
Prilikom sortiranja nizova koji sadrže podatke netrivijalnih tipova najskuplj
operacije su operacija poredenja dva elementa niza i operacija razmene dva elemente niza.
Zbog toga se prilikom izračunavanja složenosti algoritama obično u obzir uzimaju samo
ove operacije.
6. 5
Ako je dat niz neuređenih brojeva, problem njegovog sortiranja se sastoji od
preuređivanja brojeva tog niza tako da oni obrazuju rastući niz. Preciznije, za dati niz a od
n elemenata a1,a2 . . .,an treba naći permutaciju svih indeksa elemenata niza i1, i2, . . ., in
tako da novi prvi element ai1, novi drugi element ai2 i tako dalje, novi n-ti element ain u
nizu zadovoljavaju uslov ai1 ≤ ai2 ≤· · ·≤ ain .
Za rešavanje problema sortiranja niza ćemo pokazati četiri jednostavna algoritma.
To su klasični algoritmi koji približno imaju isto vreme izvršavanja koje kvadratno zavisi
od veličine ulaza (tj. broja elemenata niza).
Pre opisa algoritama sortiranja navedimo funkciju koja proverava da li je niz već
sortiran.
4. Sortiranje zamenjivanjem (bubble-sort)
Ovo je jedna od najjednostavnijih metoda sortiranja koja efikasno funkcioniše
samo za relativno mali broj elemenata koji se sortiraju. Za veći broj elemenata ova metoda
je prespora. Stoga se ova metoda vrlo retko upotrebljava osim za edukacijske svrhe.
Algoritam bubble sort ili „sortiranje mehurom“ je dobio naziv po tome što elementi
poput mehura „isplivaju“ na svoju poziciju. Algoritam se sastoji od 𝑛 iteracija, gde u
svakoj iteraciji zamenimo mesta susednim elementima ukoliko stoje u pogrešnom
redosledu. Ukoliko se krećemo kroz niz od prvog do poslednjem elementa posle 𝑖-te
iteracije će „najvećih“ 𝑖 elemenata biti sortirani i nalaziće se na pozicijama na kojima treba
da se nalaze u sortiranom nizu, a ukoliko se krećemo kroz niz od poslednjeg do prvog
elementa posle 𝑖-te iteracije će se „najmanjih“ 𝑖 elemenata nalaziti na prvih 𝑖 pozicija u
nizu i biće dobro raspoređeni.
==========================================================================
int sortiran(int a[], int n) {
int i;
for (i = 0; i < n - 1; i++)
if (a[i] > a[i + 1])
return 0;
return 1;
}
===============================================================================
7. 6
==========================================================================
void bubblesort(int a[], int n) {
int bilo_razmena, i;
do {
bilo_razmena = 0;
for (i = 0; i < n - 1; i++)
if (a[i] > a[i + 1]) {
razmeni(a, i, i+1);
bilo_razmena = 1;
}
} while (bilo_razmena);
}
===============================================================================
Pretpostavimo da se krećemo od poslednjeg do prvog elementa u nizu u svakoj
iteraciji. U prvoj iteraciji „najmanji“ element će zavšiti na prvom mestu u nizu, pošto će za
njega uvek važiti da je element koji stoji pre njega veći od njega, te će zameniti mesta.
Pošto se krećemo nizom od poslednjeg do prvog elementa, „najmanji“ element će menjati
mesta sa svim elementima dok ne bude postavljen na prvu poziciju. Posle druge iteracije
će drugi „najmanji“ element biti postavljen na drugu poziciju, itd. U 𝑖-toj iteraciji će 𝑖-ti
„najmanji“ element biti postavljen na 𝑖-to mesto.
Primer: broj članova niza: n = 5
članovi niza: [5, 3, 8, 4, 6]
Slika 1. Grafički prikaz rada Bubble sort-a
Naredna funkcija bubble sort algoritmom sortira niz a, dužine n.
8. 7
===========================================================================
void bubblesort(int a[], int n) {
do {
int i;
for (i = 0; i < n - 1; i++)
if (a[i] > a[i + 1])
razmeni(a, i, i+1);
n--;
} while (n > 1);
}
===============================================================================
Svojstvo algoritma koje obezbeduje zaustavljanje je da se nakon svake iteracije
spoljašnje petlje sledeći najveći elemenat koji nije vež bio na svojoj poziciji dolazi na nju.
Bubble sort je na osnovu ovog svojstva i dobio ime (jer veliki elementi kao mehurići
„isplivavaju“ ka kraju niza). Ovo sa jedne strane obezbeduje zaustavljanje algoritma, dok
se sa druge strane može iskoristiti za optimizaciju. Ovom optimizacijom se može smanjiti
broj poredenja, ali ne i broj razmena.
Bubble sort algoritam se smatra veoma lošim algoritmom. Neki autori čak
zagovaraju tezu da bi ga trebalo potpuno izbaciti iz nastave računarstva.
5. Sortiranje umetanjem (insertion sort)
Ova metoda slična je prethodnoj, ali je ipak nešto efikasnija. Sortiranje umetanjem
(engl. Insertion sort) je jednostavan algoritam za sortiranje, koji gradi završni sortirani niz
jednu po jednu stavku. Mnogo je manje efikasan na većim listama od mnogo složenijih
algoritama kao što su quicksort ili mergesort.
Međutim sortiranje umetanjem ima svoje prednosti:
jednostavna primena,
efikasan na malim skupovima podataka,
prilagodljiviji za skupove podataka koji su već značajno sortirani,
efikasniji u praksi od većine drugih kvadratnih algoritama, kao što su
selection sort ili bubble sort,
stabilan tj. ne menja relativni redosled elemenata sa jednakim vrednostima.
9. 8
Algoritam je dobio ime po tome što kad sortiramo prvih 𝑖−1 elemenata, 𝑖-ti element
se postavlja na neku poziciju {1,2,3,…,𝑖} da tako posle ubacivanja prvih 𝑖 elemenata niza
budu sortirani.
Pretpostavimo da su sortirani elementi na pozicijama {1,2,3…,𝑖−1}, nas zanima
gde treba da postavimo 𝑖-ti element tako da nakon ubacivanja prvih 𝑖 elemenata budu
sortirani. Uzimajući u obzir da su prvih 𝑖−1 elemenata sortirani, dovoljno je da nađemo
najveće 𝑗 za koje važi 𝑎𝑗≤𝑎𝑖, tj. prvi element sa manjom pozicijom u nizu od 𝑖 koji nije
veći od 𝑎𝑖. Kada nađemo takvo 𝑗, potrebno je ubaciti element 𝑎𝑖 između elemenata na
pozicijama 𝑗 i 𝑗+1 kako bismo dobili sortiranih prvih 𝑖 elemenata. To ubacivanje možemo
uraditi tako što ćemo pomeriti sve elemente posle pozicije 𝑗 za jedno mesto u desno, i
postaviti element, koji je bio na poziciji 𝑖 pre pomeranja, na poziciju 𝑗+1. Posle opisanih
tranformacija nad nizom, dobijamo da su prvih 𝑖 elemenata niza sortirani. Ukoliko ovo
nastavimo da radimo za svako 𝑖 do 𝑛, na kraju ćemo dobiti sortirani ceo niz.
Dakle, insertion sort se može formulisati na sledeći način: „Ako niz ima više od
jednog elementa, sortiraj rekurzivno sve elemente ispred poslednjeg, a zatim umetni
poslednji u već sortirani prefiks.“ Ovim se dobija sledeća (rekurzivna) implementacija.
Kada se eliminiše rekurzija, dolazi se do sledeće iterativne implementacije.
===========================================================================
void insertionsort(int a[], int n) {
if (n > 1) {
insertionsort(a, n-1);
umetni(a, n-1);
}
}
===============================================================================
===========================================================================
void insertionsort(int a[], int n) {
int i;
for (i = 1; i < n; i++)
umetni(a, i);
}
===============================================================================
10. 9
Invarijanta petlje je da je deo niza a[0, i-1] sortiran, kao i da se (multi)skup
elemenata u nizu a ne menja. Kako bi se obezbedila korektnost, preduslov funkcije umetni
je da je sortiran deo niza a[0, i-1], dok ona treba da obezbedi postuslov da je sortiran
deo niza a[0, i]. Funkcija umetni može biti implementirana na različite načine.
Jedna od mogućnosti je da se element menja sa svojim prethodnikom sve dok je
prethodnik veći od njega.
Prikažimo još i odgovarajuću rekurzivnu implementaciju.
Slika 2. Grafički prikaza rada Insertion sort-a
===========================================================================
void umetni(int a[], int i) {
int j;
for(j = i; j > 0 && a[j] > a[j-1]; j--)
razmeni(a, j, j-1);
}
===============================================================================
===========================================================================
void umetni(int a[], int j) {
if (j > 0 && a[j] < a[j-1]) {
razmeni(a, j, j-1);
umetni(a, j-1);
}
}
===============================================================================
11. 10
6. Quick sort sortiranje
Ovo je vrlo često korišćen algoritam sortiranja koji se pokazao brzim i
jednostavnim. Quicksort koristi jedan od osnovnih algoritamskih principa - podeli pa
vladaj, koji se zasniva na sledećoj ideji:
1. Podeli - ukoliko je problem koji rešavamo veoma mali, reši ga koristeći brute
force. U suprotnom podeli problem na više potproblema (uglavnom 2),
uglavnom istih veličina.
2. Vladaj - koristeći rekurziju reši svaki potproblem
3. Kombinuj - kombinujući rešenja svakog od potproblema, i tako reši prvobitni
problem.
U Quick sortu osnovna ideja je da se niz podeli u dve grupe elemenata: prvu grupu
čine svi elementi koji su manji ili jednaki izabranom elementu niza a drugu grupu svi
elementi koji su veći od izabranog elementa. Izabrani element se postavlja između grupa,
na svoje mesto, i tokom daljeg sortiranja niza neće biti potrebe za njegovim premeštanjem.
Takođe, svi elemeneti prve grupe ostaće levo od izabranog elementa, a svi elementi druge
grupe ostaće desno od izabranog elementa. Svaka od grupa se posebno sortira tako što se
isti postupak rekurzivno ponavlja. Deo niza koji sortiramo definišemo njegovom levom i
desnom granicom.
Prilikom podele niza cilj je da grupe sadrže približno jednak broj elemenata upravo
se na taj način postiže veća brzina ovog algoritma sortiranja. Element u odnosu na koji se
vrši podela zovemo pivot. Izbor pivota u značajnoj meri utiče na brzinu algoritma. Podela
niza često se vrši u odnosu na prvi, poslednji, srednji element niza ili u odnosu na srednji
po veličini od ta tri elementa niza. Još bolje je izbor pivota realizovati na slučajan način
(slučajnim izborom nekog elementa niza, izborom srednjeg po veličini od tri slučajno
odabrana elementa niza, i slično).
Slika 3. Raspodela niza na dva podniza oko odabranog "pivot-a" x
12. 11
Ključni korak quick sort je tzv. korak particionisanja koji nakon izbora nekog
pivotirajućeg elementa podrazumeva da se niz organizuje da prvo sadrži elemente manje
od pivota, zatim pivotirajući element, i na kraju elemente veće od pivota.
Algoritam se može implementirati na sledeći način. Poziv qsort_(a, l, d)
sortira deo niza a[l, d].
Funkcija qsort se onda jednostavno implementira.
Funkcija izbor_pivota odabire za pivot neki element niza a[l, d] i vraća
njegov indeks (u nizu a). Pozivom funkcije razmeni pivot se postavlja na poziciju l.
Funkcija particionisanje vrši particionisanje niza (pretpostavljajući da se pre
particionisanja pivot nalazi na poziciji l) i vraća poziciju na kojoj se nalazi pivot nakon
particionisanja. Funkcija se poziva samo za nizove koji imaju više od jednog elementa te
joj je preduslov da je l manje ili jednako d. Postuslov funkcije particionisanje je da je
(multi) skup elemenata niza a nepromenjen nakon njenog poziva, medjutim njihov
redosled je takav da su svi elementi niza a[l, p-1] manji ili jednaki elementu a[p],
dok su svi elementi niza a[p+1, d] veći ili jednaki od elementa a[p].
===========================================================================
void qsort_(int a[], int l, int d) {
if (l < d) {
razmeni(a, l, izbor_pivota(a, l, d));
int p = particionisanje(a, l, d);
qsort_(a, l, p - 1);
qsort_(a, p + 1, d);
}
}
===============================================================================
===========================================================================
void qsort(int a[], int n) {
qsort_(a, 0, n-1);
}
===============================================================================
13. 12
Implementacije particionisanja. Jedna od mogućih implementacija koraka
particionisanja je sledeća:
Invarijanta petlje je da je (multi)skup elemenata u nizu a nepromenjen, kao i da se
u nizu a na poziciji l nalazi pivot, da su elementi a[l+1, p] manji od pivota, dok su
elementi a[p+1, j-1] veći ili jednaki od pivota. Nakon završetka petlje, j ima
vrednost d+1, te su elementi a[p+1, d] veći ili jednaki od pivota. Kako bi se ostvario
postuslov funkcije particionisanje vrši se još razmena pivota i elementa na poziciji p –
time pivot dolazi na svoje mesto (na poziciju p).
7. Sortiranje spajanjem (merge sort)
Merge sort je jedan od algoritama koji i u najgorem slučaju garantuje složenost
𝑂(𝑛log𝑛). Međutim, zbog konstante iza ove složenosti, quicksort se češće koristi. Kao i
quicksort, merge sort radi po principu podeli pa vladaj.
Slika 5. Primer "mergesort" metode
===========================================================================
int particionisanje(int a[], int l, int d) {
int p = l, j;
for (j = l+1; j <= d; j++)
if (a[j] < a[l])
razmeni(a, ++p, j);
razmeni(a, l, p);
return p;
}
===============================================================================
14. 13
Dva već sortirana niza se mogu objediniti u treći sortirani niz samo jednim
prolaskom kroz nizove (tj. u linearno vremenu 𝑂(𝑚 + 𝑛) gde su 𝑚 i 𝑛 dimenzije polaznih
nizova).
U prikaznoj implementaciji, paralelno se prolazi kroz nizove a dimenzije m i b
dimenzije n. Promenljiva I čuva tekuću pozicija u nizu a, dok promenljiva j čuva tekuću
poziciju u nizu b. Tekući elementi se porede I manji se upisuje u niz c (na tekuću poziciju
k), pri čemu se napreduje samo u nizu iz koga je taj manji element uzet. Prolazak se ne
stigne do kraja jednog od nizova. Kada se kraći niz isprazni, eventualni preostali elementi
iz dužeg niza se nadovezuju na kraj niza c.
Merge sort algoritam deli niz na dve polovine (čija se dužina razlikuje najviše za
1), rekurzivno sortira svaku od njih, i zatim objedinjuje sortirane polovine. Problematično
je što je za objedinjavanje neophodno koristiti dodatni niz pomoćni niz. Na kraju se
izvršava vraćanje objedinjenog niza iz pomoćnog u polazni. Izlaz iz rekurzije je slučaj
jednočlanog niza (slučaj praznog niza ne može da nastupi).
===========================================================================
void merge(int a[], int m, int b[], int n, int c[]) {
int i, j, k;
i = 0, j = 0, k = 0;
while (i < m && j < n)
c[k++] = a[i] < b[j] ? a[i++] : b[j++];
while(i < m) c[k++] = a[i++];
while(j < n) c[k++] = b[j++];
}
===============================================================================
15. 14
Funkicija mergesort_ merge sort algoritmom sortira deo niza a[l, d], uz
korišćenje niza tmp kao pomoćnog.
Promenljiva n čuva broj elemenata koji se sortiraju u okviru ovog rekurzivnog poziva, a
promenljiva s čuva središnji indeks u nizu izmedu l i d. Rekurzivno se sortira
n1 = n/2 elemenata izmedu pozicija l i s-1 i n2 = n - n/2 elemenata izmedu
pozicija s i d. Nakon toga, sortirani podnizovi se objedinjuju u pomoćni niz tmp.
Ovaj pristup merge sort-u, gde krenemo sa čitavim nizom i rekurzivno delimo na
dva niza, se naziva top-down merge sort. Alternativa je da krenemo sa 𝑛 nizova od po
jednim elementom i spajamo susedne nizova, pa tako posle ovog koraka dobijamo 𝑛/2
nizova sa po 2 elementa. Daljom primenom istog dobijamo 𝑛/4 nizova sa po 4 elementa,
itd. Na kraju dobijamo sortirani niz. Ovaj pristup se naziva bottom-up merge sort.
Slika 6. Grafički prikaza rada Merge sort
===========================================================================
void mergesort_(int a[], int l, int d, int tmp[]) {
if (l < d) {
int i, j;
int n = d - l + 1, s = l + n/2;
int n1 = n/2, n2 = n - n/2;
mergesort_(a, l, s-1, tmp);
mergesort_(a, s, d, tmp);
merge(a + l, n1, a + s, n2, tmp);
for (i = l, j = 0; i <= d; i++, j++)
a[i] = tmp[j];
}
}
===============================================================================
16. 15
8. Zaključak
Algoritmi sortiranja su važan deo upravljanja podataka. Svaki algoritam ima
određene prednosti i nedostatke te u mnogim slučajevima najbolja stvar za učiniti je samo
koristiti ugrađene funkcije za sortiranje qsort. Za vreme kada to nije opcija i kad samo
trebate brzo i neuredno sortiranje pomoću algoritama, postoji mogućnost izbora.
Većina algoritama za sortiranje radi se pomoću upoređivanja podataka koji se
sortiraju. Mnogi algoritmi koji imaju istu učinkovitost nemaju istu brzinu na istom ulazu.
Neki algoritmi, kao što je quicksort, će obavljati dobro za neke ulaze, ali loše za druge.
Ostali algoritmi kao što su merge sort, ne zavise od vrste ulaznih podataka.
Na kraju, spomenimo da podaci ne moraju biti uvek smešteni u nizovima linearno
u memoriji (znači jedan iza drugoga) već mogu biti smešteni i u tzv. vezanim listama, a
baš o tom načinu smeštanja podataka takođe zavisi izbor najboljeg algoritma za sortiranje.
Ovde navedeni “merge-sort” algoritam zapravo postiže još bolje rezultate kada su podaci
smešteni u vezanim listama, a ne u nizovima (jednodimenzionalnim poljima).