Sveučilište J. J. Strossmayera u Osijeku Odjel za matematiku Sveučilišni nastavnički studij matematike i informatike. Sortiranje u linearnom vremenu

Similar documents
Projektovanje paralelnih algoritama II

Algoritam za množenje ulančanih matrica. Alen Kosanović Prirodoslovno-matematički fakultet Matematički odsjek

TEORIJA SKUPOVA Zadaci

Slika 1. Slika 2. Da ne bismo stalno izbacivali elemente iz skupa, mi ćemo napraviti još jedan niz markirano, gde će

Mathcad sa algoritmima

ZANIMLJIV NAČIN IZRAČUNAVANJA NEKIH GRANIČNIH VRIJEDNOSTI FUNKCIJA. Šefket Arslanagić, Sarajevo, BiH

Fajl koji je korišćen može se naći na

LINEARNI MODELI STATISTIČKI PRAKTIKUM 2 2. VJEŽBE

Sortiranje podataka. Ključne riječi: algoritmi za sortiranje, merge-sort, rekurzivni algoritmi. Data sorting

PRIPADNOST RJEŠENJA KVADRATNE JEDNAČINE DANOM INTERVALU

KVADRATNE INTERPOLACIJSKE METODE ZA JEDNODIMENZIONALNU BEZUVJETNU LOKALNU OPTIMIZACIJU 1

Red veze za benzen. Slika 1.

Metode praćenja planova

pretraživanje teksta Knuth-Morris-Pratt algoritam

ALGORITMI PODIJELI PA VLADAJ

Šime Šuljić. Funkcije. Zadavanje funkcije i područje definicije. š2004š 1

Rekurzivni algoritmi POGLAVLJE Algoritmi s rekurzijama

Mehurasto sortiranje Brzo sortiranje Sortiranje učešljavanjem Sortiranje umetanjem. Overviev Problemi pretraživanja Heš tabele.

Metode izračunavanja determinanti matrica n-tog reda

KLASIFIKACIJA NAIVNI BAJES. NIKOLA MILIKIĆ URL:

KRITERIJI KOMPLEKSNOSTI ZA K-MEANS ALGORITAM

Sorting algorithms. Sorting algorithms

Ariana Trstenjak Kvadratne forme

NIZOVI I REDOVI FUNKCIJA

Maja Antolović Algoritmi u teoriji brojeva

Sveučilište J. J. Strossmayera u Osijeku Odjel za matematiku DIOFANTSKE JEDNADŽBE

Fibonaccijev brojevni sustav

Quasi-Newtonove metode

Geometrijski smisao rješenja sustava od tri linearne jednadžbe s tri nepoznanice

Metoda parcijalnih najmanjih kvadrata: Regresijski model

Karakteri konačnih Abelovih grupa

Hornerov algoritam i primjene

Formule za udaljenost točke do pravca u ravnini, u smislu lp - udaljenosti math.e Vol 28.

GENERALIZIRANI LINEARNI MODELI. PROPENSITY SCORE MATCHING.

BROJEVNE KONGRUENCIJE

UNIVERZITET U BEOGRADU MATEMATIČKI FAKULTET

ALGORITAM FAKTORIZACIJE GNFS

BROWNOV MOST I KOLMOGOROV-SMIRNOVLJEVA STATISTIKA

Uvod u relacione baze podataka

Ivan Soldo. Sažetak. U članku se analiziraju različiti načini množenja matrica. Svaki od njih ilustriran je primjerom.

Prsten cijelih brojeva

Pitagorine trojke. Uvod

Matrice u Maple-u. Upisivanje matrica

Funkcijske jednadºbe

O aksiomu izbora, cipelama i čarapama

Asocijativna polja POGLAVLJE Ključevi kao cijeli brojevi

Optimizacija Niza Čerenkovljevih teleskopa (CTA) pomoću Monte Carlo simulacija

Nilpotentni operatori i matrice

Linearno programiranje i primjene

HRVATSKA MATEMATIČKA OLIMPIJADA

MATHEMATICAL ANALYSIS OF PERFORMANCE OF A VIBRATORY BOWL FEEDER FOR FEEDING BOTTLE CAPS

DISTRIBUIRANI ALGORITMI I SISTEMI

Mirela Nogolica Norme Završni rad

Sveučilište J.J.Strossmayera u Osijeku Odjel za matematiku. Sveučilišni preddiplomski studij matematike

SITO POLJA BROJEVA. Dario Maltarski PRIRODOSLOVNO MATEMATIČKI FAKULTET MATEMATIČKI ODSJEK. Diplomski rad. Voditelj rada: Doc. dr. sc.

Neprekidan slučajan vektor

Sveučilište J. J. Strossmayera u Osijeku Odjel za matematiku

Simetrične matrice, kvadratne forme i matrične norme

Matematika (PITUP) Prof.dr.sc. Blaženka Divjak. Matematika (PITUP) FOI, Varaždin

Mersenneovi i savršeni brojevi

Turingovi strojevi Opis Turingovog stroja Odluµcivost logike prvog reda. Lipanj Odluµcivost i izraµcunljivost

Sveučilište Josipa Jurja Strossmayera u Osijeku Odjel za matematiku

Teorem o reziduumima i primjene. Završni rad

Osobine metode rezolucije: zaustavlja se, pouzdanost i kompletnost. Iskazna logika 4

Pellova jednadžba. Pell s equation

Uvod u numericku matematiku

Nelder Meadova metoda: lokalna metoda direktne bezuvjetne optimizacije

Algoritam za odre divanje ukupnog poravnanja dva grafa poravnanja parcijalnog ure daja

Položaj nultočaka polinoma

Procjena funkcije gustoće

Oracle Spatial Koordinatni sustavi, projekcije i transformacije. Dalibor Kušić, mag. ing. listopad 2010.

ANALYSIS OF THE RELIABILITY OF THE "ALTERNATOR- ALTERNATOR BELT" SYSTEM

FIZIKALNA KOZMOLOGIJA VII. VRLO RANI SVEMIR & INFLACIJA

Sveučilište J.J. Strossmayera u Osijeku Odjel za matematiku. Velibor Gojić. Blok dizajni. Diplomski rad. Osijek, 2014.

Vedska matematika. Marija Miloloža

Iskazna logika 1. Matematička logika u računarstvu. oktobar 2012

Harmonijski brojevi. Uvod

Numeričke metode u ekonomiji Dr. sc. Josip Matejaš, EFZG

U ovom dijelu upoznat ćemo strukturu podataka stablo, uvesti osnovnu terminologiju, implementaciju i algoritme nad tom strukturom.

PARALELNI ALGORITMI ZA PROBLEM GRUPIRANJA PODATAKA

Uvod u analizu (M3-02) 05., 07. i 12. XI dr Nenad Teofanov. principle) ili Dirihleov princip (engl. Dirichlet box principle).

Sveučilište J.J. Strossmayera u Osijeku. Odjel za matematiku. David Komesarović. Mooreovi grafovi. Diplomski rad. Osijek, 2017.

Iterativne metode za rješavanje linearnih sustava

Matrične dekompozicije i primjene

Sveučilište u Zagrebu Fakultet prometnih znanosti Diplomski studij. Umjetna inteligencija - Genetski algoritmi 47895/47816 UMINTELI HG/

DISKRETNI LOGARITAM. 1 Uvod. MAT-KOL (Banja Luka) ISSN (p), ISSN (o) Vol. XVII (2)(2011), 43-52

AKSIOM IZBORA I EKVIVALENCIJE

Matrice traga nula math.e Vol. 26. math.e. Hrvatski matematički elektronički časopis. Matrice traga nula. komutator linearna algebra. Sažetak.

Konstrukcija i analiza algoritama

Matea Ugrica. Sveučilište J. J. Strossmayera u Osijeku Odjel za matematiku Sveučilišni diplomski studij matematike i računarstva

Fraktalno Brownovo gibanje

Shear Modulus and Shear Strength Evaluation of Solid Wood by a Modified ISO Square-Plate Twist Method

DES I AES. Ivan Nad PRIRODOSLOVNO MATEMATIČKI FAKULTET MATEMATIČKI ODSJEK. Diplomski rad. Voditelj rada: doc.dr.sc.

POOPĆENJE KLASIČNIH TEOREMA ZATVARANJA PONCELETOVOG TIPA

VIŠESTRUKO USPOREĐIVANJE

PRIRODOSLOVNO MATEMATIČKI FAKULTET MATEMATIČKI ODSJEK. Marina Zrno KOMUTATIVNI PRSTENI. Diplomski rad. Voditelj rada: prof.dr.sc.

Zadatci sa ciklusima. Zadatak1: Sastaviti progra koji određuje z ir prvih prirod ih rojeva.

PRIRODOSLOVNO MATEMATIČKI FAKULTET MATEMATIČKI ODSJEK BINARNI POLINOMI. Diplomski rad. Voditelj rada: doc. dr. sc. Goranka Nogo. Zagreb, 2017.

Formalni postupci u oblikovanju računalnih sustava

Krivulja središta i krivulja fokusa u pramenu konika. konika zadanom pomoću dviju dvostrukih točaka u izotropnoj ravnini

Transcription:

Sveučilište J. J. Strossmayera u Osijeku Odjel za matematiku Sveučilišni nastavnički studij matematike i informatike Tibor Pejić Sortiranje u linearnom vremenu Diplomski rad Osijek, 2011.

Sveučilište J. J. Strossmayera u Osijeku Odjel za matematiku Sveučilišni nastavnički studij matematike i informatike Tibor Pejić Sortiranje u linearnom vremenu Diplomski rad Mentor: doc. dr. sc. Domagoj Matijević Osijek, 2011.

Sadržaj Uvod 1 1 Algoritmi za sortiranje 3 1.1. Optimalno sortiranje............................ 4 1.2. Counting sort................................ 6 1.3. Radix sort.................................. 8 1.4. Bucket sort................................. 11 1.5. Flash sort.................................. 14 2 Testiranje algoritama za sortiranje 18 Zaključak 28 Literatura 29 Sažetak 30 Životopis 31 i

Uvod I prije računala postojali su algoritmi. A, danas kada postoje računala, postoji još više algoritama i oni se nalaze u srcu računala. No, što su to zapravo algoritmi? Ukratko, algoritam je svaka dobro definirana procedura koja na ulazu prima neki skup vrijednosti te na temelju njih proizvodi neki drugi skup vrijednosti na izlazu. Također, s računalne strane, na algortitam možemo gledati kao na alat za rješavanje određenog računalnog problema. Na primjer, ako želimo pronaći rješenje sustava linearnih jednadžbi, možemo koristiti Gaussovu metodu (algoritam) eliminacije. Ako želimo šifrirati dani tekst koristeći određeni ključ, možemo koristiti AES ili neki drugi moderni algoritam za šifriranje. Jedan od fundamentalnih problema u konstrukciji algoritama i programiranju općenito jest problem sortiranja. Cilj svakog algoritma za sortiranje jest pronaći permutaciju p(1)p(2)... p(n) danog niza brojeva a 1, a 2,..., a n takvu da je a p(1) a p(2)... a p(n). Postoji više razloga zašto se sortiranje smatra tako važnim problemom u području informatičkih znanosti: Proučavajući različite algoritme za sortiranje možemo dobiti dobar uvid u mnoge elementarne pojmove, važne u analizi i dizajnu algoritama, kao što su asimptotska notacija, podijeli pa vladaj algoritmi, razilčite strukture podataka itd. Iz tog razloga sortiranje zauzima važno mjesto u mnogim informatičkim udžbenicima. Ponekad okolnosti u primjeni zahtijevaju sortiranje. Zamislimo samo rječnik u kojemu riječi nisu poredane leksikografski. Pretraživanje sortirane liste je znatno brže, nego pretaživanje nesortirane liste podataka. Mnogi složeniji algoritmi se koriste sortiranjem. Primjerice, program koji obrađuje grafičke objekte koji su postavljeni jedan iznad drugog prvo treba sortirati objekte prema relaciji iznad, a tek onda nacrtati sve objekte po redu. Također, o važnosti algoritama za sortiranje govori i činjenica da mnogi programi za ispitivanje performansi računalnog hardwarea koriste sortiranje kao jedan od testova koje provode. 1

Kao posljedica toga razvijen je velik broj različitih algoritama za sortiranje, od onih jednostavnijih kao što su bubble sort i insertion sort, čije vrijeme izvršenja odgovara Θ(n 2 ), do onih složenijih kao što su merge sort i heap sort, čije je vrijeme izvršenja jednako Θ(n lg n). Postoje čak i algoritmi koji su izmišljeni kao informatičke šale, primjerice bogo sort s prosječnim vremenom izvršenja O(n n!). No, tema ovog rada su algoritmi koji, uz određene, dodatne, pretpostavke, imaju linearno vrijeme izvršenja i koji, kako ćemo kasnije vidjeti, čine, asimptotski gledano, klasu najbržih algoritama za sortiranje. U poglavlju 1 je dat kratak opis najvažnijih metoda koje koriste algoritmi za sortiranje, a zatim je u prvom dijelu razmatrano pitanje optimalne složenosti algoritama za sortiranje. Ostatak poglavlja je posvećen teorijskom razmatranju algoritama counting sort, radix sort, bucket sort i flash sort koji pod određenim uvjetima imaju linearno vrijeme izvršenja. Poglavlje 2 je zapravo eksperimentalni dio rada, u kojemu su testirane C++ implementacije algoritama razmatranih u poglavlju 1 te su njihova vremena izvršenja uspoređena sa STL funkcijom sort(). 2

Poglavlje 1 Algoritmi za sortiranje Već smo prije vidjeli da postoje različiti algoritmi za sortiranje. Prema tehnici kojom se koriste svi algoritmi za sortiranje se ugrubo mogu podijeliti u sljedećih osam skupina: 1. Insertion. Elemente niza promatramo jedan po jedan te svaki novi element umećemo na odgovarajuće mjesto u odnosu na već sortirani dio niza. Ovo je način na koji većina igrača slaže karte. Prirodno, najvažnije predstavnik ove skupine algoritama je insertion sort. 2. Exchange. Ako pronađemo dva elementa koja nisu u odgovarajućem redosljedu (jedan u odnosu na drugi), onda ih zamjenimo i taj postupak ponavljamo sve dok ni jedna zamjena više nije potrebna. Najpoznatiji predstavnici ove metode sortiranja su bubble sort i quick sort. 3. Selection. Prvo pronađemo najmanji element te ga nekako odvojimo od ostalih, zatim sljedeći najmanji itd. Ovom tehnikom se koriste selection sort i heap sort. 4. Merging. Ideja ove tehnike leži u spajanju dvaju prethodno sortiranih podnizova u jedan novi sortirani niz. Najvažniji algoritam koji koristi ovu metodu jest merge sort. 5. Enumeration. Sekvencijalno prolazeći kroz niz za svaki element prebrojavamo koliko ima elemenata koji su manji te koristeći to znanje određujemo konačnu poziciju svakog od elemenata. Primjer algoritma koji koristi ovu tehniku jest counting sort. 6. Distribution. Ideja algoritama koji koriste ovu metodu je upravo suprotna onima iz skupine 4. Ovdje se početni niz dijeli u više manjih podnizova tako da su svi elementi podniza 1 manji od svakog od elemenata podniza 2. U ovu skupinu pripadaju bucket sort, flash sort i radix sort. 7. Luck. Nasumice mješamo elemente niza, sve dok ne dobijemo sortirani niz. Ovo je očito vrlo neučinkovita metoda, uzimajući u obzir građu današnjh računala. 3

S druge strane, ova metoda je vrlo zanimljiva za implementaciju na kvantnim računalima. Predstavnik ove metode je već spomenuti bogo sort. 8. Special. U ovu skupinu pripadaju svi algoritmi koji zahtijevaju vrlo specijalne ulazne podatke ili pak zahtijevaju poseban hardware. Primjeri takvih algoritama su pancake sort, spaghetti sort i mrežno sortiranje. Dakako, postoje i hibridni algoritmi, odnosno, oni koji koriste više od jedne od navedenih tehnika i upravo se takvi algoritmi najviše koriste u praksi. Osim navedenih tehnika sortiranja postoji još jedno svojstvo koje će nam biti zanimljivo u daljnjoj raspravi, a to je stabilnost. Definicija 1.1. Kažemo da je algoritam za sortiranje stabilan ako se brojevi s jednakom vrijednosti u izlaznom nizu pojavljuju u istom redosljedu kao i u ulaznom nizu. 0 1 2 3 4 0 1 2 3 4 A (5,1) (8,9) (3,2) (3,4) (8,7) A (5,1) (8,9) (3,2) (3,4) (8,7) 0 1 2 3 4 0 1 2 3 4 B (3,2) (3,4) (5,1) (8,9) (8,7) B (3,4) (3,2) (5,1) (8,7) (8,9) (a) (b) Slika 1.1: Primjer stabilnog (a) i nestabilnog (b) sortiranja (po prvome elementu) niza dvodimenzionalnih vektora. U ulaznom nizu A element (3,2) se nalazi ispred elementa (3,4). Stabilno sortiranje pod (a) zadržava njihov relativni poredak, dok je u slučaju nestabilnog sortiranja pod (b) njihov poredak zamijenjen. Ovo svojstvo je važno kada uz elemente koje sortiramo postoje i satelitski podaci kako je to slučaj na slici 1.1. Prvi element dvodimenzionalnog vektora je vrijednost po kojoj sortiramo, a sve ostalo su satelitski podaci. 1.1. Optimalno sortiranje Pored same tehnike koju koristi neki algoritam ono što nas najviše zanima jest njegovo vrijeme izvršenja. Proučavajući tako različite algoritme za sortiranje nameće nam se pitanje: Koliko brzo uopće možemo sortirati? No, prije neko što odgovorimo na to pitanje, proučimo jedan poseban model sortiranja, sortiranje uspoređivanjem (comparison sort). Jedina operacija koja je dozvoljena u ovom modelu, osim, naravno, promjene redosljeda elemenata, jest njihovo uspoređivanje koristeći relaciju. Ovaj model izgleda prilično ograničavajuće, ali je veoma važan jer ga koriste, kako algoritmi iz prve četiri skupine iz prethodnog poglavlja, tako i najčešće korišteni algoritmi ugrađeni u 4

Slika 1.2: Primjer stabla odluke za niz 5, 9, 2. i : j označava uspoređivanje elemenata a i i a j. Listovi označeni pravokutnicima označavaju konačne permutacije p(1), p(2), p(3). Osjenčani put naznačuje tijek donesenih odluka prilikom sortiranja. Postoji 3! = 6 permutacija elemenata ulaznog niza pa stablo odluke mora imati najmanje 6 listova. našim računalima. Odgovorimo, onda, na pitanje: Koliko brzo možemo sortirati uspoređivanjem? Definicija 1.2. Puno binarno stablo je stablo kod kojeg svi čvorovi, osim listova, imaju točno dva djeteta. Da bismo odgovorili na to pitanje, promotrimo model stabla odluke (puno binarno stablo). Na slici 1.2 je dan primjer takvog stabla odluke, pomoću kojega možemo sortirati nizove od 3 elementa. Svaki unutarnji čvor označava jedno uspoređivanje a i a j. Lijevo podstablo označava slučaj kad je a i a j, a desno podstablo slučaj kada je a i > a j. Kada dođemo do lista u stablu odluke, došli smo do konačne permutacije koja nam daje sortirani niz. Promotrimo sada vezu između modela stabla odluke i sortiranja uspoređivanjem za ulazni niz od n elemenata. Primijetimo da svakome sortiranju uspoređivanjem odgovara jedno stablo odluke (ili više njih ako se radi o randomiziranome algoritmu), za svaku moguću vrijednost broja n. Svaki puta kad radimo uspoređivanje, algoritam može krenuti u jednom od dva smjera, koji, u slučaju stabla odluke, odgovaraju lijevom i desnom podstablu. Takvo stablo odluke koje odgovara sortiranju uspoređivanjem, sadrži redoslijed uspoređivanja za sve moguće permutacije ulaznog niza. Da bi takvo stablo odluke bilo u stanju sortirati svaki niz od n elemenata, mora biti u stanju doći do svake od n! permutacija toga niza, što znači da broj listova u tome stablu mora biti najmanje n!. Vrijeme izvršenja (broj uspoređivanja) je jednako duljini puta od korijena do lista što je u najgorem slučaju jednako visini stabla. Dakle, donja granica visine svih stabala odluke u kojima se sve permutacije pojavljuju kao listovi jest ujedno donja granica vremena izvršenja za sve algoritme temeljene na uspoređivanju. 5

Teorem 1.1. Svaki algoritam sortiranja uspoređivanjem zahtjeva Ω(n lg n) uspoređivanja u najgorem slučaju. Dokaz. Iz prethodne rasprave znamo da je dovoljno odrediti visinu stabla odluke. Promotrimo stablo odluke visine h s l listova. Da bi stablo odluke bilo u stanju dati korektan rezultat za svaku permutaciju ulaznog niza, svaka od n! permutacija se mora pojaviti na nekome od listova, što znači da je n! l. S druge strane znamo da binarno stablo visine h ima najviše 2 h listova. Slijedi da je odakle logaritmiranjem dobivamo n! l 2 h, h lg (n!). A, kako prema Stirlingovoj aproksimaciji vrijedi slijedi da je h = Ω(n lg n). lg (n!) = n lg n n ln 2 + 1 lg n + O(1), 2 Dakle, sada smo odgovorili koliko brzo možemo sortirati uspoređivanjem. Na pitanje koliko brzo uopće možemo sortirati lako je dati odgovor. Da bismo znali na koje mjesto moramo staviti svaki od n elemenata niza, očito ih moramo sve pregledati pa je donja granica vremena izvršenja za bilo koji algoritam sortiranja jednaka Ω(n). U sljedećim poglavljima ćemo vidjeti nekoliko algoritama koji postižu tu granicu. 1.2. Counting sort Counting sort pretpostavlja da je svaki od n elemenata ulaznog niza cijeli broj iz skupa {0, 1, 2,..., k}, za neki cijeli broj k. Kada je k = O(n), izvršno vrijeme odgovara Θ(n). Counting sort za svaki element x određuje broj elemenata manjih od x te koristeći tu informaciju stavlja x na pravo mjesto u izlaznom nizu. Na primjer, ako je 11 elemenata manje od x, onda se x treba postaviti na 12. mjesto. Naravno, ovakva procedura funkcionira samo ako su svi elementi međusobno različiti. No, vidjet ćemo da se uz male modifikacije može prilagoditi tako da radi i u općem slučaju. U pseudokodu za counting sort pretpostavljamo da je ulazni niz A[1..n], te je length(a) = n. Osim toga koristimo dva dodatna niza: B[1..n], kao izlazni niz, i C[0..k], koji će sadržavati informacije o tome na koje mjesto treba staviti pojedini element. 6

Slika 1.3: Rad procedure Counting-Sort na ulaznom nizu A[1..8], gdje je svaki element od A nenegativni cijeli broj ne veći od k = 5. (a) Niz A i pomoćni niz C nakon linije 5. (b) Niz C nakon linije 8. (c)-(e) Izlazni niz B i niz C nakon jedne, dvije i tri iteracije petlje u linijama 10-12. Tamno osjenčani dijelovi niza B nisu popunjeni. (f) Konačni izgled niza B nakon sortiranja. Counting-Sort(A, B, k) 1 neka je C[0..k] novi niz (polje) 2 for i = 0 to k 3 C[i] = 0 4 for j = 1 to length(a) 5 C[A[j]] = C[A[j]] + 1 6 //C[i] sad sadrži broj elemenata jedankih i. 7 for i = 1 to k 8 C[i] = C[i] + C[i 1] 9 //C[i] sad sadrži broj elemenata manjih ili jednakih i. 10 for j = length(a) downto 1 11 B[C[A[j]]] = A[j] 12 C[A[j]] = C[A[j]] 1 Slika 1.3 ilustrira rad counting sorta. Nakon što for petlja u linijama 2-3 postavi sve elemente niza C na 0, for petlja u linijama 4-5 prolazi kroz sve elemente ulaznog niza te ako je vrijednost elementa jednaka i, inkrementiramo C[i]. Dakle, nakon linije 5, C[i] sadrži broj elemenata jednakih i, za sve i = 0, 1,..., k. Linije 7-8 određuju koliko je elemenata manjih ili jednakih i, za svaki i = 0, 1,..., k. Konačno, for petlja u linijama 10-12 stavlja svaki element A[j] na točno mjesto u izlaznom nizu B. Ako su svi od n elemenata različiti, tada za svaki A[j], vrijednost C[A[j]] sadrži točnu konačnu poziciju elementa A[j], jer postoji točno C[A[j]] elemenata manjih ili jednakih A[j]. Kako nisu svi elementi nužno različiti, vrijednost C[A[j]] smanjujemo za jedan svaki puta kad stavimo element A[j] u B. Dekrementacija C[A[j]] znači da će sljedeći element s vrijednosti jednakom onoj od A[j], ako postoji, biti postavljen točno 7

jedno mjesto prije A[j] u izlaznom nizu. Analizirajmo sada izvršno vrijeme counting sorta. For petlja u linijama 2-3 Θ(k) vremena, for petlja u linijama 4-5 zahtijeva Θ(n) vremena, for petlja u linijama 7-8 zahtijeva Θ(k), a petlja u linijama 10-12 zahtijeva Θ(n) vremena. Stoga je ukupno vrijeme izvršenja Θ(k + n). U praksi, counting sort obično koristimo kad je k = O(n) te je tada vrijeme izvršenja zapravo Θ(n). No, pogledajmo sada koliko dodatne memorije koristi counting sort. Prvo imamo izlazni niz B koji će sadržavati sortirani niz te kao takav zahtijeva Θ(n) memorije. Zatim, već u prvoj liniji imamo niz C[1..k] koji očito koristi Θ(k) memorije. Osim nizova B i C imamo još brojače (cijele brojeve) i i j čija veličina ne ovisi o ulaznom nizu A te koriste Θ(1) memorije. Dakle, ukupna memorija koju koristi counting sort, osim one za samu pohranu ulaznog niza A, je jednaka Θ(n + k). Ako pretpostavimo da je k = O(n), onda dodatna memorija koju koristi counting sort odgovara Θ(n). Kako counting sort ne sortira uspoređivanjem, on može biti (i jest) brži od donje granice Ω(n lg n) dokazane u poglavlju 1.1. Štoviše, u cijelom kodu counting sorta ne postoji ni jedno uspoređivanje. Umjesto toga, counting sort koristi vrijednosti elemenata da bi odredio njihovo konačno mjesto u nizu. Odavde možemo vidjeti da donja granica od Ω(n lg n) ne vrijedi ako se udaljimo od modela sortiranja uspoređivanjem. S druge strane, valja primijetiti da counting sort koristi Θ(n + k) dodatne memorije, u odnosu na, primjerice, quick sort koji radi u mjestu. Iz posljednje for petlje u linijama 10-12 lako se vidi da je counting sort stabilan. Osim što je ovo svojstvo važno kod sortiranja složenijih nizova (onih gdje elementi sadrže satelitske podatke), stabilnost counting sorta nam je važna iz još jednog razloga: counting sort se često koristi kao potprogram unutar radix sorta. A, kao što ćemo vidjeti u sljedećem poglavlju, da bi radix sort radio pravilno, counting sort mora biti stabilan. 1.3. Radix sort Radix sort je algoritam koji su koristili strojevi za sortiranje bušenih kartica, kakve danas možemo vidjeti samo u tehničkim muzejima. Kartice imaju 80 stupaca te 12 mjesta u svakom stupcu na kojemu može biti probušena rupa. Mašina za sortiranje je mehanički programirana da pregleda određeni stupac i rasporedi kartice u 12 odjeljaka, ovisno o tome na kojem mjestu je kartica probušena. Nakon toga bi operater prikupio kartice iz svih odjeljaka te ih redom složio tako da su na vrhu one kartice koje imaju probušeno prvo mjesto u datom stupcu, zatim one koje imaju probušeno drugo mjesto itd. Za decimalne brojeve, svaki stupac bi trebao sadržavati 10 mjesta, a broj s d znamenaka bi trebao d stupaca. Kako stroj za sortiranje može pregledati samo jedan stupac odjednom, da bismo sortirali n kartica koje sadrže d-znamenkaste brojeve, potreban 8

nam je algoritam za sortiranje. Intuitivno, mogli bismo početi sortirati po najznačajnijoj znamenci te svaki odjeljak sortirati rekurzivno, a zatim posložiti redom sve kartice počevši s odjeljkom 1, zatim odjeljkom 2 itd. Upravo opisana metoda se naziva MSD-radix sort (MSD = most significant digit). Problem koji se javlja kod te metode jest veliki broj potrebnih odjeljaka. Na primjer, da bismo sortirali naše kartice s početka priče, u najgorem slučaju bi nam bilo potrebno 12 80 različitih odjeljaka. Metoda koja će nas više zanimati u ovome radu jest LSD-radix sort (LSD = least significant digit), koju ćemo nadalje zvati jednostavno radix sort. Prema [3], prva objavljena referenca na ovaj princip sortiranja se pojavljuje u raspravi L. J. Comriea o opremi za bušene kartice [Transactions of Office Machinery Users Assoc., Ltd. (1929), 25-37, esp. p. 28]. Ovaj algoritam je vrlo sličan prethodno opisanom MSD-radix sortu. Razlika je u tome što se prvo sortira po posljednjoj, najmanje značajnoj, znamenci, zatim po pretposljednjoj itd. No, umjesto da se svaki odjeljak sortira rekurzivno, nakon sortiranja po jednoj znamenci kartice se redom prikupljaju u jedan špil i sortiranje počinje ponovo po sljedećoj znamenci. Da bi ova metoda pravilno radila, algoritam koji sortira prema znamenkama mora biti stabilan i operater koji prikuplja kartice iz odjeljaka ne smije mjenjati njihov redosljed čak ni ako se sve nalaze u istom odjeljku. Slika 1.4: Primjer rada radix sorta na nizu od 7 troznamenkastih brojeva. Prvi stupac lijevo je ulazni niz. Ostali stupci prikazuju iteracije sortiranja po znamenkama sve veće značajnosti. Sjenčanje označava znamenku po kojoj se sortira. Pseudokod radix sorta je prilično jednostavan. Sljedeća procedura pretpostavlja da svaki od n elementa niza A ima d znamenaka, gdje je 1 najmanje značajna, a d, pak, najznačajnija znamenka. Radix-Sort(A, d) 1 for i = 1 to d 2 koristeći stabilan algoritam za sortiranje sortiraj niz A po znamenci i Da u liniji 2 nismo zahtijevali stabilnost algoritma koji sortira po i-toj znamenci, moglo bi se dogoditi da po završetku rada radix sorta niz A ne bude pravilno sortiran, što je ilustrirano na slici 1.5. Do toga dolazi jer nestabilan algoritam ne zadržava relativni poredak elemenata s jednakim vrijednostima te sortiranjem po znamenci veće 9

značajnosti može pokvariti redoslije dobiven sortiranjem po manje značajnoj znamenci. Slika 1.5: Primjer rada radix sorta koji koristi nestabilni algoritam za sortiranje po i-toj znamenci. Zbog nestabilnosti u drugom stupcu su zamjenjeni brojevi 720 i 329. Slično, u trećem stupcu je promjenjen redoslijed brojeva 355 i 329 te 457 i 436, što rezultira nepravilno sortiranim nizom. Propozicija 1.1. Neka je dano n d-znamenkastih brojeva i neka svaka znamenka može poprimiti jednu od najviše k mogućih vrijednosti. Tada Radix-Sort sortira te brojeve u Θ(d(n + k)) vremenu, ako stabilni algoritam koji koristi zahtijeva Θ(n + k) vremena. Dokaz. Točnost radix sorta se lako dokazuje indukcijom po broju znamenaka. Vrijeme izvršenja ovisi o stabilnom algoritmu za sortiranje koji se koristi. Kada je svaka znamenka iz skupa {0, 1, 2,..., k 1}, uz pretpostavku da k nije suviše velik, counting sort se nameće kao očiti izbor. Svaki prolaz kroz n d-znamenkastih brojeva onda zahtijeva Θ(n + k) vremena, a kako se taj postupak mora ponoviti d puta, ukupno vrijeme izvršenja radix sorta jest Θ(d(n + k)). Dakle, kad je d konstanta i k = O(n), radix sort radi u linearnom vremenu. Općenito, ovisno o računalnom zapisu, broj možemo razbiti na različite načine na znamenke. Propozicija 1.2. Neka je dano n b-bitnih brojeva te neki pozitivan cijeli broj r b. Radix-Sort sortira te brojeve u Θ((b/r)(n+2 r )) vremenu. Uz pretpostavku da stabilan algoritam za sortiranje koji se koristi zahtijeva Θ(n + k) vremena, za elemente iz skupa {0, 1, 2,..., k}. Dokaz. Za svaki r b, na broj gledamo kao da ima d = b/r znamenaka od r bitova. Svaka znamenka je cijeli broj iz skupa {0, 1,..., 2 r 1}, tako da možemo koristiti counting sort za k = 2 r 1. Na primjer, na 32-bitnu riječ možemo gledati kao da ima četiri 8-bitne znamenke, tako da je b = 32, r = 8, k = 2 r 1 = 255 i d = b/r = 4. Svaki prolaz counting sorta nas košta Θ(n+k) = Θ(n+2 r ), a kako imamo d prolaza, ukupno vrijeme izvršenja je Θ(d(n + 2 r )) = Θ((b/r)(n + 2 r )). Pokušajmo sada, za dane n i b, odrediti takav r b da je izraz (b/r)(n + 2 r ) minimalan. Ako je b lg n, tada je za bilo koji r b, n+2 r = Θ(n). Stoga, uzimajući r = b dobivamo da je vrijeme izvršenja (b/b)(n + 2 b ) = Θ(n), što je asimptotski optimalno. Ako je b lg n uzimajući r = lg n, dobivamo asimptotski gledano 10

najbolje vrijeme od Θ(bn/ lg n). Kada bismo povećali r iznad lg n, tada bi izraz 2 r u brojniku brže rastao nego izraz r u nazivniku te bismo tako dobili izvršno vrijeme iznad Ω(bn/ lg n). Kad bismo, s druge strane, smanjili r ispod lg n, tada bi izraz b/r rastao, dok bi izraz n + 2 r ostao jednak Θ(n). Je li radix sort zaista, u praksi, brži od algoritama za sortiranje baziranih na uspoređivanju, kao što je primjerice quick sort? Ako je b = O(lg n), što je često slučaj, i ako odaberemo r lg n, tada je izvršno vrijeme radix sorta Θ(n), što je bolje od očekivanog vremena izvršenja quick sorta od Θ(n lg n). No, konstantni faktori skriveni asimptotskom notacijom se razlikuju. U poglavlju 2 ćemo vidjeti da je za velike n radix sort zaista brži. Ono na što, također, treba obratiti pažnju jest činjenica da verzija radix sorta koja koristi counting sort za stabilno sortiranje, ne sortira u mjestu, već koristi Θ(n + 2 r ) dodatne memorije. Dakle, kada je opterećenje memorije na vrhuncu, možda bi bilo bolje koristiti algoritam koji radi u mjestu, kao što je quick sort. Osim toga, treba primijetiti da se LSD-radix sort ne može u potpunosti paralelizirati (jer se sortiranja po znamenkama, koja obavlja counting sort, moraju odvijati sekvencijalno) za razliku od, primjerice, quick sorta ili merge sorta. 1.4. Bucket sort Bucket sort pretpostavlja da su ulazni podaci iz uniformne distribucije i ima očekivano vrijeme izvršenja jednako Θ(n). Kao i counting, bucket sort je brz jer prtpostavlja nešto o ulaznom nizu. Dok counting sort pretpostavlja da su ulazni podaci cijeli brojevi iz skupa {0, 1, 2,..., k}, gdje je k relativno malen, bucket sort pretpostavlja da su ulazni podaci dobiveni slučajnim procesom koji distribuira elemente uniformno i nezavisno na intervalu [0, 1). Bucket sort dijeli interval [0, 1) na n podintervala (bucket) jednake duljine te raspoređuje n ulaznih brojeva u te podintervale. Kako su podaci uniformno raspoređeni na intervalu [0, 1), očekujemo da će se u svakom podintervalu nalaziti podjednako malen broj elemenata. Da bismo dobili izlazni niz, jednostavno sortiramo elemente u svakom od podintervala te redom spojimo sve podintervale. U pseudokodu za bucket sort pretpostavljamo da je A ulazni niz od n elemenata te da svaki element A[i] zadovoljava 0 A[i] < 1. Potreban nam je dodatni niz B[0,..., n 1] vezanih listi (bucket), uz pretpostavku da postoji mehanizam za održavanje takvih listi. 11

Slika 1.6: Procedura bucket sort za n = 10. (a) Ulazni niz A[1..10]. (b) Niz B[0..9] sortiranih listi (bucket) nakon izvršenja linije 8. Lista i vrijednosti iz intervala [1/10, (i + 1)/10). Sortirani izlazni niz je sastavljen od redom spojenih listi B[0], B[1],..., B[9]. Bucket-Sort(A) 1 n= length(a) 2 neka je B[0,..., n 1] novi niz (polje) 3 for i = 0 to n 1 4 neka je B[i] prazna lista 5 for i = 1 to n 6 umetni A[i] u listu B[ na[i] ] 7 for i = 0 to n 1 8 sortiraj listu B[i] insertion sortom 9 redom spoji liste B[0], B[1],..., B[n 1] Slika 1.6 prikazuje rad procedure bucket sort na nizu od 10 brojeva. Da bismo se uvjerili da algoritam zaista radi, promotrimo dva elementa A[i] i A[j]. Bez smanjenja općenitosti pretpostavimo da je A[i] A[j]. Kako je na[i] na[j], element A[i] ide ili u istu listu kao i A[j] ili u listu s manjim indeksom. Ako se A[i] i A[j] nalaze u istoj listi, onda ih for petlja u linijama 7-8 stavlja u pravilan redosljed. Ako se A[i] i A[j] nalaze u različitim listama, tada i linija 9 stavlja u pravilan redosljed. Dakle, bucket sort radi pravilno. Prijeđimo, sada, na analizu izvršnog vremena bucket sorta. Primijetimo da sve linije osim linije 8 zahtijevaju O(n) vremena u najgorem slučaju. Dakle, trebamo analizirati ukupno vrijeme potrebno za n poziva insertion sorta u liniji 8. U svrhu analize poziva insertion sorta, neka je n i slučajna varijabla kojom modeliramo broj elemenata u listi B[i]. Kako insertion sort ima kvadratno vrijeme izvršenja, vrijedi da je izvršno vrijeme bucket sorta jednako T (n) = Θ(n) + 12 n 1 i=0 O(n 2 i ).

Odredimo, sada, očekivano vrijeme izvršenja bucket sorta. Uzimajući očekivanje na obje strane te koristaći linearnost očekivanja, imamo Tvrdimo da je E[T (n)] = E [ Θ(n) + = Θ(n) + = Θ(n) + n 1 i=0 n 1 i=0 n 1 i=0 O(n 2 i ) E[O(n 2 i )] ] O(E[n 2 i ]). (1.1) E[n 2 i ] = 2 1, i = 0, 1,..., n 1. (1.2) n Svaka lista i ima istu vrijednost E[n 2 i ], jer svaki element ulaznog niza A s jednakom vjerojatnošću upada u bilo koju od listi. Da bismo dokazali jenadžbu (1.2), definirajmo indikator slučajnu varijablu Odakle slijedi da je X ij = I{A[j] upada u listu i}, i = 0, 1,..., n 1, j = 1, 2,..., n. Izračunajmo, sada, E[n 2 i ]. n n i = X ij. j=1 2 E[n 2 n i ] = E X ij j=0 n n = E X ij X ik j=1 k=1 n = E Xij 2 + j=1 1 j n = n E[Xij] 2 + j=1 1 j n 1 k n k j 1 k n k j X ij X ik E[X ij X ik ] (1.3) Promotrimo dvije sume posebno. Kako je indikator slučajna varijabla jednaka 1 s vjerojatnošću 1/n i 0 inače, slijedi da je E[Xij] 2 = 1 2 1 ( n + 02 1 1 ) = 1 n n. Kada je k j, varijable X ij i X ik su nezavisne te je E[X ij X ik ] = E[X ij ]E[X ik ] = 1 n 1 n = 1 n 2. 13

Supstituirajući te vrijednosti nazad u jednadžbu (1.3), dobivamo E[n 2 i ] = n j=1 1 n + 1 j n 1 k n k j 1 n 2 = n 1 n + n(n 1) 1 n 2 = 1 + n 1 n = 2 1 n, čime je jednadžba (1.2) dokazana. Koristeći tu činjenicu u jednadžbi (1.1), možemo zaključiti da je očekivano vrijeme izvršenja bucket sorta jednako Θ(n) + n O(2 1/n) = Θ(n). Iz prethodne rasprave možemo zaključiti da pretpostavka o uniformnoj distribuciji nije nužna, da bi izvršno vrijeme bucket sorta bilo linearno. Dovoljno je da suma kvadrata broja elemenata u listama B[i] bude linearna u odnosu na ukupan broj elemenata ulaznog niza, a za to je dovoljno da vjerojatnosti da slučajno izabrani element upadne u bilo koju od listi budu međusobno jednake, tj. X ij = 1/n, i = 0, 1,..., n 1, j = 1, 2,..., n. Dakle, da bi očekivano vrijeme izvršenja bucket sorta bilo linearno, dovoljno je poznavati distribuciju (funkciju distribucije) iz koje dolaze ulazni podaci te prema tome odrediti granice podintervala tako da se u svakoj listi B[i] nalazi približno jednak broj elemenata ulaznog niza A. Primijetimo još da bucket sort koristi dodatni niz B, što znači da zahtijeva Θ(n) dodatne memorije. Najveća prednost bucket sorta, u odnosu na druge algoritme za sortiranje u linearnom vremenu, leži u činjenici da se bucket sort može lako i prirodno paralelizirati. 1.5. Flash sort Flash sort, kao i bucket sort, pretpostavlja da podaci dolaze iz uniformne distribucije te mu je očekivano vrijeme izvršenja jednako Θ(n). Ono što flash sort razlikuje od bucket sorta jest činjenica da flash sort obavlja sortiranje u mjestu. Umjesto da elemente početnog niza A stavlja u novi niz B, kako to radi bucket sort, flash sort koristi samo dodatni niz L[0,..., m 1] kako bi odredio koliko elemenata treba staviti u svaku od m klasa, a zatim premještajući elemente unutar niza A stavlja odgovarajuće elemente u pripadne klase. Niz L dobivamo slično kao niz C u counting sortu, samo što umjesto doslovnog prebrojavanja elemenata, prebrojavamo koliko elemenata pripada svakoj od m klasa, tako da na kraju L[i] sadrži ukupan broj elemenata u svim klasama od 0 do i. Nakon toga krećemo s permutacijama (premještanjima) elemenata unutar početnog niza A, 14

koje se odvijaju na sljedeći način. Uzmemo element A[i], odredimo klasu k kojoj taj element pripada te A[i] stavimo na mjesto L[k] (prvo nepopunjeno mjesto u klasi k), a element koji se prije nalazio na tom mjestu pamtimo, jer ćemo permutacije nastaviti upravo s tim elementom. Zatim L[k] smanjujemo za jedan jer smo popunili jedno mjesto u klasi k. Može se dogoditi da tako premještajući elemente završimo na onom mjestu s kojeg smo krenuli i prije nego što smo sve elemente stavili u pripadne klase. To znači da je završio jedan ciklus permutacija te je potrebno pronaći element s kojim možemo započeti sljedeći ciklus. Za početak ciklusa jednostavno odabiremo prvi element koji se ne nalazi u odgovarajućoj klasi. Sve pemutacije se završavaju nakon n 1 izvršenih premještanja elemenata, jer ako smo n 1 elemenata stavili u odgovarajuće klase, to znači da se i n-ti element nalazi u odgovarajućoj klasi. U pseudokodu za flash sort pretpostavljamo da je A ulazni niz od n elemenata te da svaki element A[i] zadovoljava 0 A[i] < 1. Također nam je potreban pomoćni niz L[0,..., m 1]. Flash-Sort(A) 1 n = length(a) 2 neka je L[0,..., m 1] novi niz (polje) 3 for i = 0 to m 1 4 L[i] = 0 5 for i = 1 to n 6 L[ ma[i] ] = L[ ma[i] ] + 1 7 //L[i] sad sadrži broj elemenata u klasi i 8 for i = 1 to m 1 9 L[i] = L[i] + L[i 1] 10 //L[i] sad sadrži broj elemenata u svim klasama i, odnosno gornju granicu klase i 11 hold =max(a), A[1] = hold, nmove = 0, i = 0, k = m 1 12 //nmove = broj izvršenih premutacija, k = klasa trenutno promatranog elementa 13 while nmove n 1 14 while i > L[k] 1 15 i = i + 1 16 k = ma[i] 17 flash = A[i] 18 while i L[k] 19 k = m flash 20 hold = A[L[k]] 21 A[L[k]] = flash 22 flash = hold 23 L[k] = L[k] 1 24 nmove = nmove + 1 25 sortiraj niz A insertion sortom Slika 1.7 prikazuje odvijanje zamjena u mjestu na nizu od 7 elemenata u slučaju kad se konačna permutacija sastoji od jednog (a) odnosno dva ciklusa (b). 15

Slika 1.7: Primjer rada flash sorta za n = 7 i m = 7. (a) Permutacije u mjestu se odvijaju u jednom ciklusu. (b) Permutacije se odvijaju u dva ciklusa. Petlja while u linijama 13-24 osigurava da ukupni broj premještanja elemenata ne bude veći od n, jer je to dovoljno da bi se svi elementi posložili u odgovarajuće klase. Petlja while u linijama 18-24 stavlja element f lash u odgovarajuću klasu u slučaju da postoje slobodna mjesta u toj klasi, u protivnom, znači da je trenutni ciklus je završio te while petlja u linijama 14-16 pronalazi početak novog ciklusa. Da bismo provjerili ispravnost algoritma, promotrimo elemente A[i] i A[j]. Bez smanjenja općenitosti neka je A[i] A[j]. Promotrimo slučaj kad je i < j, slučaj kad je i > j se dokazuje analogno. Kako je A[i] A[j], element A[i] ide u istu klasu kao i A[j] ili u klasu s nižim indeksom. Ako elemente A[i] i A[j] treba staviti u istu klasu, treba pokazati da će ih algoritam postaviti na različita mjesta unutar te klase. A, tomu je tako jer element A[i] linija 21 stavlja na mjesto L[ m flash ] u nizu A, nakon čega se L[ m flash ] dekrementira u liniji 23 te tako više ni jedan element neće biti stavljen na to mjesto. Konačno, linija 25 elemente A[i] i A[j] stavlja u pravi poredak. Ako se A[i] i A[j] smještaju u različite klase, onda se samim time nalaze u odgovarajućem poretku. Analizirajmo sada izvršno vrijeme flash sorta. Petlja for u linijama 5-6 zahtijeva Θ(n) vremena. For petlje u linijama 3-4 te 8-9 zahtijevaju Θ(m) vremena. Kako je u praksi m = n/k za neku malu konstantu k, znači da sve tri for petlje zahtijevaju Θ(n) vremena. While petlja u linijama 18-24 se očito odvija n puta jer se svaki element stavlja u odgavarajuću klasu samo jednom. Provjerimo još vremensku složenost while petlje u linijama 14-16. Algoritam će ući u tu petlju c 1 puta, gdje je c broj ciklusa potrebnih da bismo došli do konačne permutacije. No, valja primijetiti da ukupan broj prolazaka kroz petlju, bez obzira na broj ciklusa, ne može biti veći od n 1, jer petlja svaki puta kreće od onog elementa i na kojemu je zadnji puta stala. Ostaje za provjeriti izvršno vrijeme linije 25. Kako izvršno vrijeme insertion sorta ovisi o sumi udaljenosti elmenata od njihovih pravih mjesta, pozivanje insertion sorta na cijeli niza 16

A je ekvivalentno pozivanju insertion sorta na svaku od m klasa posebno. A, kako smo vidjeli u prethodnom poglavlju, dovoljan uvijet da bi izvršno vrijeme m poziva insertion sorta bilo linearno jest da svaka klasa sadrži približno jednak broj (n/m) elemenata. Lako je vidjeti da su bucket sort i flash sort vrlo slični. Ono što ih razlikuje jest činjenica da bucket sort koristi dodatni niz da bi premjestio elemente u odgovarajuće klase, dok flash sort radi u mjestu te na taj način koristi manje memorije. No, štedeći memoriju flash sort postaje nešto (konstantno puta) sporiji, u odnosu na bucket sort, u što ćemo se i uvjeriti u sljedećem poglavlju. 17

Poglavlje 2 Testiranje algoritama za sortiranje U prethodnom poglavlju smo proučili četiri algoritma za sortiranje, koji pod određenim pretpostavkama imaju linearno vrijeme izvršenja, što znači da bi ti algoritmi trebali biti brži od bilo kojeg algoritma koji sortira uspoređivanjem, pod uvjetom da je niz koji se sortira dovoljno velik. U ovom poglavlju ćemo provjeriti koliko su ti algoritmi zaista brzi u praksi te kako promjene određenih parametara, kao što su veličina niza, raspon i tip podataka, utječu na njihove preformanse. Svi algoritmi su implementirani u programskom jeziku C++. Implementacija radix sorta je rad A. Reinalda, P. Harrisa, R. Rohera i D. Jagdmanna [2]. Naime, riječ je o veoma dobro optimiziranoj implementaciji koja dobro radi na veoma različitim tipovima podataka, u što ćemo se i uvjeriti u ostatku ovog poglavlja. Ostale algoritme (counting sort, bucket sort i flash sort) sam implementirao samostalno. Vrijeme izvršenja je mjereno funkcijom clock() iz ctime biblioteke te uspoređeno s STL (Standard Template Libary) funkcijom sort(). Funkcija sort() jest zapravo implementacija intro sorta standardno ugrađena u Microsoft Visual Studio C++ te kao takva pretstavlja optimalnu klasu algoritama za sortiranje uspoređivanjem, tj. njeno vrijeme izvršenja je jednako Θ(n lg n). Ova implementacija intro sorta, ukratko, radi na sljedeći način: Za sortiranje se koristi median od 3 quick sort ("median od 3" znači da se za pivotni element odabire medinan od prvog, srednjeg i posljednjeg elementa, tj. p = med {A[1], A[n/2], A[n]}.) sve dok je broj particija, početnog niza od n elemenata, manji od 1.5 lg n. Ako se ta granica pređe, koristi se heap sort. Također, za n 32 se koristi insertion sort. Kako funkcija sort() radi u Θ(n lg n) vremenu, dok preostala četiri algoritma rade u linearnom vremenu. Kada kažemo da ti algoritmi rade u linearnom vremenu, na umu trebamo imati da za takvo što moraju biti ispunjene određene pretpostavke, tj. raspon podataka mora biti relativno malen za counting sort, dok za bucket sort i flash sort moramo poznavati distribuciju iz koje dolaze podaci. Pod uvjetom da su te pretpostavke ispunjene, za očekivati je da će za dovoljno velike n ti algoritmi biti brži od funkcije sort(). Za koje n ti algoritmi zaista postaju brži možemo vidjeti u tablici 2.1 te na slici 2.1. 18

n counting sort radix sort bucket sort flash sort sort() 2 487 25 14 7 5 2 2 498 26 14 8 5 2 3 511 27 15 8 8 2 4 528 27 16 8 16 2 5 537 28 20 14 41 2 6 549 28 21 14 51 2 7 555 28 22 17 113 2 8 567 36 33 26 272 2 9 578 56 64 64 651 2 10 590 101 95 106 2 800 Tablica 2.1: Vremena izvršenja, mjerena u mikrosekundama, za različite n (duljine niza) koji se sortira. Mjerenja su vršena na nizovima od n uniformno distribuiranih cijelih brojeva iz segmenta [0, 32767]. Za razliku od ostalih mjerenja, vremena izvršenja iz tablice 2.1 su, radi veće preciznosti, mjerena pomoću funkcija QueryPerformanceCounter i QueryPerformanceFrequency iz Windows.h biblioteke. Slika 2.1 zorno prikazuje kako izvršno vrijeme funkcije sort() raste znatno brže nego što je to slučaj kod preostala četiri algoritma, što je, poznavajući složenost testiranih algoritama, i bilo za očekivati. Također, valja napomenuti da se testirane implementacije algoritama za sortiranje u linearnom vremenu mogu dodatno ubrzati, za male n, koristeći insertion sort, kako to radi i sama funkcija sort(). n counting sort radix sort bucket sort flash sort sort() 2 15 3 4 3 4 66 2 16 4 5 5 6 132 2 17 7 9 8 10 275 2 18 19 18 21 24 559 2 19 26 38 73 101 1 093 2 20 49 79 201 275 2 109 2 21 93 156 453 624 3 946 2 22 202 312 999 1 248 7 769 2 23 390 655 2 060 2 496 15 850 2 24 795 1 373 4 368 5 351 31 231 2 25 1 638 2 808 9 579 11 607 61 526 2 26 3 666 5 429 20 311 25 397 126 407 Tablica 2.2: Vremena izvršenja, mjerena u milisekundama, za različite n (duljine niza) koji se sortira. Mjerenja su vršena na nizovima od n uniformno distribuiranih cijelih brojeva iz segmenta [0, 32767]. Na slici 2.2 vidimo da su sva četiri algoritma za sortiranje u linearnom vremenu brži 19

Slika 2.1: Grafički prikaz vremena izvršenja iz tablice 2.1. Slika 2.2: Grafički prikaz vremena izvršenja iz tablice 2.2. 20

od funkcije sort(), što je u skladu s našom pretpostavkom s početka ovog poglavlja. Primijetimo da se bucket sort i flash sort ponašaju vrlo slično, samo što je flash sort malo sporiji. To usporenje flash sort duguje činjenici da obavlja permutacije elemenata u mjestu, no iz istog razloga koristi nešto manje memorije. Također, zanimljivo je da podaci iz tablice 2.2 pokazuju da je radix sort koji koristi counting sort za stabilno sortiranje sporiji od samog counting sorta. Zašto bismo onda uopće željeli koristiti radix sort koji ima složeniji kod, a sporiji je od counting sorta? k counting sort radix sort bucket sort flash sort sort() 2 10 192 364 1 055 1 496 9 548 2 11 199 365 1 060 1 482 9 532 2 12 202 365 1 045 1 467 9 563 2 13 205 353 1 061 1 482 9 563 2 14 215 359 1 061 1 482 9 563 2 15 234 361 1 055 1 496 9 563 2 16 250 374 1 060 1 482 9 548 2 17 265 343 1 045 1 467 9 532 2 18 359 359 1 061 1 482 9 563 2 19 609 359 1 061 1 482 9 563 2 20 858 359 1 061 1 482 9 563 2 21 983 374 1 045 1 497 9 548 2 22 1 092 358 1 029 1 497 9 563 2 23 1 154 359 1 045 1 497 9 547 2 24 1 311 359 1 077 1 498 9 532 2 25 1 575 344 1 045 1 497 9 578 2 26 1 996 359 1 061 1 482 9 594 Tablica 2.3: Vremena izvršenja za različite k (raspone podataka). Mjerenja su vršena na nizovima od n = 5 10 6 uniformno distribuiranih cijelih brojeva iz segmenta [0, k 1]. Kao što vidimo u tablici 2.3, odnosno na slici 2.3, što je raspon podataka k veći, to je counting sort sporiji, što je očekivano, uzmemo li u obzir da je vrijeme izvršenja counting sorta Θ(n + k). S druge strane, ni jedan od preostala četiri algoritma, pa tako ni radix sort, očito ne ovisi o k. To smo mogli i očekivati jer testirana implementacija radix sorta radi na znamenkama od 8 bitova pa je k = 2 8 1 = 255, bez obzira na ukupni raspon podataka. Također, očekivana vremena izvršenja bucket sorta i flash sorta ovise isključivo o tome jesu li elementi ulaznog niza u jednakom broju raspoređeni među klasama koje se kasnije sortiraju insertion sortom. Znači, za malene k counting sort je brz, no ako je k velik, radix sort se nameće kao bolji izbor. Promotrimo sada što se događa promijenimo li duljinu bitnog zapisa. Kao što vidimo na slici 2.4, vrijeme izvršenja svih algoritama, osim funcije sort(), raste s porastom duljine bitnog zapisa. To je zato što operacije pridruživanja (=), koje čine velik dio izvršnog vremena kod tih algoritama, "koštaju" nešto više, za dulji bitni zapis. 21

Slika 2.3: Grafički prikaz vremena izvršenja iz tablice 2.3. Slika 2.4: Grafički prikaz vremena izvršenja iz tablice 2.4. 22

b counting sort radix sort bucket sort flash sort sort() 8 140 94 406 452 4 883 16 156 156 390 437 4 883 32 172 344 577 671 4 805 64 374 780 780 967 5 039 Tablica 2.4: Vremena izvršenja za različite b (duljine bitnog zapisa). Mjerenja su vršena na nizovima veličine n = 5 10 6 uniformno distribuiranih cijelih brojeva iz segmenta [0, 255]. Tip podataka korišten u testiranju je b-bitni unsigned integer. No, od svih algoritama, posebno se ističe radix sort, čije vrijeme izvršenja rasta znatno brže nego kod ostalih. To je u skladu s propozicijom 1.2, prema kojoj je složenost algoritma radix sorta jednaka Θ((b/r)(n+2 r )), što znači da izvršno vrijeme radix sorta linearno ovisi o duljini bitnog zapisa b. Dosad smo se isključivo bavili nizovima čiji su elementi uniformno distribuirani cijeli brojevi, na kojima bucket sort i flash sort imaju linearno očekivano vrijeme izvršenja. Promotrimo sada što se događa kada ako brojevi nisu iz uniformne distribucije. radix sort bucket sort flash sort sort() n unif Cauchy unif Cauchy unif Cauchy unif Cauchy 2 15 4 4 3 105 4 154 66 62 2 16 5 5 5 310 6 313 132 132 2 17 9 9 8 622 10 623 275 274 2 18 18 19 21 1 236 24 1 239 559 549 2 19 38 37 73 2 459 101 2 459 1 093 1 123 2 20 79 77 201 4 895 275 4 895 2 109 2 090 2 21 156 156 453 9 563 624 9 610 3 946 4 181 2 22 312 312 999 18 704 1 248 18 704 7 769 8 096 2 23 655 624 2 060 35 677 2 496 35 739 15 850 16 318 2 24 1 373 1 248 4 368 65 052 5 351 65 442 31 231 32 323 2 25 2 808 2 480 9 579 108 654 11 607 109 387 61 526 63 070 Tablica 2.5: Usporedba vremena izvršenja. U neparnim stupcima su vremena izvršenja za uniformno distribuirane cijele brojeve iz segmenta [0, 32767], a u parnim, vremena izvršenja za slučajno generirane brojeve iz Cauchijeve distribucije. U tablici 2.5 su navedena vremena izvršenja, u milisekundama, za pojedine algoritme, mjerena na slučajno generiranim brojevima iz uniformne, odnosno Cauchyjeve distribucije pri čemu je F (x) = arctg x + 1 funkcija distribucije Cauchyjeve slučajne π 2 varijable. Kao što možemo vidjeti na slici 2.6 bucket sort i flash sort su znatno sporiji na podacima iz Cauchijeve distribucije nego na podacima iz uniformne distribucije, dok 23

Slika 2.5: Funkcija gustoće Cauchyjeve slučajne varijable s crveno označenim podintervalima. (a) vjerojatnost da se slučajno odabrani element iz Cauchijeve distribucije nalazi u bilo kojem od 10 podintervala je jedna 1/10, (b) ekvidistantna podjela podintervala. Cauchijeva distribucija generira najviše brojeva bliskih nuli te su podintervali blizu nule najkraći. vremena izvršenja radix sort i STL funkcije sort() ne ovise o distribuciji iz koje dolaze dani podaci. Bucket sort i flash sort su tako spori jer u neke klase smještaju malo, a u neke veoma mnogo elemenata te tako ne uspijevaju iskoristiti brzinu insertion sorta koja se očituje samo na nizovima s malim brojem elemenata. Tomu je tako jer, pretpostavljajući uniformnu distribuciju podataka, bucket sort i flash sort cijeli interval, iz kojeg dolaze podaci, dijele na podintervale jednake veličine (kao na slici 2.5 (b)), a kako Cauchyjeva slučajna varijabla generira najviše brojeva bliskih nuli i vrlo malo svih ostalih brojeva, to znači da će u podintervale blizu nule biti smješteno daleko najviše elemenata ulaznog niza. Primijetimo da radix sort ovdje radi na brojevima koji dolaze iz Cauchyjeve distribucije i koji su kao takvi racionalni, iako smo u poglavlju 1.3. zahtijevali da podaci nad kojima radi radix sort budu cijeli brojevi. U testiranoj implementaciji radix sorta to je postignuto korištenjem operatora reinterpret_cast, koji se koristi za prevođenje racionalnog tipa podataka u odgovarajući cjelobrojni tip. Naime, kada unutar radix sorta koristimo counting sort za sortiranje po i-toj znamenci (u testiranoj implementaciji i-tu znamenku čini i-ti bayte), problem se pojavljuje u petoj liniji našeg 24

Slika 2.6: Grafički prikaz vremena izvršenja iz tablice 2.5. pseudokoda counting sorta. 4 for j = 1 to length(a) 5 C[A[j]] = C[A[j]] + 1 Kao što vidimo kao indeks niza C se koristi element ulaznog niza A[j] koji je racionalan broj, a indeks niza može biti samo nenegativan cijeli broj. Zato se u testiranoj implementaciji u liniji 5 umjesto A[j] koristi reinterpret_cast<int8 >(&A[j]), (2.1) gdje je int8 8-bitni cijeli broj. int8 se koristi jer se sortiranje odvija po i-toj znamenci koja se također sastoji od 8 bitova. Rezultat izraza 2.1 je 8-bitni cijeli broj dobiven jednostavnim kopiranjem binarnog zapisa od A[j] koji se zatim interpretira kao cjelobrojni tip podataka int8. Valja napomenuti da duljina bitnog zapisa znamenke po kojoj se sortira mora biti jednaka duljini bitnog zapisa cjelobrojnog tipa podataka u koji se ta znamenka prevodi. U protivnom, kada bismo primjerice 16-bitnu znamenku (racionalnog broja) pretvarali u 8-bitni cijeli broj, moglo bi se dogoditi da se dvije različite znamenke preslikaju u isti cijeli broj. Kao što smo vidjeli u poglavljima 1.4. i 1.5., da bi bucket sort i flash sort imali linearno očekivano vrijeme izvršenja, nije neophodno da podaci dolaze iz uniformne distribucije. Dovoljno je da se u svakom od podinetrvala nalazi približno jednak broj elemenata niza. Za uniformnu distribuciju U(0, 1), klasu koju ćemo staviti element x se određuje formulom k = nx, gdje je k redni broj klase, a n broj elemenata niza koji se sortira. Za uniformnu distribuciju s parametrima a i b, U(a, b), odgovarajuća 25

formula glasi n(x a) k =. b a Ta formula je korištena u našim implementacijama bucket sorta i flash sorta. Primijetimo da su x te (x a)/(b a) zapravo karakteristični dijelovi funkcija distribucije slučajnih varijabli U(0, 1) i U(a, b), jer su odgovarajuće funkcije distribucije 0, za x 0 F 0,1 (x) = x, za x (0, 1] 1, inače za standardnu uniformnu slučajnu varijablu te 0, za x a x a F a,b (x) =, za x (a, b] b a 1, inače za uniformnu slučajnu varijablu s parametrima a i b. Lako se provjeri da se u općem slučaju redni broj klase u koju treba staviti broj x može odrediti formulom k = nf (x), (2.2) gdje je F (x) funkcija distribucije odgovarajuće slučajne varijable. bucket sort flash sort n unif Cauchy Cauchy[mod] unif Cauchy Cauchy[mod] 2 15 3 105 16 4 154 16 2 16 5 310 31 6 313 32 2 17 8 622 47 10 623 48 2 18 21 1 236 94 24 1 239 109 2 19 73 2 459 249 101 2 459 265 2 20 201 4 895 515 275 4 895 655 2 21 453 9 563 1 061 624 9 610 1 342 2 22 999 18 704 2 138 1 248 18 704 2 606 2 23 2 060 35 677 4 368 2 496 35 739 5 772 2 24 4 368 65 052 8 798 5 351 65 442 11 903 2 25 9 579 108 654 18 440 11 607 109 387 23 805 Tablica 2.6: Usporedba vremena izvršenja. U prva tri stupca se nalaze vremena izvršenja za bucket sort, za podatke iz uniformne distribucije, Cauchyjeve distribucije i Cauchyjeve distribucije s modificiranom formulom za određivanje klase u koju treba staviti određeni element te analogno za flash sort u sljedeća tri stupca. U tablici 2.6 vidimo vremena izvršenja za bucket sort i flash sort mjerena n podacima iz, redom, uniformne distribucije, Cauchyjeve distribucije (podintervali kao na 26

slici 2.5 (b)) te iz Cauchyjeve distribucije s prilagođenom formulom za određivanje klase (podintervali kao na slici 2.5 (a)). Preciznije, u ovom slučaju formula glasi k = ( arctg x n + 1 ), π 2 jer je funkcija distribucije Cauchyjeve slučajne varijable F (x) = arctg x π + 1 2. Slika 2.7: Grafički prikaz vremena izvršenja iz tablice 2.6. Na slici 2.7 možemo vidjeti da su algoritmi s modificiranom formulom za izračun klase znatno brži od onog s uobičajenom formulom. Ipak, tako prilagođeni algoritmi nisu jednako brzi kao oni sa standardnom formulom na podacima iz uniformne distribucije. Tomu je tako, jer je izračun same formule za određivanje klase za Cauchyjevu distribuciju, koja se mora računati barem jednom za svaki od n elemenata niza, znatno složeniji nego što je to u slučaju uniformne distribucije. U ovom poglavlju smo vidjeli kako naši algoritmi za sortiranje u linearnom vremenu funkcioniraju u praksi te njihove performanse usporedili sa STL funkcijom sort() koja radi u Θ(n lg n) vremenu. Uspoređujući međusobno vremena izvršenja C++ implementacija tih algoritama na različitim podacima, uočili smo njihove najveće prednosti i mane. Counting sort je brz u slučaju kad je raspon podataka k malen. Brzina radix sorta ovisi o duljini bitnog zapisa koji se koristi za podatke. Ako pogrešno procijenimo distribuciju iz koje dolaze podaci, bucket sort i flash sort postaju veoma spori. Flash sort je na jednakim podacima uvijek sporiji od bucket sorta, ali koristi manje memorije. 27

Zaključak Još od početaka razvoja računalne znanosti pa sve do danas problem pronalaženja što efikasnijeg algoritma za sortiranje predstavlja jedno od najistraživanijih područja u informatici. Razlozi za to su mnogostruki, od onih čisto praktičnih kao što su ušteda vremena, novca i energije pa sve do dobro nam poznate akademske znatiželje. U ovom radu smo proučili četiri algoritma koji, pod uvjetom da zadovoljavaju određene pretpostavke, imaju linearno vrijeme izvršenja. Prva dva među njima, counting sort i radix sort, se bave problemom sortiranja cijelih brojeva. No, kao što smo vidjeli u prethodnom poglavlju, takvi algoritmi se lako mogu prilagoditi za rješavanje općeg problema sortiranja, što je i očekivano uzmemo li u obzir način na koji se podaci zapisuju u današnjim računalima. Ni za jedan od algoritama promatranih u ovom radu ne možemo reći da je općenito najbolji, ili pak najbrži, ali možemo reći da su svi uglavnom brži od STL funkcije sort(), osim u nekim posebnim situacijama. Još jedno zanimljivo svojstvo koje smo mogli primijetiti jest proporcionalni odnos između brzine algoritma i potrošnje memorije. O tome koji algoritam za sortiranje je najbolje koristiti u datoj situaciji, ovisi o strukturi ulaznih podataka koje treba sortirati te hardwareu kojim raspolažemo. 28