61
1. Osnove višenitnosti Izvršavanje više zadataka (multitasking) je način na koji moderni računarski sistemi poput Linux-a, Unix-a, i Windows-a 2000 obrađuju izvršavanje više programa. Dok se čini da se mnogo elemenata simultano izvršava, CPU zapravo radi smenjivanje konteksta - dajući svakoj aplikaciji malo CPU vremena pre nego što se rad na toj aplikaciji zameni radom na drugoj. Višenitnost se dešava kada je potrebno da se više zadataka izvede u bilo kom od tih programa. Na primer, u programu za obradu teksta poput Microsoft Word-a, vi ćete možda štampati najnoviju kopiju dokumenta dok ukucavate rezime, dok sistem proverava ispravnost spelovanja (pisanja) onoga što ukucavate. Svaki od ovih zadataka - štampanje, kucanje i provera, je obrađen od druge niti u programu. Svaku nit shvatite kao jedan tok kontrola u programu. Ovo se ponekada naziva kontekst izvršavanja jer svaka nit mora imati svoje sopstvene resurse - uključujući brojač programa i slog izvršavanja - kao i kontekst izvršavanja. I pored toga, sve niti u programu i dalje dele mnoge resurse, kao što je prostor u memoriji i otvoreni fajlovi; zato, nit se takođe može nazivati i proces lake kategorije. To je jedinstven tok kontrole baš kao i proces (ili program koji se izvršava), ali je lakši za kreiranje i uništavanje od procesa u okruženju sa višestrukim zadacima jer je uključeno manje kontrole resursa. 2. Kreiranje i pokretanje niti Postoji dva načina da kreirate posao koji će biti urađen kao odvojeni zadatak: Kreirajte podklasu klase java.lang.Thread i redefinišite njenu metodu run(). Implementirajte interfejs java.lang.Runnable i prosledite ga niti koju treba pokrenuti. Vaš izbor između ova dva mehanizma zavisi od toga šta je potrebno da uradite. Kako programski jezik Java podržava samo jednostruko nasleđivanje, proverite da li želite da iskoristite to nasleđivanje da biste napravili podklasu klase Thread. Ukoliko je vaša namera da nit koristite samo kao nit, bolje je da implementirate interfejs i kreirate staru dobru nit. U većini slučajeva, ipak, sve što ćete raditi je implementacija interfejsa Runnable. Upotreba podklase klase Thread Kada kreirate posao tako što definišete podklasu klase Thread, podklasa bi trebalo da redefiniše praznu metodu run() klase Thread. Ova metoda run() je mesto gde vi definišete zadatak koji treba izvesti: public static MojaNit extends Thread { public void run() { // rad } } Da biste izazvali da nit počne da se izvršava, pozivate metodu start(), koja izaziva poziv metode run(). Metodu run() ne pozivate direktno; ukoliko to uradite, poziv će se izvršiti u glavnoj niti umesto u novoj niti. Da bi demonstrirali pokretanje niti, evo primera kreiranja pod-klase klase Thread i neophodnog poziva metode start(): // U drugoj klasi Thread t = new MojaNit(); t.start(); Implementacija interfejsa Runnable Klasa Thread zapravo implementira interfejs Runnable. To je taj interfejs Runnable koji definiše metodu run(). Alternativa pravljenja podklase klase Thread i obezbeđivanja vaše sopstvene metode run() je implementacija interfejsa Runnable i obezbedivanje metode run() tu. I dalje je potrebno da kreirate Thread, ali će konstruktor Thread prihvatiti argument Runnable koji mu govori šta da radi. Evo jednog primera: class MojRunnable implements Runnable { public void run() { // rad } } U narednom primeru, izvršavanje uključuje kreiranje Runnable i Thread pre pozivanja start () :

algoritmi2.pdf

Embed Size (px)

Citation preview

1. Osnove višenitnosti

Izvršavanje više zadataka (multitasking) je način na koji moderni računarski sistemi poput Linux-a, Unix-a, i Windows-a 2000 obrađuju izvršavanje više programa. Dok se čini da se mnogo elemenata simultano izvršava, CPU zapravo radi smenjivanje konteksta - dajući svakoj aplikaciji malo CPU vremena pre nego što se rad na toj aplikaciji zameni radom na drugoj. Višenitnost se dešava kada je potrebno da se više zadataka izvede u bilo kom od tih programa. Na primer, u programu za obradu teksta poput Microsoft Word-a, vi ćete možda štampati najnoviju kopiju dokumenta dok ukucavate rezime, dok sistem proverava ispravnost spelovanja (pisanja) onoga što ukucavate. Svaki od ovih zadataka - štampanje, kucanje i provera, je obrađen od druge niti u programu.

Svaku nit shvatite kao jedan tok kontrola u programu. Ovo se ponekada naziva kontekst izvršavanja jer svaka nit mora imati svoje sopstvene resurse - uključujući brojač programa i slog izvršavanja - kao i kontekst izvršavanja. I pored toga, sve niti u programu i dalje dele mnoge resurse, kao što je prostor u memoriji i otvoreni fajlovi; zato, nit se takođe može nazivati i proces lake kategorije. To je jedinstven tok kontrole baš kao i proces (ili program koji se izvršava), ali je lakši za kreiranje i uništavanje od procesa u okruženju sa višestrukim zadacima jer je uključeno manje kontrole resursa.

2. Kreiranje i pokretanje niti

Postoji dva načina da kreirate posao koji će biti urađen kao odvojeni zadatak:

Kreirajte podklasu klase java.lang.Thread i redefinišite njenu metodu run().

Implementirajte interfejs java.lang.Runnable i prosledite ga niti koju treba pokrenuti.

Vaš izbor između ova dva mehanizma zavisi od toga šta je potrebno da uradite. Kako programski jezik Java podržava samo jednostruko nasleđivanje, proverite da li želite da iskoristite to nasleđivanje da biste napravili podklasu klase Thread. Ukoliko je vaša namera da nit koristite samo kao nit, bolje je da implementirate interfejs i kreirate staru dobru nit. U većini slučajeva, ipak, sve što ćete raditi je implementacija interfejsa Runnable. Upotreba podklase klase Thread Kada kreirate posao tako što definišete podklasu klase Thread, podklasa bi trebalo da redefiniše praznu metodu run() klase Thread. Ova metoda run() je mesto gde vi definišete zadatak koji treba izvesti:

public static MojaNit extends Thread {

public void run() {

// rad

}

}

Da biste izazvali da nit počne da se izvršava, pozivate metodu start(), koja izaziva poziv metode run(). Metodu run() ne pozivate direktno; ukoliko to uradite, poziv će se izvršiti u glavnoj niti umesto u novoj niti. Da bi demonstrirali pokretanje niti, evo primera kreiranja pod-klase klase Thread i neophodnog poziva metode start():

// U drugoj klasi

Thread t = new MojaNit();

t.start();

Implementacija interfejsa Runnable Klasa Thread zapravo implementira interfejs Runnable. To je taj interfejs Runnable koji definiše metodu run(). Alternativa pravljenja podklase klase Thread i obezbeđivanja vaše sopstvene metode run() je implementacija interfejsa Runnable i obezbedivanje metode run() tu. I dalje je potrebno da kreirate Thread, ali će konstruktor Thread prihvatiti argument Runnable koji mu govori šta da radi. Evo jednog primera:

class MojRunnable implements Runnable {

public void run() {

// rad

}

}

U narednom primeru, izvršavanje uključuje kreiranje Runnable i Thread pre pozivanja start () :

// U drugoj klasi

Runnable r = new MojRunnable();

Thread t = new MojaNit(r);

t.start();

Pozivanje metode start klase Thread ovde izaziva pokretanje metode run() objekta Runnable koji je prosleđen konstruktoru.

Umesto da se kreira klasa koja implementira Runnable, još jedan čest način za kreiranje implementacije Runnable je upotreba anonimne unutrašnje klase, kao što je prikazano. Ovim se izbegava potreba da se dodeljuje besmisleno ime klase.

Runnable r = new Runnable() {

public void run() {

// rad

}

}

Thread t = new MojaNit(r);

t.start();

3. Metode za kontrolu niti

Klasa Threads stavlja na raspolaganje mnoge korisne metode. Kao brzi pregled, evo nekih metoda koje se najčešće koriste.

START() Ova metoda pokreće izvršavanje niti i izvršava telo definisano u njenoj metodi run(), ili u metodi run() objekta Runnable prosleđenog konstruktoru. Kontrola programa se trenutno vraća, dok nova nit paralelno izvršava run() .

INTERRUPT() Metoda interrupt() postavlja status prekida niti. Ona takođe izaziva da nit stane dok je blokirana ukoliko čeka na pristup monitoru, čeka na I/O u java.nio.channels.Channel, ili je blokirana u klasi java.nio.chanels.Selector. Ukoliko nit čeka na I/O operaciju koja nije orijentisana na kanale, interrupt () ne deblokira operaciju.

JOIN() Metoda join() izaziva da nit sačeka da se druga nit završi. Ovu metodu ćete pozivati za drugu nit, ne za nit u kojoj se kod izvršava.

Neke operacije sa nitima, koje su navedene sledeće, rade samo na niti koja se trenutno izvršava. Drugim rečima, metode su statične.

CURRENTTHREAD() Ova metoda vraća referencu ka niti koja se trenutno izvršava.

DUMPSTACK() Ovo je koristan metod debagovanja za ispisivanje sloga aktuelne niti na zahtev. Slog je trag poziva metoda, dovodeći nit do njene trenutne tačke izvršavanja.

SLEEP(LONG MILISEKUNDI) I SLEEP (LONG MILISEKUNDE, LONG NANOSEKUNDE) Ove dve metode izazivaju da radno okruženje uspava nit koja se trenutno izvršava na određeno vreme. Ovaj poziv mora biti postavljen u okviru bloka try-catch jer može biti emitovan izuzetak InterruptedException ukoliko nit bude prekinuta sa interrupt(). Nit se ne pokreće automatski nakon zadatog perioda spavanja, ali će biti izvršiva. Upotreba verzije sa nanosekundama je možda preterana, ali je dostupna.

YIELD() Ova metoda izaziva da se nit izvršavanja privremeno zaustavi i da da šansu kontroloru rasporeda niti da pokrene drugu nit. Ukoliko nema ničeg drugog da se izvrši, nit će nastaviti izvršavanje bez promene konteksta.

Naredni primer, prikazan u fragmentu koda 5.1, prikazuje neke od ovih metoda niti na delu. U ovom primeru, glavna nit kreira dve niti, potom čeka da se prva nit završi pozivajući metodu join() prve niti. Prva nit poziva metodu sleep() da bi se uspavala na 10 sekundi. U međuvremenu, druga nit poziva svoju sopstvenu metodu wait() da bi sama sebe suspendovala dok glavna nit ne pozove metodu notify() suspendovane niti. Nakon što prva nit dode do kraja, glavna nit nastavlja svoje izvršavanje, budi drugu nit pozivajući njenu metodu notify(), i čeka dok i druga nit ne dođe do kraja pozivajući metodu join() druge niti.

public class ProbaMetode {

static class PrvaNit extends Thread{

public void run(){

try {

System.out.println (" Prva nit pocinje da se izvrsava.");

sleep(10000);

System.out.println(" Prva nit je zavrsila izvrsavanje.");

} catch InterruptedException e) {

System.err.println("Greska u niti: " + e);

}

}

}

static class DrugaRunnable implements Runnable {

public synchronized void run() {

try {

System.out.println(" Druga nit pocinje da se izvrsava.");

System.out.println (" Druga nit sama sebe suspenduje.");

wait();

System.out.println(" Druga nit se ponovo pokrece i zavrsava");

} catch (InterruptedException e) {

System.out.println("Greska u izvrsivoj: " + e);

}

}

}

public static void main (String[] args) {

Thread prva = new PrvaNit();

Runnable drugaRunnable = new DrugaRunnable();

Thread druga = new Thread(drugaRunnable);

prva.start();

druga.start();

try {

System.out.println("Cekamo da se zavrsi prva nit...");

prva.join();

System.out.println "Bas dugo cekamo!");

System.out.println "Budjenje druge niti...");

synchronized(drugaRunnable){

drugaRunnable.notify();

}

System.out.println("Cekamo da se zavrsi druga nit...");

druga.join();

} catch (InterruptedException e) {

System.out.println("Greska u glavnoj: " + e);

}

System.out.println("Ja sam takodje spreman za kraj.");

}

}

Fragment koda 5.1 Metode niti

Izlaz iz ovog programa je prikazan ovde:

>java ProbaMetode Cekamo da se zavrsi prva nit... Prva nit pocinje da se izvrsava. Druga nit pocinje da se izvrsava. Druga nit sama sebe suspenduje. Prva nit je zavrsila izvrsavanje. Bas dugo cekamo! Budjenje druge niti... Cekamo da se zavrsi druga nit... Druga nit se ponovo pokrece i zavrsava se. Ja sam takodje spreman za kraj.

4. Životni ciklus niti

Klasu Thread koristite da biste učinili svoje programe višenitnim. Interno, radno okruženje se takođe oslanja na niti da bi izvelo važne zadatke poput učitavanja klasa, obradu događaja i interakciju sa operativnim sistemom. U svakom trenutku u vremenu, sve niti iz vašeg programa će biti u jednom od četiri stanja: new (nove), runnable (izvršive), blocked (blokirane) ili dead (mrtve). Ova stanja su ilustrovana na slici 5.1.

Slika 5.1 Životni ciklus niti

Nove niti Kada kreirate nove niti pomoću ključne reči new, time uvodite stanje nova. Članovi podataka klase su inicijalizovani, ali objekat samo sedi u ovom trenutku, jedući memoriju, ne radeći ništa više. Kada je pozvana metoda start() niti, kreiraju se odgovarajući resursi sistema da bi izvršili zadatak, i nit ulazi u stanje izvršavanja. Izvršive niti Kada su pokrenute, izvršive niti mogu biti izabrane od Java radnog okruženja da bi se zaista izvršavale. U tom trenutku, nit nije samo izvršiva, već se zaista i izvršava. Zamislite kao da izvršivo stanje ima dva podtipa nazvana pripravan (ili čeka na red) i radi. Kada nit uđe u izvršno stanje, ona zapravo dobija CPU cikluse. Kada je pripravna i čeka na red, ona jednostavno čeka.

Kontrolor rasporeda niti kontroliše prelaz od spreman na radi. Medutim, nit može pozvati metođu yield() da bi se dobrovoljno premestila iz radnog stanja u stanje pripravnosti. Metoda yield() je statična metoda i deluje na aktuelnu nit. Ne možete potčiniti (yield()) drugu nit da biste je privole li da stane. Blokirane niti Nit može ući u blokirano slanje kada se desi jedan od nekoliko događaja:

Nit pozove metodu wait() objekta

Nit pozove metodu sleep()

Nit je blokirana čekajuci da se završi I/O operacija

Nit čeka drugu nit pomoću metode join()

Nit u blokiranom stanju neće biti u rasporedu za izvršavanje. Ona će biti vraćena nazad u izvršivo stanje, konkurišući za CPU cikluse, kada se desi suprotan dogadaj u odnosu na događaju blokiranja, kao što sledi:

Ukoliko je nit blokirana pozivom metode wait() objekta, suprotan događaj je kada bude pozvana metoda objekta notify() ili metoda notifyAll().

Ukoliko je nit uspavana, suprotan događaj je kada istekne zadato vreme spavanja.

Ukoliko je nit blokirana u I/O, suprotan događaj je kada se završi određena I/O operacija.

Ukoliko nit čeka da bude pridružena (join), suprotan događaj je kada se završi druga nit.

Uz izuzetak niti blokiranih zbog I/O, blokirane niti takođe mogu biti prekinute pozivanjem interrupt().

Mrtve niti Kada nit dosegne kraj svoje metode run(), ona prelazi u stanje mrtva. Mrtve niti ne mogu biti reanimirane. Da bi se izbeglo njihovo umiranje, možete uključiti petlju while u metodu run() niti koja će održati nit u radu. Naravno, ukoliko je potrebno da nit uradi zadatak samo jednom, petlja while očigledno nije potrebna.

Na primer, da biste izazvali da nit stane pri sledećem prolazu kroz petlju, uobičajeno je zadržati referencu niti koja se izvršava i proveriti da li je ona null, gde neki spoljni faktor može promeniti promenljivu signalizirajući niti da stane. To izgleda ovako:

Thread trenutnaNit = Thread.currentThread();

while (trenutnaNit != null) {

// ... obavi posao

}

Kada nit umre, dolazi do sahrane i emituje se greška java.lang.ThreadDeath. Klasa ThreadDeath je podklasa klase java.lang.Error i može biti uhvaćena u try-catch bloku. Ukoliko hvatate generičku grešku ili konkretno ThreadDeath u odredbi catch bloka, morate osigurati da greška ThreadDeath ponovo bude emitovana. Ukoliko greška ne stigne do radnog okruženja, onda sistem neće osloboditi resurse niti. Evo demonstracije ponovnog emitovanja greške ThreadDeath:

try {

// nit je umrla

} catch (ThreadDeath smrt) {

// verovatno nesto radi

throw death;

5. Grupe niti

Sve niti koje se izvršavaju u sistemu su deo tačno jedne ThreadGroup (grupe niti). Svaka grupa niti može sadržati niti ili druge grupe niti. Niti su grupisane u strukturu koja liči na stablo, gde je koren stabla sistemska grupa niti.

Grupe niti koristite za organizaciju, što vam omogućava da obrađujete i kontrolišete grupe niti kao celinu. Na primer, možete olakšati bezbednost tako što ćete urediti da niti iz jedne grupe ne mogu promeniti ponašanje niti iz druge. Osim toga, grupe se mogu koristiti da bi se ograničili prioriteti niti.

Kada pokrenete Java aplikaciju, izvršni sistem kreira glavnu grupu niti ThreadGroup u okviru sistemske ThreadGroup. Glavna nit je kreirana u ovoj grupi da bi pokrenula vašu metodu main(). Sve korisnički kreirane niti i grupe niti postaju članovi ove ThreadGroup. Da biste kreirali niti u sopstvenoj grupi niti, prosleđujete grupu kao prvi argument da bude konstruktor niti:

ThreadGroup grupa = new ThreadGroup (imeGrupe); Thread t1 = new Thread (grupa, izvrsiva); Thread t2 = new Thread (grupa, izvrsiva, imeNiti); Thread t3 = new Thread (grupa, izvrsiva, imeNiti, velicinaSloga); Thread t4 = new Thread (grupa, imeNiti);

6. Prioriteti i raspored

Kontrola rasporeda niti definiše kako Java radno okruženje menja zadatke i odabira zadatak koji će sledeći biti izvršen. Da bi pomoglo u raspoređivanju, radno okruženje se oslanja na prioritete postavljene za svaki zadatak. Što je više podešenje prioriteta, veća je važnost zadatka, i verovatnije je da će se on izvršiti ranije (ukoliko nema drugih zadataka visokog prioriteta koji čekaju na red). Prisvajanje rasporeda i deljenje vremena Kada novi zadatak stigne i želi da se izvrši, radno okruženje će proveriti prioritet zadatka. Ukoliko novi zadatak ima prioritet viši od zadatka koji se trenutno izvršava, zadatak sa većim prioritetom će se izvršiti, potiskujući zadatak koji se do tada izvršavao nazad kontroloru rasporeda. Ovo ponašanje se naziva prisvajanjem rasporeda.

Osim što mogu biti prekinuti zbog prisvajanja rasporeda, niti se takođe mogu dobrovoljno osloboditi CPU pozivanjem metode yield() niti. Kada se ovo desi, nit koja se izvršava kaže da bi ona volela da nastavi sa korišćenjem CPU, ali da je sada dobro vreme da se proveri i vidi da li još neka nit želi da uradi neki posao. Ukoliko postoji neka izvršiva nit koja čeka sa istim prioritetom kao i aktuelna nit, jedna od tih niti će preuzeti CPU. Ukoliko vaša (trenutna) nit ne ustupa CPU drugim nitima sa istim prioritetom, one će ga možda prisvojiti - do ovoga dolazi zato što izvršavanje niti može biti vremenski deljeno. Deljenje vremena znači da će se niti sa jednakim prioritetima na putu CPU naizmenično izvršavati, deleći vreme da bi svakoj niti omogućile pristup.

Tačan algoritam rasporeda niti je specifičan za pojedine platforme i nije formalno definisan. U suštini, nemojte se osloniti na prioritete ili prisvajanje u redosledu izvršavanja niti. Osim toga, kontrolori rasporeda mogu invertovati prioritete da bi obezbedili da ne bude niti koje su zapostavljene u drugim izvršavanjima.

Vrednosti prioriteta niti Kao što je pomenuto, vi se oslanjate na prioritete niti da biste vršili kontrolu nad time koji zadatak će pristupiti CPU. Klasa Thread definiše set od 10 vrednosti za te prioritete, počev od MIN_PRIORITY i zaključno sa MAX_PRIORITY. Takođe postoji "normalni" prioritet, N0RM_PRIORITY, koji je podrazumevani prioritet za novokreirane niti. Radni sistem takođe može koristiti druge prioritete za zadatke sistema. Menjanje aktuelne vrednosti prioriteta se radi pomoću metode setPriority():

nit.setPriority (Thread.NORM_PRIORITY + 1);

7. Dobijanje informacija o nitima i o grupama niti

Niti i grupe niti se u suštini koriste u svrhu obrade, kao što je pokretanje i zaustavljanje niti ili menjanje karakteristika. Osim toga, dve klase, Thread i ThreadGroup mogu obezbediti mnoge detalje o pojedinim nitima. Slede šest stavki informacija koje možete zahtevati od instance klase Thread:

getName() Vraća ime niti.

getPriority() Vraća prioritet niti.

getThreadGroup() Vraća roditeljsku grupu niti.

isAlive() Vraća true ukoliko je nit pokrenuta, ali još uvek nije mrtva.

isDaemon() Vraća true ukoliko je nit demonska.

islnterrupted() Vraća true ukoliko je nit bila prekinuta.

Klasa ThreadGroup ima sopstveni set od šest metoda informacija. Neke se primenjuju na grupu kao celinu, a neke na određenu nit.

getName() Vraća ime grupe niti.

getParent() Vraća roditeljsku grupu niti.

getMaxPriority() Vraća maksimalni prioritet niti iz grupe.

activeCount() Vraća broj aktivnih niti.

activeGroupCount() Vraća broj aktivnih grupa niti.

enumerate(Thread list [ ]), Dodaje sve aktivne niti ili grupe niti u niz list. Ukoliko je enumerate(Thread list [ ]), recursive true (podrazumevana vrednost), svi odgovarajući boolean recursive), elementi u svim podgrupama niti će takođe biti iskopirane. enumerate(ThreadGroup list [ ]), Ova metoda vraća broj elemenata dodatih u niz. enumerate(ThreadGroup list [ ]), boolean recursive)

8. Izvršavanje periodičnih zadataka

Često se sa zadacima dešava da vi izvršite zadatak, a da ga zatim na neko vreme uspavate pre nego što ga ponovo pokrenete. Umesto programiranja ovog dela "spavaj na kratko", imate nekoliko biblioteka klasa koje umesto vas rade ovaj posao. To vas čini odgovornim samo za sam zadatak. Klasa java.util.Timer obezbeđuje raspored, a java.util.TimerTask obezbeđuje zadatak.

Kod klase Timer, umesto kreiranja implementacije Runnable ili kreiranja podklase klase Thread da biste definisali koji posao treba obaviti, vi pravite podklasu klase TimerTask i obezbeđujete implementaciju run(). Na primer, da biste definisali zadatak koji će ispisivati poruku, možete ponuditi sledeću definiciju anonimne klase. Obratite pažnju kako je ova definicija jednostavna.

TimerTask zadatak = new TimerTask() {

public void run() {

System.out.println("Poruka")

}

};

Da biste izvršili TimerTask, prvo je potrebno da kreirate instancu klase Timer:

Timer tajmer = new Timer();

Potom planirate zadatak koji se ponavlja ili u metodi schedule() ili u metodi scheduleAtFixed Rate(). Postoji šest varijacija dve metode za zadavanje rasporeda:

Ove dve varijacije izvršavaju zadatak nakon zadate zadrške ili u zadatom trenutku:

schedule (TimeTask zadatak, long zadrska)

schedule (TimeTask zadatak, Date vreme)

Ove dve varijacije izvršavaju zadatak sa ponavljanjem, počevši nakon pauze od zadate zadrške u milisekundama ili u zadatom trenutku, i potom ponavljaju zadatak nakon što istekne makar zadati period vremena u milisekundama:

schedule(TimeTask zadatak, long zadrska, long period)

schedule(TimeTask zadatak, Date vreme, long period)

Ove dve varijacije izvršavaju zadatak sa ponavljanjem, počevši nakon što sačekaju zadati period zadrške u milisekundama ili u zadatom trenutku, i potom ponavljaju zadatak. Ukoliko je sistem zauzet kada je vreme da se jedan od ova dva zadataka izvrši, sukcesivna izvršavanja se nagomilavaju.

scheduleAtFixedRate(TimeTask zadatak, long zadrska, long period)

scheduleAtFixedRate(TimeTask, Date vreme, long period)

Razlika izmedu metoda schedule() i scheduleAtFixedRate() je vreme između uzastopnih izvršavanja planiranog zadatka. Kod metode schedule(), period je minimalan interval. Kod scheduleAtFixedRate(), sa druge strane, vremenski planer će nagomilavati sukcesivna izvršavanja ukoliko je sistem zauzet i nije u mogućnosti da izvrši jedno ili više izvršenja zadatka.

Vremenski period zadat za obe metode zadavanja rasporeda nije zadrska od kraja izvršenja zadatka, već vremenski period od početka prethodnog izvršavanja. Dole data tabela će vam pomoći da razumete ponašanje rasporeda i razlike izmedu schedule() i scheduleAtFixedRate(). U suštini, scheduleAtFixedRate() će se igrati sustizanja da bi osigurao da je izvršen odgovarajući broj zadataka za dodeljeno vreme, dok schedule() jedino brine o započinjanju sledećeg izvršavanja nakon što sačeka makar zadati period vremena.

nitschedule(zadatak, 500) scheduleAtFixedRate(zadatak, 500)

1: 500 ms 1: 500 ms

2: 1000 ms 2: 1000 ms

Sistem zauzet do 1700 ms Sistem zauzet do 1700 ms

3: 1700 ms 3: 1700 ms

4: 2200 ms 4: 2000 ms

Sistem zauzet do 3100 ms Sistem zauzet do 3100 ms

5: 3100ms 5: 3100 ms

6: 3600 ms 6: 3110 ms

7: 4100 ms 7: 3500 ms

* Podrazumeva da se zadatak izvršava 10 ms.

Klasa ProbaUtilTimer daje kompletniji primer upotrebe java.util.Timer od svega što je prikazano ranije. Osim što jednostavno prikazuje poruku, klasa spava jednu i jednu četvrtinu sekunde. Takođe ćete primetiti da je trenutno vreme prikazano na nekoliko mesta tako da možete videti efekat rasporeda. Siobodno promenite poziv metode schedule() u scheduleAtFixedRate(), ili uvećajte vreme sleep() da biste videli kako se menja ponašanje rasporeda:

import java.util.*;

public class ProbaUtilTimer {

public static void main (String[] args) {

TimerTask zadatak = new TimerTask() {

public void run() {

System.out.println("Poruka");

System.out.println("Pre spavanja:" + new Date());

try {

Thread.sleep(1250);

} catch (InterruptedException ignorisan) {

}

System.out. println("Nakon spavanja:" + new Date());

}

};

Timer tajmer = new Timer();

tajmer.schedule(zadatak, 500, 5000);

System.out. println("Glavna: " + new Date());

}

}

Zaustavljanje ponavljanja periodičnih zadataka lako metode schedule() i scheduleAtFixedRate() prihvataju vrednost perioda sa kojim bi zadatak trebalo da se ponavlja, one se automatski izvršavaju zauvek. Ukoliko je potrebno da otkažete izvršavanje svih zadataka koji se ponavljaju, možete pozvati metodu cance() klase Timer. Da biste otkazali izvršavanje pojedinog zadatka, pozovite metodu cancel() određenog objekta TimerTask.

Stekovi

Stek (stack) je kolekcija objekata koji su umetnuti i uklonjeni u skladu sa principom last-in first-out

(LIFO). Objekti mogu biti umetnuti u stek u bilo kom trenutku, ali jedino poslednji umetnuti objekat

može biti uklonjen u bilo kom trenutku. Stek je najjednostavnije zamisliti kao gomilu predmeta istog

tipa naslaganih jedan na drugi, a na gomilu možete dodavati predmete samo na vrh i možete ih

uzimati samo sa vrha, na primjer, gomila knjiga koje stoje jedna na drugoj, ili gomila novčića. Ime

“stek” je izvedeno iz metafore gomile pločica naslonjenih na oprugu, u kafeterija držaču pločica. U

ovom slučaju, osnovne operacije obuhvataju “guranje“ (push) i “skidanje“ (pop) ploča iz steka. Kada

nam je potrebna nova pločica iz držača, mi “skidamo“ pločicu na vrhu, i kada dodajemo pločicu, mi je

“guramo“ dole u stek da postane pločica na vrhu. Možda najbolji primer za stek je držač PEZ

bombona, koji smešta bombone u kontejner sa oprugom koji “istiskuje” napolje bombonu na vrhu

steka kada je vrh kontejnera podignut.

Stekovi

Stek (stack) je kolekcija objekata koji su umetnuti i uklonjeni u skladu sa principom last-in first-out (LIFO). Objekti mogu biti umetnuti u stek u bilo kom trenutku, ali jedino poslednji umetnuti objekat može biti uklonjen u bilo kom trenutku. Stek je najjednostavnije zamisliti kao gomilu predmeta istog tipa naslaganih jedan na drugi, a na gomilu možete dodavati predmete samo na vrh i možete ih uzimati samo sa vrha, na primjer, gomila knjiga koje stoje jedna na drugoj, ili gomila novčića. Ime “stek” je izvedeno iz metafore gomile pločica naslonjenih na oprugu, u kafeterija držaču pločica. U ovom slučaju, osnovne operacije obuhvataju “guranje“ (push) i “skidanje“ (pop) ploča iz steka. Kada nam je potrebna nova pločica iz držača, mi “skidamo“ pločicu na vrhu, i kada dodajemo pločicu, mi je “guramo“ dole u stek da postane pločica na vrhu. Možda najbolji primer za stek je držač PEZ bombona, koji smešta bombone u kontejner sa oprugom koji “istiskuje” napolje bombonu na vrhu steka kada je vrh kontejnera podignut. To je prikazano na slici 7.1

Slika 7.1 Šematski prikaz PEZ držača bombona; fizička implementacija stek ADT-a.

Stekovi su osnovne strukture podataka. Oni se koriste u mnogim aplikacijama uključujući sledeće.

Primer 1: Web pretraživači interneta smeštaju adrese nedavno posećenih sajtova u stek. Svaki put kada korisnik poseti novi sajt, adresa tog sajta je “gurnuta“ u stek adresa. Pretraživači tada dozvoljavaju korisniku da “uzme“ natrag prethodno posećeni sajt korišćenjem dugmeta “back“.

Primer 2: Editori teksta obično obezbeđuju “undo“ mehanizam koji poništava nedavne operacije editovanja i vraća dokument u prethodno stanje. Ova operacija vraćanja može biti postignuta čuvanjem promena teksta u steku.

9. Stek abstraktni tip podataka

Stekovi su najednostavniji od svih struktura podataka, ali su ipak među najvažnijim, pošto su korišćeni u velikom broju različitih aplikacija koje uključuju mnogo više sofistiranih struktura podataka. Formalno, stek je abstraktni tip podataka (ADT) koji podržava sledeća dva metoda:

push(e): Umetni element e, na vrh steka

pop(): Ukloni iz steka i vrati element na vrhu steka; pojavljuje se greška ako je stek prazan.

Dodatno, definisaćemo dva sledeća metoda:

size(): Vraća broj elemenata u steku

isEmpty(): Vraća Boolean ukazujući da je stek prazan

pop(): Vraća element na vrhu steka, bez njegovog uklanjanja; pojavljuje se greška ako je stek prazan

Primer 3: Sledeća tabela pokazuje seriju stek operacija i nihovih efekata na početno prazan stek intedžera.

Operacija Izlaz Sadržaji Steka

push(5) - (5)

push(3) - (5,3)

pop() 3 (5)

push(7) - (5,7)

pop() 7 (5)

top() 5 (5)

pop() 5 ()

pop() "error" ()

isEmpty() true ()

push(9) - (9)

push(7) - (9,7)

push(3) - (9,7,3)

push(5) - (9,7,3,5)

size() 4 (9,7,3,5)

pop() 5 (9,7,3)

push(8) - (9,7,3,8)

pop() 8 (9,7,3)

pop() 3 (9,7)

Stek interfejs u Javi Zbog svoje važnosti, stek struktura podataka je uključena kao “built-in“ klasa u Javin paket java.util. Klasa java.util.Stack je struktura podataka koja smešta generičke Java objekte i uključuje, među drugima, metode push(), pop(), peek() (ekvivalentno sa top()), size() i empty() (ekvivalentno sa isEmpty()). Metodi pop() i peek() izazivaju izuzetak EmptyStackException ako su pozvani kada je stek prazan. Mada je pogodnije koristiti built-in klasu java.util.Stack, poučno je naučiti kako da se projektuje i implementira stek “od nule“. Implementiranje abstraktnog tipa podataka u Javi obuhvata dva koraka. Prvi korak je definisanje Java Aplikativnog Programskog Interfejsa (API), ili jednostavno interfejsa, koji opisuje imena metoda koje ADT podržava i kako treba da budu deklarisani i korišćeni. U skladu sa tim, mi moramo definisati izuzetke za bilo koje uslove greške koji mogu nastati. Na primer, uslov greške koji se pojavljuje kada se poziva metod pop() ili top() za prazan stek, signaliziran je preko izuzetka tipa EmptyStackException, koji je definisan u fragmentu koda 7.1.

/**

* Runtime izuzetak izazvan kada neko pokušava da izvede operaciju

* top ili pop na praznom steku.

*/

public class EmptyStackException extends RuntimeException {

public EmptyStackException(String err) {

super(err);

}

}

Fragment koda 7.1 Izuzetak izazvan metodama pop() i top() stek interfejsa kada je pozvan prazan stek

Kompletan interfejs za ADT stek prikazan je u fragmentu koda 7.2. Primetimo da je ovaj interfejs veoma generalan pošto on specificira koji elementi date klase (i njenih podklasa) mogu biti umetnuti u stek. On postiže ovu opštost korišćenjem koncepta generičnosti, koji smo ranije opisali.

/**

* Interfejs za stek:kolekcija objekata koji su umetnuti i uklonjeni

* u skladu sa principom last-in first-out. Ovaj interfejs uključuje

* glavne metode java.util.Stack

*

* @author Roberto Tamassia

* @author Michael Goodrich

* @see EmptyStackException

*/

public interface Stack<E> {

/**

* Vraća broj elemenata u stek.

* @vraća broj elemenata u stek.

*/

public int size();

/**

* Vraća da li je stek prazan.

* @vraća true ako je stek prazan, u suprotnom vraća false.

*/

public boolean isEmpty();

/**

* Pregleda element na vrhu steka.

* @vraća zadnji element u stek.

* @izuzetak EmptyStackException ako je stek prazan.

*/

public E top()

throws EmptyStackException;

/**

* Umeće element na vrh steka.

* @param elementa da bude umetnut.

*/

public void push (E element);

/**

* Uklanja element na vrhu steka.

* @vraća uklonjeni element.

* @izuzetak EmptyStackException ako je stek prazan.

*/

public E pop()

throws EmptyStackException;

}

Fragment koda 7.2 Interfejs steka dokumentovan sa komentarima u Javadoc stilu. Primetimo korišćenje generičkog parametrizovanog tipa E, koji implicira da stek može sadržati elemente neke

specifične klase.

10. Jednostavna niz-bazirana implementacija steka

Stek možemo da implementiramo smeštanjem njegovih elemenata u niz. Specifično, stek u ovoj implementaciji sastoji se od N-elemenata niza S plus intedžer promenljiva t koja daje indeks elementa na vrhu u nizu S. Element na vrhu je smešten u ćeliji S[t]. To je prikazano na slici 7.2

Slika 7.2 Implementiranje steka sa nizom S

Pozivanjem kojim nizovi startuju sa indeksom 0 u Javi, mi inicijalizujemo t na -1, i koristimo ovu vrednost za t da identifikujemo prazan stek. Isto tako, možemo da koristimo t da odredimo broj elemenata (t+1). Mi takođe uvodimo novi izuzetak, nazvan FullStackException, da istaknemo grešku koja nastaje ako pokušamo da umetnemo novi element u puni niz. Međutim, izuzetak FullStackException je specifičan za ovu implementaciju i nije definisan u stek ADT. Mi dajemo detalje niz-bazirane implementacije steka u fragmentu koda 7.3.

Algorithm size(): return t + 1

Algorithm isEmpty(): return (t < 0)

Algorithm top(): If isEmpty() then throw a EmptyStackException return S[t]

Algorithm push(e): If size() = N then throw a FullStackException

t t + 1

S[t] e

Algorithm pop():

If isEmpty() then throw a EmptyStackException

e S[t]

S[t] null

t t - 1 return e

Fragment koda 7.3 Implementiranje steka korišćenjem niza date veličine N

Analiziranje niz-bazirane implementacije steka Ispravnost metoda u niz-baziranoj implementaciji steka sledi odmah iz definicije samih metoda. Ipak, postoji jedna interesantna tačka koja obuhvata implementaciju pop metoda. Primetimo da smo izbegli resetovanje starog S[t] na null i da ipak imamo ispravan metod. Međutim, postoji razmena (trade-off) i da bi mogli da izbegnemo ovo dodeljivanje, treba da razmišljamo o implementaciji ovih algoritama u Javi. Razmena obuhvata Java mehanizam sakupljanja otpada (garbage collection) koji pretražuje memoriju tragajući za objektima koji nisu više referencirani kao aktivni objekti i prepravlja ovaj prostor za dalju upotrebu. Neka je e = S[t] element na vrhu pre nego što je pozvan pop metod. Pravljenjem S[t] kao null reference, mi ukazujemo da stek više ne drži referencu na objekat e. U stvari, ako ne postoji druga aktivna referenca na e, tada će memorijski prostor uzet od strane e biti popravljen od strane sakupljača otpada. Sledeća tabela pokazuje vreme rada za metode u realizaciji steka kao niza. Svaki od stek metoda u realizaciji niza izvršava konstantni broj iskaza obuhvatajući aritmetičke operacije, upoređenja i dodeljivanja, U skladu sa tim, pop takođe poziva isEmpty, koji sam radi u konstantom vremenu. Zato, u ovoj implementaciji stek ADT, svaki metod radi u konstantnom vremenu, tj. svako izvršavanje je O(1) vreme. Korišćenost prostora je O(N) gde je N veličina niza, određena od trenutka

kada je stek instanciran. Primetimo da je korišćenost prostora nezavisna od broja n N elemenata koji su trenutno u steku.

Metod Vreme

size O(1)

isEmpty O(1)

top O(1)

push O(1)

pop O(1)

Konkretna Java implementacija pseudo koda prikazanog u fragmentu koda 7.3, sa Java klasom ArrayStack koja implementira stek interfejs, prikazana je u fragmentu koda 7.4. Zabeležimo da koristimo simboličko ime CAPACITY, da specificiramo kapacitet niza. Ovo nam dozvoljava da specificiramo kapacitet niza na jednom mestu u našem kodu.

/**

* Implementacija stek ADT korišćenjem fiksirane-dužine niza.

* Izuzetak je izazvan ako je pokušana push operacija kada je

* veličina steka jednaka dužini niza. Ova klasa uključuje glavne

* metode built-in klase java.util.Stack.

*/

public class ArrayStack<E> implements Stack<E> {

protected int capacity; // Aktuelni kapacitet stek niza

public static final int CAPACITY = 1000; // default kapacitet niza

protected E S[]; //Generički niz korišćen da implementira stek

protected int top = -1; // indeks za vrh steka

public ArrayStack() {

this(CAPACITY); // default kapacitet

}

public ArrayStack(int cap) {

capacity = cap;

S = (E[]) new Object[capacity];//kompajler može da upozori, ali je OK

}

public int size() {

return (top + 1);

}

public boolean isEmpty() {

return (top < 0);

}

public void push(E element) throws FullStackException {

if (size() == capacity)

throw new FullStackException("Stack is full.");

S[++top] = element;

}

public E top() throws EmptyStackException {

if (isEmpty())

throw new EmptyStackException("Stack is empty.");

return S[top];

}

public E pop() throws EmptyStackException {

E element;

if (isEmpty())

throw new EmptyStackException("Stack is empty.");

element = S[top];

S[top--] = null; // dereferencira S[top] za sakupljanje otpada.

return element;

}

public String toString() {

String s;

s = "[";

if (size() > 0) s+= S[0];

if (size() > 1)

for (int i = 1; i <= size()-1; i++) {

s += ", " + S[i];

}

return s + "]";

}

// Štampa statusne informacije o nedavnim operacijama i steku.

public void status(String op, Object element) {

System.out.print("------> " + op); // print this operation

System.out.println(", returns " + element); // what was returned

System.out.print("result: size = " + size() + ", isEmpty = " +

isEmpty());

System.out.println(", stack: " + this); // contents of the

stack

}

/**

* Testira program izvođenjem serije operacija stekova, štampa

izvedene

* operacije, vraćene elemente i sadržaj obuhvaćenog steka, nakon

svake

* operacije.

*/

public static void main(String[] args) {

Object o;

ArrayStack<Integer> A = new ArrayStack<Integer>();

A.status("new ArrayStack<Integer> A", null);

A.push(7);

A.status("A.push(7)", null);

o = A.pop();

A.status("A.pop()", o);

A.push(9);

A.status("A.push(9)", null);

o = A.pop();

A.status("A.pop()", o);

ArrayStack<String> B = new ArrayStack<String>();

B.status("new ArrayStack<String> B", null);

B.push("Bob");

B.status("B.push(\"Bob\")", null);

B.push("Alice");

B.status("B.push(\"Alice\")", null);

o = B.pop();

B.status("B.pop()", o);

B.push("Eve");

B.status("B.push(\"Eve\")", null);

}

}

Fragment koda 7.4 Niz-bazirana Java implementacija interfejsa steka

Ispod, je prikazan izlaz iz gornjeg ArrayStack programa. Primetimo, da preko korišćenja generičkih tipova, mi možemo da kreiramo ArrayStack A za smeštanje intedžera i drugi ArrayStack B koji smešta stringove karaktera.

Osvrt na niz-baziranu implementaciju steka Niz implementacija steka je jednostavna i efikasna. Ipak, ova implementacija ima jedan negativni aspekt – mora se pretpostaviti fiksna gornja granica CAPACITY, krajnje veličine steka. U prethodno prikazanom fragmentu koda, mi smo izabrali vrednost kapaciteta 1.000 manje ili više proizvoljno. Aplikacija stvarno može zahtevati mnogo manje prostora nego što smo izabrali, što će bespotrebno trošiti memoriju. Alternativno, aplikacija može zahtevati više prostora nego što smo izabrali, što će prouzrokovati da naša stek implementacija generiše izuzetak kada klijent program pokuša da gurne njegov 1.001 objekat u stek. Zato, čak i sa njenom jednostavnošću i efikasnošću, niz bazirana implementacija steka nije obavezno idealna.

11. Implementiranje steka sa generički povezanom listom

U ovom delu, mi istražujemo korišćenje jednostruko povezane liste da implementiramo ADT stek. U projektovanju takve implementacije, mi moramo da odlučimo da li je vrh steka na glavi ili repu liste. Međutim ovo je jasno najbolji izbor ovde, pošto mi možemo da umetnemo i obrišemo elemente u konstantnom vremenu jedino na glavi. Prema tome, mnogo je efikasnije imati vrh steka na glavi naše liste. Takođe, da bi izveli operaciju size u konstantnom vremenu, mi čuvamo trag trenutnog broja elemenata u instanci promenljive. Pre nego korišćenja povezane liste koja može samo da smesti objekte određenog tipa, kao što smo videli u delu koji se odnosio na jednostruko povezane liste, u ovom slučaju implementiramo stek korišćenjem generički povezane liste. Prema tome, mi treba da koristimo generičku vrstu čvora da implementiramo ovu povezanu listu. Mi prikazujemo takvu klasu Node u fragmentu koda 7.5.

public class Node<E> {

// Instance promenljivih:

private E element;

private Node<E> next;

/**Kreira čvor sa referencom null na svoj element i sledeći čvor*/

public Node() {

this(null, null);

}

/** Kreira čvor sa datim elementom i sledećim čvorom */

public Node(E e, Node<E> n) {

element = e;

next = n;

}

// Metodu pristupa:

public E getElement() {

return element;

}

public Node<E> getNext() {

return next;

}

// Methodi modifikovanja:

public void setElement(E newElem) {

element = newElem;

}

public void setNext(Node<E> newNext) {

next = newNext;

}

}

Fragment koda 7.5 Klasa Node, koja implementira generički čvor za jednostruko povezane liste Generička NodeStack klasa Java implementacija steka, preko generičke jednostruko povezane liste prikazana je u fragmentu koda 7.6. Svi metodi Stack interfejsa su izvršeni u konstantnom vremenu. U skladu sa tim da bude vremenski efikasna, ova implementacija povezane liste ima zahtev za prostorom koji je O(n), gde je n trenutni broj elemenata u steku. Prema tome, ova implementacija ne zahteva da bude kreiran novi izuzetak da upravlja problemom prekoračenja veličine. Mi koristimo instancu promenljive top da uputimo na glavu liste (koja pokazuje na objekat null kada je lista prazna). Kada mi gurnemo novi element e u stek, mi jednostavno kreiramo novi čvor v za e, referencu e iz v, i umećemo v na glavu liste. Isto tako, kada mi skinemo element iz steka, mi jednostavno uklanjamo čvor na glavi liste i vraćamo njegov element. Prema tome, mi izvodimo sva umetanja i uklanjanja elemenata na glavi liste.

public class NodeStack<E> implements Stack<E> {

protected Node<E> top; // referenca na čvor glava

protected int size; // broj elemenata u steku

public NodeStack() { // konstruisanje praznog steka

top = null;

size = 0;

}

public int size() { return size; }

public boolean isEmpty() {

if (top == null) return true;

return false;

}

public void push(E elem) {

//* kreiranje i povezivanje novog čvora */

Node<E> v = new Node<E>(elem, top);

top = v;

size++;

}

public E top() throws EmptyStackException {

if (isEmpty()) throw new EmptyStackException("Stack is empty.");

return top.getElement();

}

public E pop() throws EmptyStackException {

if (isEmpty()) throw new EmptyStackException("Stack is empty.");

E temp = top.getElement();

top = top.getNext(); // povezivanje van bivšeg čvora na vrhu

size--;

return temp;

}

}

Fragment koda 7.6 Klasa NodeStack, koja implementira Stack interfejs korišćenjem jednostruko povezane liste, čiji čvorovi su objekti klase Node iz fragmenta koda 7.5

12. Preokretanje niza korišćenjem steka

Možemo da koristimo stek da preokrenemo elemente u nizu, i pomoću toga da kreiramo nerekurzivni algoritam za problem preokretanja niza koji smo videli u delu koji se odnosio na linearnu rekurziju. Osnovna ideja je da jednostavno gurnemo sve elemente niza po redu u stek i zatim ispunimo niz ponovo guranjem elemenata iz steka. U fragmentu koda 7.7, mi dajemo Java implementaciju ovoga algoritma.

/** Nerekurzivni generički metod za preokretanje niza */

public static <E> void reverse(E[] a) {

Stack<E> S = new ArrayStack<E>(a.length);

for (int i=0; i < a.length; i++)

S.push(a[i]);

for (int i=0; i < a.length; i++)

a[i] = S.pop();

}

Fragment koda 7.7 Generički metod koji preokreće elemente u nizu tipa objekta E, korišćenjem steka deklarisanog korišćenjem Stack <E> interfejsa

Slučajno, ovaj metod takođe ilustruje kako mi možemo da koristimo generičke tipove u jednostavnoj aplikaciji koja koristi generički stek. U osnovi, kada su elementi izgurani iz steka kao u ovom primeru, oni su automatski vraćeni kao elementi tipa E; stoga, oni mogu odmah biti vraćeni u ulazni niz. Mi pokazujemo primer korišćenja ovog metoda u fragmentu koda 7.8.

/** Test rutina za preokretanje nizova */

public static void main(String args[]) {

Integer[] a = {4, 8, 15, 16, 23, 42}; //autoboxing dozvoljava ovo

String[] s = {"Jack", "Kate", "Hurley", "Jin", "Boone"};

System.out.println("a = " + Arrays.toString(a));

System.out.println("s = " + Arrays.toString(s));

System.out.println("Preokretanje...");

reverse(a);

reverse(s);

System.out.println("a = " + Arrays.toString(a));

System.out.println("s = " + Arrays.toString(s));

}

Izlaz iz ovog metoda je sledeći:

a = [4, 8, 15, 16, 23, 42]

s = [Jack, Kate, Hurley, Jin, Michael]

Preokretanje…

a = [42, 23, 16, 15, 8, 4]

s = [Michael, Jin, Hurley, Kate, Jack]

Fragment koda 7.9 Test metoda za preokretanje dva niza

Redovi za čekanje

Druga osnovna struktura podataka je red za čekanje (queue). To je blizak “rođak” steka, pošto je red za čekanje kolekcija objekata koji su umetnuti i uklonjeni u skladu sa first-in first-out (FIFO) principom. To znači, da elementi mogu biti umetnuti u bilo kom trenutku, ali jedino element koji je

najduže bio u redu za čekanje može biti uklonjen u bilo kom trenutku. To znači da se nove komponente dodaju na jednom kraju, a izbacuju sa drugog. Mi obično kažemo da elementi ulaze u red za čekanje sa kraja (rear) i uklanjaju se sa početka (front). Metafora za ovu terminologiju su ljudi u liniji koji čekaju da uđu u zabavni par za vožnju. Ljudi koji čekaju na takvu vožnju dolaze na kraj linije i dobijaju vožnju sa početka linije.

13. Abstraktni tip podataka reda za čekanje

Formalno, abstraktni tip podataka reda za čekanje definiše kolekciju koja čuva objekte u sekvenci, gde je pristup i brisanje elementa ograničeno sa prvim elementom u sekvenci, koji se zove početak (front) reda za čekanje, i umetanje elemenata je ograničeno sa krajem sekvence, koja se zove kraj (rear) reda za čekanje. Ovo ograničenje primenjuje pravilo da su stavke umetnute i obrisane u redu za čekanje u skladu sa first-in first-out (FIFO) principom. Abstraktni tip podataka (ADT) reda za čekanje podržava dva osnovna metoda:

enqueue(e): Umeće element e na kraj sekvence

dequeue(): Uklanja i vraća iz reda za čekanje objekat na početku; greška se pojavljuje ako je red za čekanje prazan

Dodatno, slično sa slučajem ADT Steka, ADT red za čekanje uključuje sledeće metode za podršku:

size(): Vraća broj objekata u redu za čekanje

isEmpty(): Vraća Boolean vrednost koja ukazuje da li je red za čekanje prazan

front(): Vraća, ali ne uklanja, početni objekat u redu za čekanje; greška se pojavljuje ako je red za čekanje prazan

Primer 4: Sledeća tabela pokazuje seriju operacija reda za čekanje i njihove efekte na početno prazan red za čekanje Q intedžer objekata. Zbog jednostavnosti, mi koristimo intedžere umesto intedžer objekata kao argumenata operacija.

Operacija Izlaz početak Q kraj

enqueue(5) - (5)

enqueue(3) - (5,3)

dequeue() 5 (3)

enqueue(7) - (3,7)

dequeue() 3 (7)

front() 7 (7)

dequeue() 7 ()

dequeue() "error" ()

isEmpty() true ()

enqueue(9) - (9)

enqueue(7) - (9,7)

size() 2 (9,7)

enqueue(3) - (9,7,3)

enqueue(5) - (9,7,3,5)

dequeue() 9 (7,3,5)

Primeri primene Postoji nekoliko mogućih primena redova za čekanje. Radnje, pozorišta, centri za rezervaciju i drugi slični servisi tipično procesiraju zahteve mušterija u skladu sa FIFO principom. Red za čekanje zato treba da bude logičan izbor za strukture podataka koje manipulišu procesiranjem transakcija za takve aplikacije. Na primer, to treba da bude prirodan izbor za upravljanje pozivima u centru za rezervacije aerodroma.

14. Jednostavna niz-bazirana implementacija reda za čekanje

Predstavićemo jednostavnu realizaciju reda za čekanje posmatrajući niz Q, fiksiranog kapaciteta koji smešta njegove elemente. Pošto je glavno pravilo ADT reda za čekanje da mi umećemo i uklanjamo objekte u skladu sa FIFO principom, moramo odlučiti kako ćemo pratiti trag početka i kraja reda za čekanje. Jedna mogućnost je da prilagodimo pristup koji smo koristili za implementaciju steka, dozvoljavajući da Q[0] bude početak reda za čekanje i zatim puštajući da red za čekanje raste od tog mesta. Međutim, ovo nije efikasno rešenje zato što zahteva da pomerimo sve elemente unapred za jednu ćeliju niza svaki put kada izvodimo dequeue operaciju. Takva implementacija će stoga uzeti O(n) vreme da izvede dequeue metod, gde je n trenutni broj objekta u redu za čekanje. Ako želimo da postignemo konstantno vreme za svaki queue metod, potreban nam je različiti pristup. Korišćenje niza na cirkularni način Da bi izbegli pomeranje objekata jednom kada su smešteni u Q, definišemo dve promenljive f i r, koje imaju sledeća značenja:

f je indeks ćelije niza Q koja smešta prvi element reda za čekanje (koji je sledeći kandidat da bude uklonjen dequeue operacijom), sem ako red za čekanje nije prazan (u tom slučaju f = r)

r je indeks sledeće dostupne ćelije niza u Q

Početno, mi dodeljujemo f = r = 0, koji ukazuje da je red za čekanje prazan. Sada, kada uklonimo element sa početka reda za čekanje, mi inkrementiramo f do indeksa sledeće ćelije. Isto tako, kada dodamo element, mi ga smeštamo u ćeliju Q[r] i inkrementiramo r do indeksa sledeće dostupne ćelije u Q. Ova šema nam dozvoljava da implementiramo metode, front, enqueue i dequeue u konstantnom vremenu, koje je O(1) vreme. Međutim, još uvek postoji problem sa ovim pristupom.

Razmatraćemo, na primer, šta se događa kada ponavljamo enqueue i dequeue na jednom elementu, N različitih puta. Mi treba da imamo f = r = N. Ako zatim pokušamo da umetnemo element još jednom, mi ćemo dobiti grešku array-out-of-bounds (pošto N validne lokacije u Q su od Q[0] od Q[n-1], čak i kada postoji mnogo “soba“ u redu za čekanje u ovom slučaju. Da bi izbegli ovaj problem i da bi mogli da iskoristimo ceo niz Q, mi dozvoljavamo da se f i r indeksi “obavijaju oko” (wrap around) kraja Q. To znači, da sada gledamo Q kao “cirkularni niz“ koji ide od Q[0] do Q[n-1] i zatim se odmah vraća na Q[0] ponovo. To je prikazano na slici 7.3.

Slika 7.3 Korišćenje niza Q na cirkularni način

a) “normalna“ konfiguracija sa f r

b) “wrap around“ konfiguracija sa r < f. Ćelije koje smeštaju elemente reda za čekanje su zasenčene.

Implementiranje ovo cirkularnog pogleda na Q je realno gledano prilično lako. Svaki put kada inkrementiramo f ili r, mi izračunavamo ove inkremente kao “(f+1) mod N“ ili “(r+1) mod N“, respektivno.

Podsetimo se da operator “mod“ je modulo operator, koji je izračunat uzimanjem ostatka nakon operacije deljenja. Na primer, 14 podeljeno sa 4 je 3 sa ostatkom 2, tako da 14 mod 4 = 2.

Specifično, za date intedžere x i y takve da je x 0 i y 0, imamo da je x mod y = x - x/y y. To znači, da ako je r = x mod y, tada postoji nenegativni intedžer q, takav da je x = qy + r. Java koristi “%“ da označi modulo operator. Korišćenjem modulo operatora, mi možemo da posmatramo Q kao cirkularni niz i da implementiramo svaki metod reda za čekanje u konstantnom vremenu (koje je O(1) vreme). Mi opisujemo kako da koristimo ovaj pristup da implementiramo red za čekanje u fragmentu koda 7.10. Implementacija koristi modulo operator da “obavije“ indekse oko kraja niza i takođe uključuje dve instance promenljivih, f i r, koje indeksiraju početak reda za čekanje i prvu praznu ćeliju nakon kraja reda za čekanje, respektivno.

Algorithm size(): return (N – f + r) mod N

Algorithm isEmpty(): return (f = r)

Algorithm front(): If isEmpty() then throw a QueueEmptyException return Q[f]

Algorithm dequeue(): If isEmpty() then throw a QueueEmptyException

temp Q[f]

Q[f] null

f (f+1) mod N return temp

Algorithm enqueue(e): If size() = N - 1 then throw a FullQueueException

Q[r] e

r (r + 1) mod N

Fragment koda 7.10 Implementacija reda za čekanje korišćenjem cirkularnog niza

Gornja implementacija sadrži značajan detalj, koji može biti prvo propušten. Razmotrićemo situaciju koja se pojavljuje ako mi umećemo N objekata u Q bez njihovog uklanjanja. Mi ćemo imati f = r, koji je isti uslov koji se pojavljuje kada je red za čekanje prazan. Stoga, mi u ovom slučaju nećemo biti u mogućnosti da iskažemo razliku između punog i praznog reda za čekanje. Na sreću, ovo nije veliki problem i postoji različiti broj načina koji rade sa tim. Rešenje koje ćemo opisati ovde insistira da Q ne može nikad držati više od N - 1 objekata. Ovo jednostavno pravilo za manipulisanje punim redom za čekanje vodi računa o konačnom problemu sa našim rešenjem, i vodi ka opisu metoda reda za čekanje pseudo-kodom datim u fragmentu koda 7.5. Primetimo naše uvođenje implementaciono-specifičnog izuzetka, zvanog FullQueueException, koje signalizira da nema više elemenata koji mogu biti umetnuti u red za čekanje. Primetimo takođe da način na koji izračunavamo veličinu reda za čekanje preko izraza (N – f + r) mod N, daje ispravan

rezultat u obe, “normalnoj” konfiguraciji (kada je f r) i u “obavijenoj okolo“ konfiguraciji (kada je r < f). Java implementacija reda za čekanje preko niza je slična onoj za stek. Sledeća tabela pokazuje vreme rada metoda preko niz-bazirane implementacije reda za čekanje. Kao i sa našom niz-baziranom implementacijom steka, svaki od metoda reda za čekanje u nizu realizuje izvršavanje konstantog broja iskaza obuhvatajući aritmetičke operacije, upoređivanja i dodeljivanja. Zato, svaki metod u ovoj implementaciji radi O(1) vremena. Korišćenost prostora je O(N), gde je N veličina niza, određena trenutkom kada je red za čekanje kreiran. Primetimo da je korišćenost prostora nezavisna od broja n < N elemenata koji su stvarno u nizu.

Metod Vreme

size O(1)

isEmpty O(1)

front O(1)

enqueue O(1)

dequeue O(1)

Kao i sa niz-baziranom implementacijom steka, jedini pravi nedostatak niz-bazirane implementacije reda za čekanje je da mi veštački setujemo da kapacitet reda za čekanje bude neka fiksna vrednost. U realnim aplikacijama, može nam biti potrebno više ili manje kapaciteta reda za čekanje nego što smo predvideli, ali ako imamo dobru procenu kapaciteta, tada je niz-bazirana implementacija prilično efikasna.

15. Implementiranje reda za čekanje sa generički povezanom listom

Mi možemo efikasno da implementiramo ADT red za čekanje korišćenjem generičke jednostruko povezane liste. Iz razloga efikasnosti, mi biramo početak reda za čekanje da bude glava liste i kraj reda za čekanje da bude rep liste. Na ovaj način, mi uklanjamo sa glave i umećemo na rep. Primetimo

da moramo da održimo reference na čvorove glave i repa liste. Pre nego da idemo u svaki detalj ove implementacije, mi jednostavno dajemo Java implementaciju za osnovne metode reda za čekanje u fragmentu koda 7.11.

public void enqueue(E elem) {

Node<E> node = new Node<E>();

node.setElement(elem);

node.setNext(null); // čvor će biti novi novi rep čvor

if (size == 0)

head = node; //specijalni slučaj prethodno praznog reda za čekanje

else

tail.setNext(node); // dodavanje čvora na rep liste

tail = node; // ažuriranje reference na rep čvor

size++;

}

...

public E dequeue() throws EmptyQueueException {

if (size == 0)

throw new EmptyQueueException("Queue is empty.");

E tmp = head.getElement();

head = head.getNext();

size--;

if (size == 0)

tail = null; // red za čekanje je prazan

return tmp;

}

Fragment koda 7.11 Metodi enqueue i dequeue u implementaciji ADT reda za čekanje preko jednostruko povezane liste, korišćenjem čvorova iz klase Node date u fragmentu koda 7.5

Svaki od metoda ADT reda za čekanje implementiranog sa jednostruko povezanom listom radi u O(1) vremenu. Mi takođe izbegavamo potrebu da specificiramo maksimalnu veličinu za red za čekanje, kao što je rađeno sa niz-baziranom implementacijom reda za čekanje, ali ova prednost dolazi od povećanja trošenja količine prostora korišćenog po elementu. Pored toga metodi u implementacij i reda sa čekanje sa jednostruko povezanom listom su komplikovaniji nego što možda želimo, zbog čega moramo da vodimo više računa kako radimo u specijalnim slučajevima kada je red za čekanje prazan pre metoda enqueue ili kada red za čekanje postaje prazan nakon metoda dequeue.

16. Dvostruko-završeni redovi za čekanje

Razmatraćemo sada red za čekanje kao strukturu podataka koja podržava umetanje i brisanje i na početku i na kraju reda za čekanje. Takvo izduženje reda za čekanje se zove dvostruko-završeni red za čekanje ili deque, koji se obično izgovara “dek” da izbegnemo konfuziju sa dequeue metodom regularnog ADT reda za čekanje, koji se izgovara skraćeno kao “D.Q“.

Abstraktni tip podataka dvostruko-završenog reda za čekanje

Abstraktni tip podataka dvostruko-završenog reda za čekanje je bogatiji od ADT steka i ADT reda za čekanje. Osnovni metodi dvostruko-završenog reda za čekanje su sledeći:

addFirst(e): Umeće novi element e na početak dvostruko-završenog reda za čekanje

addLast(e): Umeće novi element e na kraj dvostruko-završenog reda za čekanje

removeFirst(): Uklanja i umeće prvi element dvostruko-završenog reda za čekanje; greška se

pojavljuje ako je dvostruko-završeni red za čekanje prazan

removeLast(): Uklanja i umeće zadnji element dvostruko-završenog reda za čekanje; greška

se pojavljuje ako je dvostruko-završeni red za čekanje prazan

Dodatno, ADT dvostruko-završenog reda za čekanje može takođe da uključi sledeće metode za podršku:

getFirst(): Vraća prvi element dvostruko-završenog reda za čekanje; greška se

pojavljuje ako je dvostruko-završeni red za čekanje prazan

getLast(): Vraća zadnji element dvostruko-završenog reda za čekanje; greška se

pojavljuje ako je dvostruko-završeni red za čekanje prazan

size(): Vraća broj objekata dvostruko-završenog reda za čekanje

isEmpty(): Određuje da li je dvostruko-završeni red za čekanje prazan

Sledeća tabela pokazuje seriju operacija i njihove efekte na početno prazni dvostruko-završeni red za čekanje D intedžer objekata. Zbog jednostavnosti, koristimo cele brojeve umesto intedžer objekata kao argumenata operacija.

Operacija Izlaz D

addFirst(3) - (3)

addFirst(5) - (5,3)

removeFirst() 5 (3)

addLast(7) - (3,7)

removeFirst() 3 (7)

removeLast() 7 ()

removeFirst() "error" ()

isEmpty() true ()

7.2.5.2 Implementiranje dvostruko-završenog reda za čekanje Pošto dvostruko-završeni red za čekanje zahteva umetanje i uklanjanje na oba kraja liste, korišćenje povezane liste da implementira dvostruko-završeni red za čekanje biće neefikasno. Međutim, možemo da koristimo dvostruko povezanu listu da implementiramo dvostruko-završeni red za čekanje efikasno. Za umetanje novog elementa e, možemo da pristupimo elementu p pre nego što mesto e treba da krene i čvor q nakon mesta e treba da krene. Da umetnemo novi element između dva čvora p i q (li jedan ili oba mogu da budu stražari), mi kreiramo novi čvor t, koji ima linkove prev i next, koji se odnose respektivno na p i q, i tada imamo next link od p koji upućuje na t, i imamo prev link od q koji upućuje na t. Isto tako, da uklonimo element smešten u čvoru t, možemo da pristupimo čvorovima p i q na obe strane od t (i ovi čvorovi moraju da postoje, pošto moramo da koristimo stražare). Da uklonimo čvor t između čvorova p i q, mi jednostavno imamo p i q koji upućuju jedan na drugi umesto na t. Mi ne moramo da menjamo nijedno polje u t, i sada t može biri popravljeno sakupljačem otpada, pošto niko ne upućuje na t. Sledeća tabela pokazuje vremena rada metoda za dvostruko-završeni red za čekanje implementiran sa dvostruko povezanom listom. Primetimo da svaki metod radi u O(1) vremenu.

Metod Vreme

size, is Empty O(1)

getFirst, getLast O(1)

addFirst, addLast O(1)

removeFirst, removeLast O(1)

Prema tome, dvostruko povezana lista može biti korišćena da implementira svaki metod ADT dvostruko-završenog red za čekanje u konstantnom vremenu. Slučajno, svi metodi ADT dvostruko-završenog reda za čekanje, koji su opisani iznad, uključeni su u java.util.LinkedList <E> klasu. Tako da ako nam je potreban dvostruko-završeni red za čekanje i ne želimo da ga implementiramo od nule, možemo jednostavno da koristimo java.util.LinkedList <E> klasu. Interfejs Deque prikazan je u fragmentu koda 7.13 i implementacija ovog interfejsa prikazana je u fragmentu koda 7.14.

public interface Deque<E> {

/** Vraća broj elemenata u deque */

public int size();

/** Vraća da li je deque prazan */

public boolean isEmpty();

/** Vraća prvi element; uhvaćen je izuzetak ako je deque prazan */

public E getFirst() throws EmptyDequeException;

/** Vraća zadnji element; uhvaćen je izuzetak ako je deque prazan */

public E getLast() throws EmptyDequeException;

/** Umeće element na bude prvi u deque. */

public void addFirst (E element);

/** Umeće element na bude zadnji u deque. */

public void addLast (E element);

/** Uklanja prvi element; uhvaćen je izuzetak ako je deque prazan */

public E removeFirst() throws EmptyDequeException;

/** Uklanja zadnji element; uhvaćen je izuzetak ako je deque prazan */

public E removeLast() throws EmptyDequeException;

}

Fragment koda 7.13 Interfejs Deque dokumentovan sa komentarima u javadoc stilu. Primetimo korišćenje generički parametrizovanog tipa E, koje implicira da dvostruko-završeni red za čekanje

može da sadrži elemente bilo koje specificirane klase

public class NodeDeque<E> implements Deque<E> {

protected DLNode<E> header, trailer; // stražari

protected int size; // broj elemenata

public NodeDeque() { // inicializovanje praznog deque

header = new DLNode<E>();

trailer = new DLNode<E>();

header.setNext(trailer); // make header point to trailer

trailer.setPrev(header); // make trailer point to header

size = 0;

}

public int size() {

return size;

}

public boolean isEmpty() {

if (size == 0)

return true;

return false;

}

public E getFirst() throws EmptyDequeException {

if (isEmpty())

throw new EmptyDequeException("Deque is empty.");

return header.getNext().getElement();

}

public void addFirst(E o) {

DLNode<E> second = header.getNext();

DLNode<E> first = new DLNode<E>(o, header, second);

second.setPrev(first);

header.setNext(first);

size++;

}

public E removeLast() throws EmptyDequeException {

if (isEmpty())

throw new EmptyDequeException("Deque is empty.");

DLNode<E> last = trailer.getPrev();

E o = last.getElement();

DLNode<E> secondtolast = last.getPrev();

trailer.setPrev(secondtolast);

secondtolast.setNext(trailer);

size--;

return o;

}

}

Fragment koda 7.14 Klasa NodeDeque koja implementirana Deque interfejs, osim što nismo prikazali klasu DLNode, koja je generička dvostruko povezana lista, i nismo prikazali metode getLast, addLast

removeFirst

Niz liste Pretpostavimo da imamo kolekciju S od n elemenata smeštenih u određenom linearnom redu, tako da mi možemo da se obraćamo elementima u S kao prvom, drugom, trećem i tako dalje. Takvoj kolekciji se generički obraćamo kao listi (list) ili sekvenci. Mi možemo jedinstveno da se obraćamo svakom elementu e u S korišćenjem intedžera u opsegu [0, n -1] koji je jednak broju elemenata S koji prethode elementu e u S. Indeks elementa e u S je broj elemenata koji se nalaze pre elementa e u S. Stoga, prvi element e u S ima indeks 0 i zadnji element ima indeks n -1. Takođe, ako element od S ima indeks i, njegov prethodni element (ako postoji) ima indeks i -1, i njegov sledeći element (ako postoji) ima indeks i +1. Ovaj koncept indeksa je povezan sa rangom elementa u listi, koji je obično definisan da bude za jedan veći nego njegov indeks; tako da prvi element ima rang 1, drugi element ima rang 2, itd. Sekvenca koja podržava pristup svojim elementima preko njihovih indeksa zove se niz lista (ili vektor, korišćenjem starijih termina). Pošto je naša definicija indeksa više konzistentna sa načinom na koji su nizovi indeksirani u Javi i drugim programskim jezicima (kao što su C i C++), mi ćemo se obraćati mestu gde je element smešten u niz listi kao njegovom “indeksu”, a ne njegovom ”rangu” (mada možemo da koristimo r da označimo ovaj indeks, ako se slovo ”i” koristi za brojač for-petlje). Ovaj koncept indeksa je jednostavna moćna notacija, pošto može biti korišćena da specificira gde da se umetne nov element u listu ili gde da se ukloni stari element.

17. Abstraktni tip podataka niz liste

Kao i ADT, niz lista S ima sledeće metode (pored standardnih size() i isEmpty() metoda):

get(i): Vraća element S sa indeksom i; uslov greške pojavljuje sa ako je i < 0 ili i > size() -1.

set(i,e): Zamenjuje sa e i vraća element sa indeksom i; uslov greške pojavljuje sa ako i < 0 ili i> size() -1.

add(i,e): Umeće novi element e u S da ima indeks i; uslov greške pojavljuje sa ako i < 0 ili i> size().

remove(i): Uklanja iz S element sa indeksom i; uslov greške pojavljuje sa ako i < 0 ili i> size() - 1.

Mi ne insistiramo da niz treba da bude korišćen da implementira niz listu, tako da je element sa indeksom 0 smešten na indeks 0 u nizu, mada je to jedna (vrlo prirodna) mogućnost. Definicija indeksa pruža nam način da uputimo na ”mesto” gde je element smešten u sekvenci, bez vođenja računa o konkretnoj implementaciji takve sekvence. Međutim, indeks elementa može da se menja svaki put kada je sekvenca ažurirana, kao što ilustrujemo u sledećem primeru.

Primer 5: Ispod su prikazane neke operacije na početno praznoj niz listi S.

Operacija Izlaz S

add(0,7) - (7)

add(0,4) - (4,7)

get(1) 7 (4,7)

add(2,2) - (4,7,2)

get(3) "error" (4,7,2)

remove(1) 7 (4,2)

add(1,5) - (4,5,2)

add(1,3) - (4,3,5,2)

add(4,9) - (4,3,5,2,9)

get(2) 5 (4,3,5,2,9)

set(3,8) 2 (4,3,5,8,9)

19. Jednostavna niz-bazirana implementacija

Očigledan primer za implementiranje ADT niz liste je korišćenje niza A, gde A[i] smešta (referencu na) element sa indeksom i. Mi biramo veličinu N niza A dovoljno velikom i održavamo broj elemenata u instanci promenljive n < N. Detalji ove implementacije ADT niz liste su jednostavni. Da bi implementirali get(i) operaciju, na primer, mi samo vraćamo A[i]. Implementacija metoda add(i,e) i remove(i) su date u fragmentu koda 7.15. Sa n smo označili instancu promenljive koja smešta broj elemenata u niz listi.

Algorithm add(i,e): for j = n -1, n - 2,..., i do

A[j+1] A[j] {pravi sobu za novi element}

A[i] e

n n + 1

Algorithm remove(i):

e A[i] {e je privremena promenljiva} for j = i, i + 1,..., n - 2 do

A[j] A[j+1] {popunjava za uklonjeni element}

n n – 1 return e

Fragment koda 7.15 Metodi add(i,e) i remove(i) u niz implementaciji ADT niz liste

Važan (i koji troši vreme) deo ove implementacije obuhvata pomeranje elemenata gore ili dole da bi se sačuvale zauzete ćelije u nizu pripojenim. Ove operacije pomeranje su zahtevane da održe naše pravilo da se uvek smešta element čiji indeks liste je i, na indeks i u nizu A. To je prikazano na slici 7.4

18.Šablon adapter

Klase su često pisane tako da obezbede sličnu funkcionalnost sa drugim klasama. Šablon projektovanja adapter primenjuje se na neki sadržaj gde želimo da modifikujemo postojeću klasu tako da njeni metodi odgovaraju sličnim, ali iz različitih klasa ili interfejsa. Jedan opšti način za primenu šablona projektovanja je da definiše novu klasu na takav način da ona sadrži instancu stare klase kao skriveno polje, i da implementira svaki metod nove klase korišćenjem metoda ove skrivene instance promenljive. Rezultat primene šablona projektovanja je da novokreirana klasa izvodi skoro iste funkcije kao prethodna klasa, ali na mnogo pogodniji način. U skladu sa našom diskusijom ADT niz liste, mi primećujemo da je ovaj ADT dovoljan da definiše adapter klasu za deque ADT, kao što je prikazano u tabeli ispod.

Deque Metod Realizacija sa metodima niz-liste

size(), isEmpty() size(), isEmpty()

getFirst() get(0)

getLast() get(size() -1)

addFirst(e) add(0,e)

addLast(e) add(size(),e)

removeFirst() remove (0)

removeLast() remove(size() - 1)

Slika 7.5 Niz-bazirana implementacija niz liste S koja smešta n elemenata

a) pomeranje nagore za umetanje na indeks i

b) pomeranje naniže za uklanjanje na indeksu i Osobine jednostavne niz-bazirane implementacije Sledeća tabela pokazuje vremena rada najgoreg-slučaja metoda niz liste sa n elemenata realizovanih u značenju niza. Metodi isEmpty, size, get i set jasno rade O(1) vremena, ali umetanje i uklanjanje metoda može uzeti mnogo više od ovog vremena. Posebno, add(i,e) radi O(n) vreme. U stvari, najgori slučaj za ovu operaciju pojavljuje se kada je i = 0, pošto svi postojeći n elementi treba da budu pomerani napred. Sličan argument primenjuje se na metod remove(i), koji radi O(n) vreme, zato što moramo da pomerimo unazad n-1 elemenata u najgorem slučaju (i = 0). U stvari, pretpostavljajući da jednako verovatno da svaki mogući indeks bude prosleđen kao argument ovih operacija, njihovo prosečno vreme rada je O(n), za koje treba da pomerimo prosečno n/2 elemenata.

Metod Vreme

size() O(1)

isEmpty() O(1)

get(i) O(1)

set(i,e) O(1)

add(i,e) O(n)

remove(i) O(n)

Posmatrajući bliže add(i,e) i remove (i), mi primećujemo da svaki rad u vremenu O(n – i + 1), za jedino one elemente sa indeksom i i više treba da bude pomeren nagore ili nadole. Prema tome, umetanje ili uklanjanje stavke na kraju niz liste, korišćenjem metoda add(n,e) i remove (n-1), traje O(1) vremena svako, respektivno. Osim toga, ovo zapažanje ima interesantnu posledicu za prilagođavanje ADT niz liste u deque ADT listu prikazanu u sekciji 7.3.1. Ako je ADT niz lista u ovom slučaju implementirana preko niza opisanog gore, tada metodi addLast i removeLast od deque rade svaki O(1) vreme. Međutim, metodi addFirst i removeFirst od deque rade svaki O(n) vreme. Realno, sa malo napora, možemo da proizvedemo niz-baziranu implementaciju niz liste koja postiže O(1) vreme za umetanje i uklanjanje na indeks 0, kao i umetanje i uklanjanje sa kraja niz liste. Međutim, postizanje ovoga zahteva da mi zadamo pravilo da je element sa indeksom i smešten u niz na indeks i, kao što smo mogli da koristimo cirkularni pristup nizu kao onaj koji smo koristili da implementiramo red za čekanje.

20. Jednostavni interfejs i java.util.ArrayList klasa

Da bi se pripremili za konstruisanje Java implementacije ADT niz liste, mi pokazujemo u fragmentu koda 7.16, Java interfejs IndexList, koji koristi IndexOutOfBoundsException da signalizira pogrešan argument indeksa.

public interface IndexList<E> {

/** Vraća broj elemenata u ovoj listi. */

public int size();

/** Vraća da li je lista prazna. */

public boolean isEmpty();

/** Umeće element e da bude indeks i, pomerajući sve elem. nakon

ovoga.*/

public void add(int i, E e)

throws IndexOutOfBoundsException;

/** Vraća element na indeks i, bez njegovog uklanjanja. */

public E get(int i)

throws IndexOutOfBoundsException;

/** Uklanja i vraća element na indeks i, pomerajući elem. nakon

ovoga. */

public E remove(int i)

throws IndexOutOfBoundsException;

/** Zamenjuje elem. sa indeksom i sa e, vraćajući prethodni elem. na

i.*/

public E set(int i, E e)

throws IndexOutOfBoundsException;

}

Fragment koda 7.16 Interfejs IndexList za ADT niz listu

Klasa java.util.ArrayList Java obezbeđuje klasu java.util.ArrayList, koja implementira sve metode koje smo gore dali za našu ADT niz listu. To znači, da uključuje sve metode uključene u fragmentu koda 7.7 za interfejs IndexList. Osim toga, klasa java.util.ArrayList ima osobine u skladu sa onim od naše pojednostavljene ADT niz liste. Na primer, klasa java.util.ArrayList takođe uključuje metod clear(), koji uklanja sve elemente iz niz liste, i metod toArray(), koji vraća niz koji sadrži sve elemente niz liste u istom redosledu. U skladu sa tim, klasa java.util.ArrayList takođe ima metode za pretragu liste, uključujući metod indexOf(e), koji vraća indeks prve pojave elementa jednakom e u niz listi, i metod lastIndexOf(e) koji vraća indeks zadnje pojave elementa jednakom e u niz listi. Oba od ovih metoda vraćaju (pogrešnu) vrednost indeksa -1 ako nije pronađen element jednak e.

21. Implementiranje niz liste korišćenjem proširenog niza

Osim implementiranja metoda interfejsa IndexList (i nekih drugih korisnih metoda), klasa java.util.ArrayList obezbeđuje interesantnu osobinu da prevazilazi slabosti u implementaciji jednostavnog niza. Specifično, glavna slabost implementacije jednostavnog niza za ADT niz listu prethodno datu, je da zahteva dodatnu specifikaciju fiksiranog kapaciteta, za ukupni broj elemenata koji može biti smešten u niz listi. Ako je trenutni broj elemenata n, niz lista je mnogo manja od N, i tada će ova implementacija trošiti prostor. Što je najgore, ako n pređe prošlo N, tada će ova implementacija pući. Umesto toga, klasa java.util.ArrayList koristi interesantnu tehniku proširenja-niza, tako da ne moramo da brinemo o prekoračenju niza kada koristimo ovu klasu. Kao i klasa java.util.ArrayList, obezbedićemo način da povećamo niz A koji smešta elemente niz liste S. Naravno, u Javi (i drugim programskim jezicima), mi ne možemo stvarno da povećamo niz A; njegov kapacitet je fiksiran na nekom broju N, kao što smo već videli. Umesto toga, kada se pojavi prekoračenje (overflow), to znači da je n = N i mi pravimo poziv na metod add, i izvodimo sledeće dodatne korake:

Dodeljivanje novog niza B kapaciteta 2N

Neka je B[i] A[i], za i=0,...N-1

Neka je A B, to znači da koristimo B kao niz koji podržava S

Umećemo novi element u A

Ova strategija zamene niza poznata je kao prošireni niz (extendable array), u kojoj može biti posmatrano proširenje kraja postojećeg niza da napravi prostor za više elemenata. To je prikazano na slici 7.6.

Slika 7.6 Ilustracija tri koraka za “rast” proširenog niza

a) kreiranje novog niza B

b) kopiranje elemenata iz A u B

c) dodeljivanje reference A novom nizu. Nije prikazano buduće sakupljanje otpada starog niza

Implementiranje interfejsa IndexList sa proširenim nizom Delovi Java implementacije ADT niz liste korišćenjem proširenog niza prikazani su u fragmentu koda 7.17. Ova klasa obezbeđuje jedino značenje da niz raste.

/** Realizacija indeksne liste preko niza, koji je dupliran kada

* veličina indeksne liste prekoračuje kapacitet niza */

public class ArrayIndexList<E> implements IndexList<E> {

private E[] A; // niz koji smešta elemente indeksne liste

private int capacity = 16; // početna dužina niza A

private int size = 0; // broj elemenata smeštenih u indeksnoj

listi

/** Kreira indeksnu listu sa početnim kapacitetom 16

public ArrayIndexList() {

A = (E[]) new Object[capacity]; //Kompajler može da upozori, ali

je OK

}

/** Umeće element na zadati indeks */

public void add(int r, E e)

throws IndexOutOfBoundsException {

checkIndex(r, size() + 1);

if (size == capacity) { // prekoračenje

capacity *= 2;

E[] B =(E[]) new Object[capacity];

for (int i=0; i<size; i++)

B[i] = A[i];

A = B;

}

for (int i=size-1; i>=r; i--) // pomera elemente na gore

A[i+1] = A[i];

A[r] = e;

size++;

}

/** Uklanja element na zadatom indeksu */

public E remove(int r)

throws IndexOutOfBoundsException {

checkIndex(r, size());

E temp = A[r];

for (int i=r; i<size-1; i++) // pomera elemente na dole

A[i] = A[i+1];

size--;

return temp;

}

Fragment koda 7.17 Delovi klase ArrayIndexList koja realizuje ADT niz listu preko proširenog niza. Metod checkIndex(r,n) koji nije prikazan proverava da li je indeks r u opsegu [0,n-1].

Amortizovana analiza proširenog niza Ova strategija zamene niza može na prvi pogled izgledati sporo, zato što izvođenje zamene jednostavnog niza umetanjem nekog elementa može da traje O(n) vreme. Primetimo još da nakon što izvedemo zamenu niza, naš novi niz nam dozvoljava da dodamo n novih elemenata u niz listu pre nego što niz mora biti zamenjen ponovo. Ova jednostavna činjenica dozvoljava nam da pokažemo da izvođenje serije operacija na početnoj praznoj niz listi je zaista prilično efikasno. Kao skraćena notacija, možemo da se odnosimo na umetanje elementa da bude zadnji element u niz lista kao operaciju guranja (push). To je prikazano na slici 7.7.

Slika 7.7 Vremena rada serije operacija guranja na java.util.ArrayList početne veličine 1

Korišćenjem algoritamskog šablona projektovanja koji se zove amortizacija (amortization), možemo da pokažemo da je izvođenje sekvence operacija guranja na niz listi implementiranoj sa proširenim nizom zaista prilično efikasno. Da bi izveli analizu amortizovanja, mi koristimo tehniku računanja gde posmatramo računar kao novac-operativni uređaj koji zahteva plaćanje jednog sajber-dolara za konstantnu količinu vremena izračunavanja. Kada je operacija izvršena, mi treba da imamo dovoljno sajber-dolara dostupnih na našem trenutnom “računu u banci” da platimo za vreme rada te operacije. Prema tome, ukupna suma sajber-dolara potrošenih za neko izračunavanje biće proporcionalna ukupnom vremenu provedenom na tom izračunavanju. Lepota korišćenja ovog metoda analize je da možemo da preplatimo neke operacije da bi sačuvali sajber-dolare da platimo nešto drugo.

Predlog 1: Neka S bude niz lista implementirana preko proširenog niza sa početnom dužinom 1. Ukupno vreme da izvedemo seriju n operacija guranja u S, počevši od S koje je bilo prazno je O(n).

Opravdanost: Pretpostavimo da je jedan sajber-dolar dovoljan da platimo za izvršavanje svake operacije guranja u S, isključujući vreme provedeno za rast niza. Takođe, pretpostavimo da rast niza od veličine k do veličine 2k zahteva k sajber-dolara za vreme provedeno u kopiranju elemenata. Naplatićemo svaku operaciju guranja tri sajber-dolara. Prema tome, mi preplaćujemo svaku operaciju guranja koja ne uzrokuje prekoračenje od dva sajber-dolara. Razmišljajmo da smo profitirali dva sajber-dolara u umetanju koje ne povećava niz pošto je “smešten“ umetnuti element. Prekoračenje se

pojavljuje kada niz lista S ima 2i elemenata, za neki intedžer i 0, i kada veličina niza korišćenog od strane niz liste koju predstavlja S je 2. Prema tome, dupliranje veličine niza će zahtevati 2i sajber-dolara. Na sreću, ovi sajber-dolari mogu biti pronađeni u elementima smeštenim u ćelijama od 2i-1 do 2i -1. To je prikazano na slici 7.8. Primetimo da se prethodno prekoračenje pojavilo kada je broj elemenata postao veći od 2i-1 prvi put, i prema tome sajber-dolari smešteni u ćelijama od 2i-1 do 2i -1 nisu prethodno potrošeni. Stoga, imamo opravdanu amortizacionu šemu u kojoj je svaka operacija naplaćena tri sajber-dolara i plaćeno je sve vreme izračunavanja. To znači, da možemo da platimo izvršavanje n operacija guranja korišćenjem 3n sajber-dolara. Drugim rečima, amortizovano vreme rada svake operacije guranja je O(n); stoga, ukupno vreme rada n operacija guranja je O(n).

Slika 7.8 Ilustracija serije operacija guranja na niz listi:

a) niz sa 8 ćelija je pun, sa dva sajber-dolara “smeštena“ u ćelijama od 4 od 7

b) operacije guranja prouzrokuju prekoračanje i dupliranje kapaciteta. Kopiranje starih osam elemenata u novi niz je plaćeno sajber-dolarima smeštenim u tabeli. Umetanje novog elementa je plaćeno jednim od sajber-dolara plaćenim operaciji guranja, i dva zarađena sajber-dolara smeštena su u ćeliji 8.

Iteratori

Tipično izračunavanje na niz listi, listi ili sekvenci je kretanje kroz njihove elemente po redu, jednom u vremenu, na primer, da bi se tražio specifični element.

22. Iterator i Iterable abstraktni tipovi podataka

Iterator je šablon softverskog projektovanja koji sažima proces skaniranja kroz kolekciju elemenata, jedan element u trenutku. Iterator se sastoji od sekvence S, trenutnog elementa u S, i načina stupanja na sledeći element u S i pravljenjem njega kao trenutnog elementa. Prema tome, iterator proširuje koncept ADT pozicije. U stvari, pozicija može biti posmatrana kao iterator koji ne ide nikuda. Iterator učaurava koncepte “mesto” i “sledeće” u kolekciji objekata. Mi definišemo za ADT iterator sledeća dva metoda:

hasNext(): Testira da li postoje elementi levo u iteratoru

addFirst(e): Vraća sledeći element u iterator

Primetimo da ADT iterator ima značenje “trenutni“ element u kretanju sekvence. Prvi element u iteratoru je vraćen prvim pozivom metoda next, pretpostavljajući naravno da iterator sadrži najmanje jedan element.

Iterator obezbeđuje jedinstvenu šemu da pristupi svim elementima kolekcije objekata na način koji je nezavistan od specifične organizacije kolekcije. Iterator za niz listu, listu ili sekvencu treba da vrati elemente u skladu sa njihovim linearnim uređenjem. Jednostavni iteratori u Javi Java obezbeđuje iterator preko interfejsa java.util.Iterator. Ovaj interfejs podržava dodatni (opcioni) metod da ukloni prethodno vraćen element iz kolekcije. Međutim, ova funkcionalnost (uklanjanje elemenata preko iteratora) je nešto kontraverzno sa objektno-orijentisane tačke gledišta i nije iznenađujuće da je njegova implementacija preko klasa opciona. Slučajno, Java takođe obezbeđuje interfejs java.util.Enumeration, koji je istorijski stariji od iterator interfejsa i koristi imena hasMoreElements() i nextElement(). Iterable abstraktni tip podataka Da bi obezbedili jedinstveni generički mehanizam za skaniranje kroz strukturu podataka, ADT kolekcije smeštanja objekata treba da podrže sledeći metod:

iterator(): Vraća iterator elemenata u kolekciji

Ovaj metod je podržan od strane klase java.util.ArrayList. U stvari, ovaj metod je toliko

značajan, da postoji ceo interfejs, java.lang.Iterable, koji ima samo ovaj metod u sebi. Ovaj metod može da napravi jednostavnim za nas specificiranje proračuna koji treba da izvode petlju kroz elemente liste. Da bi garantovali da čvor lista podržava gornje metode, na primer, možemo da dodamo ovaj metod u interfejs PositionList, kao što je prikazano u fragmentu koda 7.10.

public interface PositionList<E> extends Iterable<E> {

// ...svi drugi metodi ADT liste ...

/** Vraća iterator svih elemenata u listi */

public Iterator<E> iterator();

}

Fragment koda 7.10 Dodavanje iterator metoda u interfejs PositionList

Dajući takvu definiciju PositionList, možemo da koristimo iterator vraćen od strane metoda iterator() da kreiramo string reprezentaciju čvor liste, kao što je prikazano u fragmentu koda 7.11.

/** Vraća tekstualnu reprezentaciju date čvor liste */

public static <E> String toString(PositionList<E> l) {

Iterator<E> it = l.iterator();

String s = "[";

while (it.hasNext()) {

s += it.next(); //implicitno kastovanje sledećeg elementa u

String

if (it.hasNext())

s += ", ";

}

s += "]";

return s;

}

Fragment koda 7.11 Primer Java iteratora korišćenog da konvertuje čvor listu u string

Java for-each petlja

Pošto kretanje u petlji kroz elemente vraćene od strane iteratora je česta operacija, Java obezbeđuje skraćenu notaciju za takve petlje, koje se zovu for-each petlja. Sintaksa za takvu petlju je sledeća:

for (Type name:expression)

loop_statement

gde expression izračunava kolekciju koja implementira interfejs java.lang.Iterable, Type je tip objekta vraćen od strane iteratora za ovu klasu, i name je ime promenljive koja će uzeti vrednosti elemenata od ovog iteratora u loop_statement. Ova notacija je zaista samo skraćena za sledeće:

for (Iterator Type it=expression.iterator(); it.hasNext();) {

Type name = it.next();

loop_statement

}

Na primer ako imamo listu vrednosti (values) Intedžer objekata i values implementira java.lang.Iterable, tada možemo da dodamo sve intedžere u values kao sledeće:

List<Integer> values;

// ... iskaz koji kreira nove vrednosti liste i puni ih sa

Intedžerima

int sum = 0;

for (Integer i : values)

sum += i; // unboxing dozvoljava ovo

Mi ćemo čitati gornju petlju kao, “za svaki Intedžer i u values, uradi telo petlje (u ovom slučaju, dodaj i u sum).” U skladu sa gornjom formom for-each petlje, Java takođe dozvoljava da for-each petlja bude definisana za slučaj kada je izraz (expression) niz tipa Type, koji u ovom slučaju može biti ili osnovnog tipa ili tipa objekta. Na primer, možemo da saberemo intedžere u nizu v, koji smešta prvih deset pozitivnih intedžera, kao sledeće:

int[] v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

int total = 0;

for (int i : v)

total += i;

23. Implementiranje iteratora

Jedan način da implementiramo iterator za kolekciju elemenata je da napravimo “snimak” toga i da iterišemo kroz to. Ovaj pristup će obuhvatiti smeštanje kolekcije u posebne strukture podataka koje podržavaju sekvencijalni pristup svojim elementima. Na primer, možemo da umetnemo sve elemente kolekcije u red za čekanje, u kom slučaju metod hasNext() će odgovarati !isEmpty() i metod next() će odgovarati engueue(). Sa ovim pristupom, metod iterator() traje O(n) vreme za kolekciju veličine n. Pošto je ovo površinsko kopiranje relativno skupo, mi više volimo da u većini slučajeva imamo iteratore koji rade na samoj kolekciji, a ne na kopiji. U implementiranju ovog direktnog pristupa, mi jedino moramo da čuvamo trag gde u kolekciji stoji kursor iteratora. Prema tome, kreiranje novog iteratora na ovaj način jednostavno obuhvata kreiranje iterator objekta koji predstavlja kursor smešten baš pre prvog elementa u kolekciji. Isto tako, izvođenje metoda next() obuhvata vraćanje sledećeg elementa, ako on postoji, i pomeranje kursora upravo pored ove pozicije elementa. Prema tome, u ovom pristupu, kreiranje iteratora traje O(1) vreme, koliko radi svaki od metoda iteratora. Mi pokazujemo implementiranje klase kao što je iterator u fragmentu koda 7.12.

public class ElementIterator<E> implements Iterator<E> {

protected PositionList<E> list; // osnovna lista

protected Position<E> cursor; // sledeća pozicija

/** Kreira element iterator preko date liste */

public ElementIterator(PositionList<E> L) {

list = L;

cursor = (list.isEmpty())? null : list.first();

}

public boolean hasNext() { return (cursor != null); }

public E next() throws NoSuchElementException {

if (cursor == null)

throw new NoSuchElementException("No next element");

E toReturn = cursor.element();

cursor = (cursor == list.last())? null : list.next(cursor);

return toReturn;

}

}

Fragment koda 7.12 Klasa ElementIterator za PositionList

U fragmentu koda 7.13 prikazano je kako ovaj iterator može biti korišćen da implementira metod iterator() u klasi NodePositionList.

/** Vraća iterator svih elemenata u listi */

public Iterator<E> iterator() { return new ElementIterator<E>(this);

}

Fragment koda 7.13 Metod iterator() klase NodePositionList

Iteratori pozicije Za ADT koji podržava pojam pozicije, kao što je ADT lista i ADT sekvenca, mi možemo takođe da obezbedimo sledeći metod:

positions(): Vraća Iterable objekat (kao što je niz lista ili čvor lista) koji sadrži pozicije u kolekciji elemenata

Iterator vraćen ovim metodom dozvoljava nam da izvodimo petlju kroz pozicije liste. Da bi garantovali da čvor lista podržava ovaj metod, mi možemo da ga dodamo u interfejs PositionList, kao što je pokazano u fragmentu koda 7.14.

public interface PositionList<E> extends Iterable<E> {

// ...svi drugi metodi ADT liste ...

/** Vraća ponovljivu kolekciju svih čvorova u listi */

public Iterable<Position<E>> positions();

}

Fragment koda 7.14 Dodavanje iterator metoda u interfejs PositionList

Tada možemo, na primer, da dodamo implementaciju ovog metoda u NodePositionList, kao što je prikazano u fragmentu koda 7.15. Ovaj metod koristi samu klasu NodePositionList da kreira listu koja sadrži pozicije originalne liste kao svoje elemente. Vraćanje ove liste pozicija kao našeg Iterable objekta dozvoljava nam da zatim pozovemo iterator() na ovom objektu da bi dobili iterator pozicija iz originalne liste.

/** Vraća ponovljivu kolekciju svih čvorova u listi */

public Iterable<Position<E>> positions() { // kreira pozicije liste

PositionList<Position<E>> P = new NodePositionList<Position<E>>();

if (!isEmpty()) {

Position<E> p = first();

while (true) {

P.addLast(p); // dodaje poziciju p kao zadnji element liste P

if (p == last())

break;

p = next(p);

}

}

return P; // vraća P kao naš Iterable objekat

}

Fragment koda 7.15 Metod positions() klase NodePositionList

Metod iterator() vraćen ovim i drugim Iterable objektima definiše ograničeni tip iteratora koji dozvoljava jedino jedan prolaz kroz elemente. Međutim, moćniji iteratori takođe mogu da budu definisani koji nam dozvoljavaju da se pomeramo napred i nazad preko određenog uređenja elemenata.

24. Definicije i osobine stabla

Stablo je abstraktni tip podataka koji smešta elemente hijerarhijski. Sa izuzetkom elementa na vrhu, svaki element u stablu ima element roditelj (parent) i nula ili više elemenata dece (children). Stablo je obično vizuelizovano smeštanjem elemenata unutar pravougaonika i crtanjem veza između roditelja i dece sa pravim linijama. To je prikazano na slici 7.7. Mi tipično pozivamo element na vrhu korenom (root) stabla, i to je nacrtano kao najviši element, sa drugim elementima koji su povezani ispod (upravo suprotno od botaničkog drveta).

Slika 7.7 Stablo sa 17 čvorova koje predstavlja organizaciju fiktivne korporacije. Koren smešta

Electronic R’Us. Deca korena smeštaju R&D, Sales, Purchasing i Manufacture. Unutrašnji čvorovi smeštaju Sales, International, Electronic R’Us i Manufacturing.

Formalna definicija stabla Formalno, mi definišemo stablo T kao skup čvorova koji smeštaju elemente tako da čvorovi imaju relacije roditelj-dete, koje zadovoljavaju sledeće karakteristike:

Ako T nije prazno, ono ima specijalni čvor, koji se zove koren T, koji nema roditelja.

Svaki čvor v od T različit od korena ima jedinstveni roditelj čvor w; svaki čvor sa roditeljom w je dete od w.

Primetimo da u skladu sa našom definicijom, stablo može biti prazno, što znači da nema čvorova. Ova konvencija nam dozvoljava da definišemo stablo rekurzivno, takvo da stablo T je ili prazno ili se sastoji od čvora r, koji se zove koren T i (moguće praznog) skupa stabala čiji koreni su deca od r.

Druge čvor veze Dva čvora koji su deca istog roditelja su rođena braća (siblings). Čvor v je spoljašnji ako v nema dece. Čvor v je unutrašnji ako ima jedno ili više dece. Spoljašnji čvorovi su takođe poznati kao rastanci (leaves). Primer 1: U većini operativnih sistema, fajlovi su organizovani hijerarhijski u ugnježdene direktorijume (takođe zvane foldere), koji su predstavljeni korisniku u formi stabla (Slika 7.8). Specifičnije, unutrašnji čvorovi stabla su povezani sa direktorijumima i spoljašnjim čvorovi su povezani sa ispravnim fajlovima. U operativnim sistemima UNIX i Linux, koren stabla se zove “root direktorijum” i predstavljen je preko simbola ”/”.

Čvor u je predak (ancestor) čvora v ako je u = v ili ako je u je predak roditelja v. Suprotno tome, kažemo da je čvor v potomak (descedent) čvora u, ako je u potomak od v. Na primer, na slici 7.8, cs252/ je predak od papers/, i pr3 je potomak od cs016/. Podstablo (subtree) od T ukorenjeno (rooted) u čvoru v, je stablo koje se sastoji od potomaka v u T (uključujući samo v). Na slici 7.8, podstablo ukorenjeno u cs016/ sastoji se od čvorova cs016/, grades, homeworks/, programs/, hw1, hw2, hw3, pr1, pr2 i pr3.

Slika 7.8 Stablo koje predstavlja deo fajl sistema

Ivice i staze u stablima Ivica (edge) stabla T je par čvorova (u,v) takav da je u roditelj v, ili obrnuto. Staza (path) od T je sekvenca čvorova takvih da bilo koja dva uzastopna čvora u sekvenci formiraju ivicu. Na primer, stablo na slici 7.8 sadrži stazu (cs252/, projects/, demos/, market). Primer 2: Relacija nasleđivanja između klasa u Java programu formira stablo. Koren java.lang.Object,

je predak svih drugih klasa. Svaka klasa C, je potomak ovog korena i koren je podstablo klasa koje izdužuju C. Zato u ovom stablu nasleđivanja, postoji staza od C do korena java.lang.Object. Uređena stabla Stablo je uređeno (ordered) ako postoji linearno uređenje definisano za dete svakog čvora; to znači da možemo da identifikujemo decu čvora da budu prvo, drugo, treće i tako dalje. Takvo uređenje je obično vizuelizovano aranžiranjem rođene braće s leva na desno, u skladu sa njihovim uređenjem. Uređena stabla tipično ukazuju na linearni red među rođenom braćom njihovim nabrajanjem u ispravnom redu. Primer 3: Komponente struktuiranog dokumenta, kao što je knjiga, su hijerarhijski organizovane kao stablo čiji unutrašnji delovi su paragrafi, tabele, slike i tako dalje. To je prikazano na slici 7.9.

Slika 7.9 Uređeno stablo povezano sa knjigom

Koren stabla odgovara samoj knjizi. U stvari, mi možemo da razmotrimo dalje proširenje stabla da pokaže paragraf koji se sastoji od rečenica, rečenice se sastoji od reči i reči se sastoje od karaktera. Takvo stablo je primer uređenog stabla, zato što postoji dobro-definisani redosled među decom svakog čvora.

Abstraktni tip podataka stabla

ADT stablo smešta elemente na pozicije, koje su, kao i pozicije u listi definisane relativno u odnosu na susedne pozicije. Pozicije u stablu su njegovi čvorovi, i susedne pozicije zadovoljavaju veze roditelj-dete koje definišu validno stablo. Stoga, mi koristimo pojmove “pozicija” i ”čvor” za stabla. Kao i pozicija u listi, pozicija objekta za stablo podržava metod:

element(): Vraća objekat smešten na ovoj poziciji

Međutim, realna snaga pozicija čvora dolazi od metoda predaka ADT stabla koji vraćaju i prihvataju pozicije, kao što su sledeće:

root(): Vraća koren stabla; greška se pojavljuje ako je stablo prazno.

parent(v): Vraća roditelja od v; greška se pojavljuje ako je v koren.

children(v): Vraća ponovljivu kolekciju koja sadrži decu čvora v

Ako je stablo T uređeno, tada ponovljiva kolekcija children(v), smešta decu od v po redu. Ako je v spoljašnji čvor, tada je children(v) prazno. U skladu sa gornjim osnovnim metodima predaka, mi takođe uključujemo sledeće metode upita:

isInternal(v): Testira da li je čvor v unutrašnji

isEnternal(v): Testira da li je čvor v spoljašnji

isRoot(v): Testira da li je čvor v u korenu

Ovi metodi prave programiranje sa stablima lakšim i čitljivijim, pošto možemo da ih koristimo u kondicionalu if iskaza i while petlji, pre nego da koristimo neintuitivne kondicionale. Postoji određeni broj generičkih metoda, koje stablo verovatno treba da podrži i koji nisu neminovno povezani sa strukturom stabla, koji uključuju sledeće:

size(): Vraća broj čvorova u stablu

isEmpty(): Testira da li stablo ima neke čvorove ili nema

iterator(): Vraća iterator svih elemenata smeštenih u čvorovima stabla

positions(): Vraća ponovljivu kolekciju svih čvorova stabla

replace(v,e): Zamenjuje sa e i vraća element smešten u čvoru v

Bilo koji metod koji uzima poziciju kao argument treba da generiše uslov greške ako je ta pozicija pogrešna. Ovde, mi ne definišemo nikakve specijalizovane metode ažuriranja za stabla. Umesto toga, mi preferiramo da opišemo različite metode ažuriranja stabla u vezi sa specifičnim aplikacijama stabla.

Implementiranje stabla

Java interfejs prikazan u fragmentu koda 7.16 predstavlja ADT stablo. Uslovima greške se manipuliše na sledeći način: Svaki metod koji može uzeti poziciju kao argument, može izazvati InvalidPositionException, da ukaže da je pozicija pogrešna. Metod parent izaziva BoundaryViolationException ako je pozvan na koren. Metod root izaziva EmptyThreeException ako je pozvan na prazno stablo.

/**

* Interfejs za stablo gde čvorovi imaju proizvoljan broj dece.

*/

public interface Tree<E> {

/** Vraća broj čvorova u stablu. */

public int size();

/** Vraća da li je stablo prazno. */

public boolean isEmpty();

/** Vraća iterator elemenata smeštenih u stablu. */

public Iterator<E> iterator();

/** Vraća ponovljivu kolekciju čvorova. */

public Iterable<Position<E>> positions();

/** Zamenjuje element smešten na datom čvoru. */

public E replace(Position<E> v, E e)

throws InvalidPositionException;

/** Vraća koren stabla. */

public Position<E> root() throws EmptyTreeException;

/** Vraća roditelja datog čvora. */

public Position<E> parent(Position<E> v)

throws InvalidPositionException, BoundaryViolationException;

/** Vraća ponovljivu kolekciju dece datog čvora. */

public Iterable<Position<E>> children(Position<E> v)

throws InvalidPositionException;

/** Vraća da li je dati čvor unutrašnji. */

public boolean isInternal(Position<E> v)

throws InvalidPositionException;

/** Vraća da li je dati čvor spoljašnji. */

public boolean isExternal(Position<E> v)

throws InvalidPositionException;

/** Vraća da li je dati čvor koren stabla. */

public boolean isRoot(Position<E> v)

throws InvalidPositionException;

}

Fragment koda 7.16 Java interfejs Tree koji predstavlja ADT stablo. Dodatni metodi ažuriranja mogu biti dodati, zavisno od aplikacije.

Povezane strukture za stabla Prirodan način za realizovanje stabla T je korišćenje povezane strukture, gde mi predstavljamo svaki

čvor v od stabla T preko pozicije objekta (slika 7.10a) sa sledećim poljima: Referenca na element smešten na v, link na roditelja v, i neka vrsta kolekcije (na primer, niz lista) da smesti linkove na decu v. Ako je v koren stabla T, tada je polje roditelj od v jednako nuli. Takođe, mi smeštamo reference na koren stabla T i broj čvorova od T u unutrašnje promenljive. Ova struktura je šematski prikazana na slici 7.10b.

Slika 7.10 Povezana struktura za generalno stablo

pozicija objekta povezana sa čvorom

deo strukture podataka povezane sa čvorom i njegovom decom

Sledeća tabela sažima performanse implementacije stabla korišćenjem povezane strukture. Primetimo da korišćenjem kolekcije da smestimo decu od svakog čvora v, možemo da implementiramo children(v) jednostavno vraćanjem reference na ovu kolekciju. Sa cv je označen broj dece čvora v. Korišćenost prostora je O(n).

Operacija Vreme

size, isEmpty 0(1)

iterator, positions 0(n)

replace 0(1)

root, parent 0(1)

children(v) 0(cv)

islnternal, isExternal, isRoot 0(1)

25.Algoritmi za kretanje po stablu

Ovde će biti prikazani algoritmi za izvođenje izračunavanja kretanja po stablu sa pristupom stablu preko ADT metoda stabla.

Dubina i visina

Neka v bude čvor stabla T. Dubina v je broj predaka od v, isključujući samo v. Primetimo da ova deficija ukazuje da dubina korena T je 0. Dubina čvora v može biti takođe rekurzivno definisana kao sledeće:

Ako je v koren, tada dubina od v je 0

U suprotnom, dubina v je jedan plus dubina roditelja od v

Bazirano na ovoj definiciji mi predstavljamo jednostavan, rekurzivni algoritam, depth u fragmentu koda 7.17, za izračunavanje dubine čvora v u stablu T. Ovaj metod zove sebe rekurzivno na roditelja od v, i dodaje 1 vraćenoj vrednosti.

Algorithm depth(T,v): if v je koren od T then return 0 else return 1 + depth(T,w), gde je w roditelj od v u stablu T

Fragment koda 7.17 Algoritam za izračunavanje dubine čvora v u stablu T

Jednostavna Java implementacija algoritma je prikazana u fragmentu koda 7.12.

public static <E> int depth (Tree<E> T, Position<E> v) {

if (T.isRoot(v))

return 0;

else

return 1 + depth(T, T.parent(v));

}

Fragment koda 7.17 Metod depth pisan u Javi

Vreme rada algoritma depth(T,v) je O(dv) gde dv označava dubinu čvora v u stablu T, zato što algoritam izvodi konstantno-vreme rekurzivni korak za svaki predak od v. Prema tome, algoritam depth(T,v) radi u O(n) vremenu najgoreg-slučaja, gde je n ukupni broj čvorova od T, pošto čvor od T može imati dubinu n-1 u najgorem slučaju.

Visina Visina čvora v u stablu T je takođe definisana rekurzivno:

Ako je v spoljašnji čvor, tada visina v je 0

U suprotnom, visina v je jedan plus maksimalna visina deteta od v

Visina stabla T koje nije prazno je visina korena od T. Na primer stablo na slici 7.7 ima visinu 4. U skladu sa tim, visina takođe može biti posmatrana kao sledeće.

Predlog 1: Visina stabla T koje nije prazno jednaka je maksimalnoj dubini spoljašnjeg čvora od T.

U fragmentu koda 7.18 prikazan je algoritam height1 za izračunavanje visine stabla T koje nije prazno baziran na predlogu iznad.

Algorithm height1(T):

h 0 for svaki vrh v u T do if v je spoljašnji čvor od T then

h max(h,depth(T,v)) return h

Fragment koda 7.18 Algoritam height1 za izračunavanje visine stabla T koje nije prazno. Primetimo da ovaj algoritam zove algoritam depth prikazan u fragmentu koda 7.17

Implementacija algoritma height1 u Javi prikazana je u fragmentu koda 7.19.

public static <E> int height1 (Tree<E> T) {

int h = 0;

for (Position<E> v : T.positions()) {

if (T.isExternal(v))

h = Math.max(h, depth(T, v));

}

return h;

}

Fragment koda 7.19 Metod height1 napisan u Javi. Primetimo korišćenje metoda max klase java.lang.Math

Nažalost, algoritam height1 nije veoma efikasan. Pošto height1 poziva algoritam depth(v) za

svaki spoljašnji čvor v od T, vreme rada height1 je dato kao O(n + v(1+dv)), gde je n broj čvorova od

T, dv je dubina čvora v, i E je skup spoljašnjih čvorova od T. U najgorem slučaju, suma v(1+dv) je proporcionalna n2. Prema tome, algoritam height1 radi u O(n2) vremenu.

Algoritam height2 prikazan u fragmentu koda 7.20 izračunava visinu stabla T na mnogo efikasniji način korišćenjem rekurzivne definicije visine.

Algorithm height2(T,v): if v je spoljašnji čvor od T then return 0 else

h 0 for svako dete w od v u T do

h max(h,height2(T,w)) return 1+h

Fragment koda 7.20 Algoritam height2 za izračunavanje visine podstabla od stabla T koje je ukorenjeno u čvoru v

Implementacija algoritma height2 u Javi prikazana je u fragmentu koda 7.21.

public static <E> int height2 (Tree<E> T, Position<E> v) {

if (T.isExternal(v)) return 0;

int h = 0;

for (Position<E> w : T.children(v))

h = Math.max(h, height2(T, w));

return 1 + h;

}

Fragment koda 7.21 Metod height2 napisan u Javi

Algoritam height2 je mnogo efikasniji od algoritma height1 (prikazanog u fragmentu koda 7.18). Algoritam je rekurzivni i ako je početno pozvan na korenu od T, on će najzad biti pozvan na svakom čvoru od T. Prema tome, možemo da odredimo vreme rada ovog metoda sumiranjem preko svih čvorova, količine vremena provedene u svakom čvoru (u nerekurzivnom delu). Procesiranje svakog čvora u children(v) traje O(cv) vreme, gde cv označava broj dece čvora v. Takođe, while petlja ima cv iteracija i svaka iteracija petlje traje O(1) vreme plus vreme za rekurzivni poziv deteta od v. Prema tome, algoritam height2 traje O(1+cv) vreme u svakom čvoru v, i njegovo vreme rada je

O(v(1+cv)). Da bi kompletirali analizu, mi koristimo sledeću osobinu.

Predlog 2: Neka T bude stablo sa n čvorova, i neka cv označava broj dece čvora v od T. Tada,

sumiranje preko vrhova u T, je vcv = n - 1.

Opravdanost: Svaki čvor od T, sa izuzetkom korena, je dete drugog čvora, i prema tome doprinosi jednoj jedinici gornje sume.

Po predlogu 2, vreme rada algoritma height 2, kada je pozvan u korenu od T je O(n), gde je n broj čvorova od T.

Preorder kretanje

Kretanje po stablu (traversal) je sistemski način pristupa ili “poseta“, svih čvorova od T. Ovde će biti prikazana osnovna šema kretanja za stabla, koja se zove preorder kretanje. U preorder kretanju po stablu T, prvo je posećen koren od T i zatim su obiđena rekurzivno podstabla ukorenjena u njegovoj deci. Ako je stablo uređeno, tada su podstabla obiđena u skladu sa redom dece. Specifična akcija povezana sa “posetom” čvora v zavisi od primene ovogo kretanja, i može da obuhvati bilo šta od inkrementiranja brojača do izvođenja nekih kompleksnih izračunavanja za v. Pseudo-kod za preorder kretanje postabla ukorenjenog u čvoru v prikazan je u fragmentu koda 7.22.

Algorithm preorder(T,v): izvodi “posetu“ akcije za čvor v for svako dete w od v u T do preorder(T,w) {rekurzivno kretanje podstabla ukorenjenog u w}

Fragment koda 7.22 Algoritam preorder za izvođenje preorder kretanja postabla ukorenjenog u čvoru v

Preorder algoritam kretanja je koristan za stvaranje linearnog uređenja čvorova stabla gde roditelji moraju uvek doći pre njihove dece u poretku. Takva uređenja imaju nekoliko različitih primena. Mi istražujemo jednostavnu instancu takve aplikacije u sledećem primeru.

Primer 1: Preorder kretanje stabla povezanog sa dokumentom (kao u primeru stabla koji smo ranije videli, a koje predstavlja deo fajl sistema), ispituje čitav dokument sekvencijalno, od početka do kraja. Ako su spoljašnji čvorovi uklonjeni pre kretanja, tada kretanje ispituje tablicu sadržaja dokumenta. To

je prikazano na slici 7.11.

Slika 7.11 Preorder kretanje uređenog stabla, gde su deca svakog čvora uređena s leva na desno

Preorder kretanje je takođe efikasan način da se pristupi svim čvorovima stabla. Da bi opravdali ovo, razmotrićemo vreme rada preorder kretanja stabla T sa n čvorova, pod pretpostavkom da poseta čvora traje O(1) vreme. Svaki čvor v, nerekurzivnog dela algoritma preorder kretanja zahteva vreme O(1+Cv), gde je Cv broj dece od v. Prema tome, globalno vreme rada preorder kretanja T je O(n). Algoritam toStringPreorder(T,v), implementiran u Javi u fragmentu koda 7.23 izvodi preorder štampanje podstabla čvora v od T, to znači da on izvodi preorder kretanje podstabla ukorenjenog u v i štampa element smešten u čvoru kada je čvor posećen. Pozivanjem toga, za uređeno stablo T, metod T.children(v) vraća ponovljivu kolekciju koja pristupa deci v po redu.

public static <E> String toStringPreorder(Tree<E> T, Position<E> v) {

String s = v.element().toString(); // glavna "poseta" akcija

for (Position<E> w : T.children(v))

s += ", " + toStringPreorder(T, w);

return s;

}

Fragment koda 7.23 Metod toStringPreorder(T,v) koji izvodi preorder štampanje elemenata u podstablu čvora v of T

Postoji interesantna primena algoritma preorder kretanja koja proizvodi string reprezentaciju čitavog stabla. Pretpostavimo ponovo da za svaki element e smešten u stablu T, pozivanje e.toString() vraća string povezan sa e. Umetnuta string reprezentacija P(T) stabla T je rekurzivno definisana kao sledeće. Ako se T sastoji od jednog čvora v, tada

P(T) = v.element().toString()

U suprotnom,

P(T) = v.element().toString() + ( + P(T1) + , +...+ , + P(Tk) + ),

gde je v koren od T i T1, T2,..., Tk su podstabla ukorenjena u deci od v, koja su data po redu ako je T uređeno stablo.

Primetimo da je gornja definicija P(T) rekurzivna. Takođe, mi ovde koristimo + da označimo povezivanje stringa. Umetnuta reprezentacija stabla sa slike 7.7 prikazana je na slici 7.12.

Slika 7.12 Umetnuta reprezentacija stabla sa slike 7.7. Uvlačenje, prekidi linija i razmaci su dodati

zbog jasnoće

Postorder kretanje

Drugi značajni algoritam kretanja je postorder kretanje. Ovaj algoritam može biti posmatran kao suprotan preorder kretanju, zato što rekurzivno prvo kreće od podstabla ukorenjenog u deci korena, i zatim posećuje koren. Međutim, on je sličan sa preorder kretanjem, po tome da ga koristimo da rešimo određeni problem specijalizovanjem akcija povezanih sa “posetom“ čvora v. Ipak, kao i za preorder

kretanje, ako je stablo uređeno, mi pravimo rekurzivne pozive za decu čvora v u skladu sa njihovim specifičnim redom. Pseudo-kod za postorder kretanje prikazan je u fragmentu koda 7.24.

Algorithm postorder(T,v): for svako dete w od v u T do postorder(T,w) {rekurzivno kretanje podstabla ukorenjenog u w}

izvodi “posetu“ akcije za čvor v

Fragment koda 7.24 Algoritam postorder za izvođenje postorder kretanja postabla ukorenjenog u čvoru v

Ime postorder kretanje dolazi od činjenice da će ovaj metod kretanja posetiti čvor v nakon što je posetio sve druge čvorove podstabla ukorenjenog u v (slika 7.13).

Slika 7.13 Postorder kretanje uređenog stabla sa slike 7.11

Ukupno vreme provedeno u nerekurzivnim delovima algoritma je proporcionalno vremenu provedenom u poseti dece svakog čvora u stablu. Prema tome, postorder kretanje stabla T sa n čvorova traje O(n) vreme, pretpostavljajući da poseta svakog čvora traje O(1) vreme. To znači, da postorder kretanje radi u linearnom vremenu. Kao primer postorder kretanja, u fragmentu koda 7.25 prikazujemo Java metod toStringPostorder koji izvodi postorder kretanje stabla T. Ovaj metod štampa elemente smeštene u čvoru kada je posećen. Metod implicitno poziva toString na elemente, kada su uključeni u operacije povezivanja stringa.

public static <E> String toStringPostorder(Tree<E> T, Position<E> v)

{

String s = "";

for (Position<E> w : T.children(v))

s += toStringPostorder(T, w) + " ";

s += v.element(); // main visit action

return s;

}

Fragment koda 7.25 Metod toStringPostorder(T, v) koji izvodi postorder štampanje elemenata u podstablu čvora v od T

Metod postorder kretanja je koristan za rešavanje problema gde želimo da izračunamo neku osobinu za svaki čvor v u stablu, ali izračunavanje takve osobine za v zahteva da imamo već izračunatu istu osobinu za decu v. Takva aplikacija je ilustrovana u sledećem primeru. Primer 2: Razmatraćemo stablo T fajl sistema, gde spoljašnji čvorovi predstavljaju fajlove i unutrašnji čvorovi predstavljaju direktorijume. Recimo da želimo da izračunamo prostor na disku korišćen od strane direktorijuma, koji je rekurzivno dat kao suma od:

Veličine samog direktorijuma

Veličine fajlova u direktorijumu

Prostora korišćenog od strane dece direktorijuma

Ovaj proračun može biti urađen sa postorder kretanjem stabla T (slika 7.14). Nakon što su obiđena podstabla unutrašnjeg čvora v, mi izračunavamo prostor korišćen od v dodavanjem veličine samog direktorijuma v i fajlova sadržanih u v, prostoru korišćenom od strane svakog unutrašnjeg deteta od v, koji je izračunat rekurzivnim postorder kretanjem dece od v.

Slika 7.14 Stablo koje predstavlja fajl sistem, prikazujući imena i veličinu povezanih

fajlova/direktorijuma unutar svakog čvora, i prostor diska korišćen od strane povezanih direktorijuma iznad svakog unutrašnjeg čvora

Druge vrste kretanja Mada su preorder i postorder kretanja česti način posete čvorova stabla, mi takođe možemo da zamislimo druga kretanja. Na primer, mi možemo da obiđemo stablo tako da posetimo sve čvorove na dubini d pre nego što posetimo čvorove na dubini d+1. Uzastopno numerisanje čvorova stabla T kako smo ih posetili u ovom kretanju zove se numerisanje nivoa (level numbering) čvorova od T.

Binarna stabla

Binarno stablo je uređeno stablo sa sledećim osobinama:

Svaki čvor ima najviše dvoje dece

Svako čvor dete je označeno da bude ili levo dete ili desno dete

Levo dete prethodi desnom detetu u uređivanju dece čvora

Podstablo ukorenjeno na levom ili desnom detetu unutrašnjeg čvora v zove se levo podstablo ili desno podstablo, respektivno od v. Binarno stablo je pravilno (proper) ako svaki čvor ima ili nulu ili dva deteta. Neki ljudi takođe se odnose prema takvim stablima kao punim binarnim stablima. Tako, u pravilnom binarnom stablu, svaki unutrašnji čvor ima tačno dva deteta. Binarno stablo koje nije pravilno je nepravilno (improper). Primer 3: Značajna klasa binarnih stabla pojavljuje se u kontekstu gde mi želimo da predstavimo broj

različitih rezultata koji mogu da budu rezultat odgovora na serije da-ili-ne pitanja. Svaki unutrašnji čvor

je povezan sa pitanjem. Startujući od korena, mi idemo na levo ili desno dete trenutnog čvora, zavisno

da li je odgovor na pitanje bio “Da“ ili “Ne“. Sa svakom odlukom, mi pratimo ivicu od roditelja do deteta,

eventualno crtajući stazu u stablu od korena do spoljašnjeg čvora. Takva binarna stabla su poznata

kao stabla odluke (decision trees), zato što svaki unutrašnji čvor v u takvom stablu predstavlja

odluku šta uraditi, ako je na pitanja povezana sa precima v odgovoreno na način koji vodi ka v. Stablo

odluke je pravilno binarno stablo.

26. Osobine binarnog stabla

Binarno stablo ima nekoliko interesantnih osobina koje manipulišu sa vezama između njegovih visina i broja čvorova. Mi označavamo skup svih čvorova stabla T na istoj dubini d kao nivo d od T. U binarnom stablu, nivo 0 ima najviše jedan čvor (koren), nivo 1 ima najviše dva čvora (deca korena), nivo 2 ima najviše 4 čvora, i tako dalje (slika 7.17). U osnovi, nivo d ima najviše 2d čvorova.

Slika 7.17 Maksimalni broj čvorova u nivoima binarnog stabla

Možemo da vidimo da maksimalni boj čvorova na nivoima binarnog stabla raste eksponencijalno kako mi idemo naniže kroz stablo. Iz ovog jednostavnog opažanja, mi možemo da izvedemo sledeće osobine povezujući visinu binarnog stabla T sa njegovim brojem čvorova.

Predlog 1: Neka T nije prazno binarno stablo, i neka n, nE, nI i h označavaju broj čvorova, broj spoljašnjih čvorova, broj unutrašnjih čvorova i visinu stabla T, respektivno. Tada T ima sledeće osobine:

1. h + 1 n 2h+1 - 1

2. 1 nE 2h

3. h nI 2h - 1

4. log(n + 1) - 1 h n – 1

Takođe, ako je T pravilno, tada T ima sledeće osobine:

1. 2h + 1 n 2h+1 - 1

2. h + nE 2h

3. h nI 2h - 1

4. log(n + 1) - 1 h (n – 1) / 2

Povezivanje unutrašnjih čvorova sa spoljašnjim čvorovima u pravilnom binarnom stablu U skladu sa gornjim osobinama binarnog stabla, mi takođe imamo sledeće veze između unutrašnjih i spoljašnjih čvorova u pravilnom binarnom stablu.

Predlog 2: Ako T nije prazno binarno stablo, sa nE spoljašnjim čvorovima i nI unutrašnjim čvorovima, mi imamo da je nE = nI + 1

Opravdanost: Mi opravdamo ovaj predlog uklanjanjem čvorova iz T i njihovim deljenjem u dve “gomile“, gomilu unutrašnjeg čvora i gomilu spoljašnjeg čvora, dok T ne postane prazno. Gomile su početno prazne. Na kraju, gomila spoljašnjeg čvora će imati jedan čvor više nego gomila unutrašnjeg čvora. Mi razmatramo dva slučaja:

Slučaj 1: Ako T ima samo jedan čvor v, mi uklanjamo v i stavljamo ga na gomilu spoljašnjeg čvora. Prema tome, gomila spoljašnjeg čvora ima jedan čvor i gomila unutrašnjeg čvora je prazna.

Slučaj 2: Inače (ako T ima više od jednog čvora), mi uklanjamo iz T (proizvoljno) spoljašnji čvor w i njegovog roditelja v, koji je unutrašnji čvor. Mi stavljamo w na gomilu spoljašnjeg čvora i v u gomilu unutrašnjeg čvora. Ako v ima roditelja u, tada mi povezujemo u sa prethodnim bratom z od w, kao što je prikazano na slici 7.18. Ova operacija, uklanja jedan unutrašnji čvor i jedan spoljašnji čvor, i ostavlja stablo da bude pravilno binarno stablo. Ponavljanjem ove operacije, mi smo konačno ostali sa završnim stablom koje se sastoji od jednog čvora. Primetimo da je isti broj unutrašnjih i spoljašnjih čvorova uklonjen i smešten na njihove respektivne gomile preko sekvence operacije koje vode ka ovom završnom stablu. Sada, mi uklanjamo čvor finalnog stabla i smeštamo ga u gomilu spoljašnjeg čvora. Prema tome, gomila spoljašnjeg čvora ima jedan čvor više nego gomila unutrašnjeg čvora.

Slika 7.18 Operacija koja uklanja spoljašnji čvor i njegov čvor roditelj

Primetimo da gornje relacije ne stoje, u osnovi, za nepravilna binarna stabla i nebinarna stabla, mada postoje druge interesantne veze.

27. Jednostavna list-bazirana implementacija mape

Jednostavan način za implementiranje mape je smeštanje n slogova u listu S, implementirano kao dvostruko povezana lista. Izvođenje osnovnih metoda get(k), put(k,v) i remove(k), obuhvata jednostavno skaniranje liste S nadole tražeći slog sa ključem k. Pseudo-kod za izvođenje ovih metoda u mapi M prikazan je u fragmentu koda 7.19.

Algorithm get(k): Input: Ključ k Output: Vrednost za ključ k u mapi ili null ako ne postoji ključ k u M for svaka pozicija p u S.positions() do if p.element().getKey() = k then

return p.element().getValue() {ne postoji slog sa ključem jednakim k} return null

Algorithm put(k,v): Input: Par ključ-vrednost (k,v) Output: Stara vrednost povezana sa ključem k u M, ili null ako je k novo for svaka pozicija p u S.positions() do if p.element().getKey() = k then

t p.element().getValue() B.set(p,(k,v)) return t {vraća staru vrednost}

S.addLast((k,v))

n n + 1 {inkrementira promenljivu smeštajući broj slogova} return null {nije postojao prethodni slog sa ključem jednakim k}

Algorithm remove(k): Input: Ključ k Output: Uklonjena vrednost za ključ k u M, ili null ako ne postoji ključ k u M for svaka pozicija p u S.positions() do if p.element().getKey() = k then

t p.element().getValue() S.remove(p)

n n - 1 {dekrementira promenljivu smeštajući broj slogova } return t {vraća uklonjenu vrednost} return null {ne postoji slog sa ključem jednakim k}

Fragment koda 7.19 Algoritmi za osnovne metode mape sa listom S

List-bazirana implementacije mape je jednostavna, ali je jedino efikasna za vrlo male mape. Svaki od osnovnih metoda traje O(n) vreme na mapi sa n slogova, zato što svaki metod obuhvata pretragu kroz celu listu u najgorem slučaju.

28. Heš tabele

Jedan od najefikasnijih načina da se implementira mapa je korišćenjem heš tabele. U osnovi, heš

tabela se sastoji od dve glavne komponente, kanta niza (bucket array) i heš funkcije (hesh function).

Kanta niz

Kanta niz za heš tabelu je niz A veličine N, gde se o svakoj ćeliji niza A razmišlja kao o ”kanti” (to je kolekcija parova ključ-vrednost) i intedžer N definiše kapacitet niza. Ako su ključevi intedžeri koji su distribuirani u opsegu od [0, N-1], ovaj kanta niz je sve što je neophodno. Slog e sa ključem k je jednostavno umetnut u kantu A[k]. To je prikazano na slici 7.22. Da bi sačuvali prostor, prazna kanta može biti zamenjena sa objektom null.

Slika 7.22 Kanta niz veličine 11 za slogove (1,D), (3,C), (3,Z), (6,A), (6,C) I (7,Q)

Ako su naši ključevi jedinstveni intedžeri u opsegu [0, N-1], tada svaka kanta drži najviše jedan slog. Prema tome, pretraga, umetanje i uklanjanje u kanta niz traje O(1) vreme. Ovo zvuči kao veliko dostignuće, ali ima dva nedostatka. Prvo, korišćeni prostor je proporcionalan sa N. Prema tome, ako je N mnogo veće od broja slogova n stvarno prisutnih u mapi, mi imamo gubitak prostora. Drugi nedostatak je da je zahtevano da ključevi budu intedžeri u opsegu [0, N-1], što često nije slučaj.

Heš funkcije

Drugi deo strukture heš tabele je funkcija h, koja se zove heš funkcija, koja mapira svaki ključ k u našu mapu u intedžer u opsegu [0, N-1], gde je N kapacitet kanta niza za ovu tabelu. Opremljeni sa takvom heš funkcijom h, mi možemo da primenimo kanta niz na proizvoljne ključeve. Glavna ideja ovoga pristupa je da se koristi vrednost heš funkcija h(k), kao indeks u našem kanta nizu A, umesto ključa k (koji je najverovatnije nepogodan za korišćenje indeksa kanta niza). To znači, da mi smeštamo slog (k, v) u kantu A[h(k)], Naravno, ako postoje dva ili više ključeva sa istom heš vrednošću, tada će dva različita sloga biti mapirana u istu kantu u A. U ovom slučaju, mi kažemo da se pojavila kolizija. Jasno, ako svaka kanta od A može da smesti jedino jedan slog, tada mi ne možemo da povežemo više od jednog sloga sa jednom kantom, što je problem u slučaju kolizije. Da bi bili sigurni, postoje načini manipulisanja sa kolizijom, ali najbolja strategija je pokušati da se one izbegnu na prvom mestu. Mi kažemo da je heš funkcija “dobra“ ako mapira ključeve u našu mapu, tako da minimizira koliziju što je moguće više. Iz praktičnih razloga, mi takođe želimo da heš funkciju bude brza i laka za proračun. Sledeći konvenciju u Javi, mi posmatramo izračunavanje heš funkcije h(k), pošto se sastoji od dve akcije – mapiranja ključa k u intedžer, koji se zove heš kod, i mapiranja heš koda u intedžer unutar opsega indeksa [0, N-1] kanta niza, koji se zove funkcija kompresije. To je prikazano na slici 7.23.

Slika 7.23 Dva dela heš funkcije: heš kod i funkcija kompresije

29. Merge-Sort algoritam

Ovde će biti prezentovana tehnika sortiranja, nazvana merge-sort, koja će biti opisana na jednostavan i kompaktan način korišćenjem rekurzije.

Podeli-i-osvoji (Divide-and-conquer)

Merge-sort je baziran na šablonima projektovanja algoritma nazvanim podeli-i-osvoji. Podeli-i-osvoji šablon sastoji se od sledeća tri koraka:

Podeli: Ako je veličina ulaza manja od određenog praga (recimo, jedan ili dva elementa), rešiti problem direktno korišćenjem jasnog metoda i vraćanjem tako dobijenog rešenja. U suprotnom, podeliti ulazne podatke u dva ili više razdvojenih podskupova.

Vrati: Rekurzivno rešavanje podproblema povezanih sa podskupovima.

Osvoji: Uzimanje rešenja podproblema i njihovo “spajanje” u rešenje originalnog problema.

Korišćenje podeli-i-osvoji za sortiranje U problemima sortiranja mi imamo sekvencu n objekata, smeštenih u povezanu listu ili niz, zajedno sa nekim komparatorom koji definiše ukupno uređenje ovih objekata, i od nas je traženo da kreiramo uređenu prezentaciju ovih objekata. Da bi dozvolili sortiranje takve prezentacije, opisaćemo naš algoritam za sortiranje na visokom nivou za sekvence i objasnićemo detalje neophodne da to implementiramo za povezane liste i nizove. Da bi sortirali sekvencu S sa n elemenata korišćenjem tri podeli-i-osvoji koraka, algoritam merge-sort radi sledeće:

Podeli: Ako S ima nula ili jedan element, vraća S odmah; ono je već sortirano. U suprotnom (S ima najmanje dva elementa), uklanjamo sve elemente iz S i stavljamo ih u dve sekvence,

S1 i S2, od kojih svaka sadrži polovinu elemenata iz S; to znači, da S1 sadrži prvih n/2

elemenata iz S, i S2 sadrži ostalih n/2 elemenata.

Vrati: Rekurzivno sortira sekvence S1 i S2

Osvoji: Stavlja nazad elemente u S spajanjem sortiranih sekvenci S1 i S2 u sortiranu sekvencu.

U obeležavanju koraka podeli, mi smo koristili notaciju x koja ukazuje na plafon (ceiling) x, to znači

da je najmanji intedžer m, takav da je x m. Slično, notacija x ukazuje na pod (floor) od x, to znači,

da je najveći intedžer k, takav da je k x.

Slika 8.12 Merge-sort stablo T za izvršavanje merge-sort algoritma na sekvenci sa 8 elemenata

a) ulazne sekvence procesirane u svakom čvoru b) izlazne sekvence generisane u svakom čvoru

Slika 8.13 Vizuelizacija izvršavanja merge-sort. Svaki čvor stabla predstavlja rekurzivni poziv merge-sort. Čvorovi nacrtani sa isprekidanim linijama predstavljaju pozive koji nisu još napravljeni. Čvorovi nacrtani sa debelim linijama predstavljaju trenutne pozive. Prazni čvorovi nacrtani sa tankim linijama predstavljaju kompletirane pozive. Preostali čvorovi (nacrtani sa tankim linijama i koje nisu prazne)

predstavljaju pozive koji čekaju da se vrati prozvano dete.

Možemo da vizuelizujemo izvršavanje merge-sort algoritma preko binarnog stabla T, koje se zove merge-sort stablo. Svaki čvor stabla T predstavlja rekurzivni poziv merge-sort algoritma. Mi povezujemo sa svakim čvorom v od T sekvencu S koja je procesirana pozivanjem povezanim sa v. Deca čvora v su povezana sa rekurzivnim pozivima koji procesiraju podsekvence S1 i S2 od S. Spoljašnji čvorovi stabla T su povezani sa pojedinačnim elementima od S, koji odgovaraju instancama algoritma koje prave ne-rekurzivne pozive. Slika 8.12 sažima izvršavanje merge-sort algoritma prikazivanjem ulaznih i izlaznih sekvenci procesiranim u svakom čvoru merge-sort stabla. Korak-po-korak evolucija merge-sort stabla prikazana je na slikama od 8.13 do 8.15. Ova vizuelizacija algoritma u pojmovima merge-sort stabla pomaže nam u analiziranju vremena rada merge-sort algoritma. Posebno, pošto veličina ulazne sekvence približno deli na pola svaki rekurzivni poziv merge-sort, visina merge-sort stabla je oko log n (podsetimo da je baza 2 od log izostavljena).

Slika 8.14 Vizuelizacija izvršavanja merge-sort

Predlog 1: Merge-sort stablo povezano sa izvršavanjem merge-sort na sekvenci veličine n ima visinu

log n.

Slika 8.15 Vizuelizacija izvršavanja merge-sort. Nekoliko pozivanja su izostavljena između (l) i (m) i između (m) i (n). Zabeležimo da je korak osvojiti izveden u koraku (p).

Spajanje nizova i lista

Da bi spojili dve sortirane sekvence, korisno je znati da li su implementirane kao nizovi ili liste. Prema tome, mi dajemo detaljni pseudo-kod koji opisuje kako spojiti dve sortirane sekvence predstavljene kao nizovi i kao povezane liste. Spajanje dva sortirana niza Mi počinjemo sa implementacijom niza, koji je prikazan u fragmentu koda 8.4. Na slici 8.16 ilustrovan je korak u spajanju dva sortirana niza.

Algorithm merge(S1, S2, S): Input: Sortirane sekvence S1 i S2 i prazna sekvenca S, od kojih su sve implementirane kao nizovi Output: Sortirana sekvenca S koja sadrži elemente iz S1 i S2

i j 0 while i < S1.size() and j < S2.size() do

if S1.get(i) S2.get(j) then S.addLast(S1.get(i)) {kopira i-ti element iz S1 na kraj od S}

i i + 1 else S.addLast(S2.get(j)) { kopira j-ti element iz S2 na kraj od S }

j j + 1 while i < S1.size() do { kopira preostale elemente iz S1 u S } S.addLast(S1.get(i))

i i + 1 while j < S2.size() do { kopira preostale elemente iz S2 u S } S.addLast(S2.get(j))

j j + 1

Fragment koda 8.4 Algoritam za spajanje dve sortirane niz-bazirane sekvence

Slika 8.16 Korak u spajanju dva sortirana niza a) nizovi pre koraka kopiranja b) nizovi posle koraka kopiranja

Spajanje dve sortirane liste U fragmentu koda 8.5 data je list-bazirana verzija algoritma merge, za spajanje dve sortirane sekvence, S1 i S2, implementirane kao povezane liste.

Slika 8.17 Primer izvršavanja algoritma merge prikazanog u fragmentu koda 8.5

Algorithm merge(S1, S2, S): Input: Sortirane sekvence S1 i S2 i prazna sekvenca S, implementirane kao povezane liste Output: Sortirana sekvenca S koja sadrži elemente iz S1 i S2

while S1 nije prazna and S2 nije prazna do

if S1.first().element() S2.first().element() then { pomeranje prvog elementa iz S1 na kraj od S } S.addLast(S1.remove(S1.first())) else { pomeranje prvog elementa iz S2 na kraj od S } S.addLast(S2.remove(S2.first())) { pomeranje preostalih elemenata iz S1 u S } while S1 nije prazna do S.addLast(S1.remove(S1.first())) { pomeranje preostalih elemenata iz S2 u S } while S2 nije prazna do S.addLast(S2.remove(S2.first()))

Fragment koda 8.5 Algoritam merge za spajanje dve sortirane sekvence implementirane kao povezane liste

Glavna ideja je iterativno uklanjanje najmanjeg elementa sa početka jedne od dve liste i njegovo dodavanje na kraj izlazne sekvence S, dok je jedna od dve ulazne liste prazna, u kojoj tački mi

kopiramo ostatak druge liste u S. Primer izvršavanja ove verzije algoritma merge prikazan je na slici 8.17.

Vreme rada za spajanje Mi analiziramo vreme rada algoritma merge pravljenjem nekih jednostavnih zapažanja. Neka n1 i n2 budu brojevi elemenata S1 i S2, respektivno. Algoritam merge ima tri while petlje. Nezavisno da li analiziramo niz-baziranu ili list-baziranu verziju, operacije izvedene unutar svake petlje traju O(1) vreme svaka. Ključno zapažanje je da tokom svake iteracije jedne od petlji, jedan element je iskopiran ili pomeren iz S1 ili S2 u S (i taj element nije više razmatran). Pošto nisu izvedena umetanja u S1 ili S2, ovo zapažanje ukazuje da je ukupni broj iteracija tri petlje n1 + n2. Prema tome, vreme rada algoritma merge je O(n1+n2).

8.2.3 Vreme rada merge-sort

Sada kada imamo date detalje merge-sort algoritma, u obe njegove verzije, niz-baziranoj i lista-baziranoj, i imamo analizirano vreme rada ključnog merge algoritma korišćenog u koraku osvojiti, možemo da analiziramo vreme rada kompletnog merge-sort algoritma, predpostavljajući da je data ulazna sekvenca n elemenata. Zbog jednostavnosti, mi ograničavamo našu pažnju na slučaj gde je n stepena 2. Kao što smo uradili u analizi merge algoritma, mi pretpostavljamo da su ulazna sekvenca S i pomoćne sekvence S1 i S2, svaka kreirana rekurzivnim pozivom merge-sort, implementirane ili kao nizovi ili povezane liste (isto kao S), tako da spajanje dva sortirane sekvence može biti urađeno u linearnom vremenu. Kao što smo prethodno pomenuli, mi analiziramo merge-sort algoritam korišćenjem merge-sort stabla T. Mi zovemo vreme provedeno u čvoru v od T vremenom rada rekurzivnog poziva povezanog sa v, isključujući vreme čekanja da bi se završio rekurzivni poziv povezan sa decom od v. Drugim rečima, vreme provedeno u čvoru v uključuje vremena rada koraka podeli i osvoji, ali isključuje vreme rada koraka vrati. Mi smo već zapazili da su detalji koraka podeli jasni; ovaj korak radi u vremenu proporcionalnom veličini sekvence za v. U skladu sa tim, kao što je diskutovano iznad, korak osvojiti, koji se sastoji od spajanja dve sortirane podsekvence, takođe traje linearno vreme, nezavisno da li radimo sa nizovima ili povezanim listama. To znači, dozvoljavajući da i označava dubinu čvora v, da vreme provedeno u čvoru v je O(n/2), pošto veličina sekvence upravljane rekurzivnim pozivom povezanim sa v je n/2i. Posmatrajući stablo T više globalno, kao što je prikazano na slici 8.18, vidimo da dajući našu definiciju “vreme provedeno u čvoru”, vreme rada merge-sort je jednako sumi vremena provedenih u čvorovima od T. Primetimo da T ima tačno 2 čvora na dubini i. Ovo jednostavno zapažanje ima značajnu konsekvencu, pošto ukazuje da ukupno vreme provedeno u svim čvorovima od T na dubini i

je O(2n/2i), koje je O(n). Prema predlogu 1, visina od T je logn. Prema tome, pošto vreme

provedeno u svakom od log n + 1 nivoa od T je O(n), mi imamo sledeći rezultat:

Predlog 2: Algoritam merge-sort sortira sekvencu S veličine n u O(n logn) vremenu, pretpostavljajući da dva elementa od S mogu biti upoređena u O(1) vremenu.

Slika 8.18 Vizuelna analiza vremena merge-sort stabla T. Svaki čvor je prikazan sa oznakom veličine njegovog podproblema.

8.2.4 Java implementacija merge-sort

U ovom delu predstavljamo Java lista-baziranu implementaciju merge-sort algoritma. Rekurzivna list-bazirana implementacija merge-sort U fragmentu koda 8.6, prikazujemo kompletnu Java implementaciju list-baziranog merge-sort algoritma kao statički rekurzivni metod, mergeSort. Komparator je korišćen da se odluči relativno uređenje dva elementa.

/**

* Sortira elemente liste u neopadajućem redu u skladu

* sa komparatorom c, korišćenjem merge-sort algoritma.

**/

public static <E> void mergeSort (PositionList<E> in, Comparator<E> c) {

int n = in.size();

if (n < 2)

return; // in lista je već sortirana u ovom slučaju

// podeli

PositionList<E> in1 = new NodePositionList<E>();

PositionList<E> in2 = new NodePositionList<E>();

int i = 0;

while (i < n/2) {

in1.addLast(in.remove(in.first())); //pomeranje prvih n/2 elem. u in1

i++;

}

while (!in.isEmpty())

in2.addLast(in.remove(in.first())); // pomeranje ostatka u in2

// vrati

mergeSort(in1,c);

mergeSort(in2,c);

//osvoji

merge(in1,in2,c,in);

}

/**

* Spajanje dve sortirane liste, in1 and in2, u soritranu listu in.

**/

public static <E> void merge(PositionList<E> in1, PositionList<E>

in2,Comparator<E> c, PositionList<E> in) {

while (!in1.isEmpty() && !in2.isEmpty())

if (c.compare(in1.first().element(), in2.first().element()) <= 0)

in.addLast(in1.remove(in1.first()));

else

in.addLast(in2.remove(in2.first()));

while(!in1.isEmpty()) // pomeranje preostalih elemenata od in1

in.addLast(in1.remove(in1.first()));

while(!in2.isEmpty()) // pomeranje preostalih elemenata od in2

in.addLast(in2.remove(in2.first()));

}

Fragment koda 8.6 Metodi mergeSort i merge koji implementiraju rekurzivni merge-sort algoritam

U ovoj implementaciji, ulaz je lista L, i pomoćne liste L1 i L2 su procesirane rekurzivnim pozivima. Svaka lista je modifikovana umetanjem i brisanjem jedino na glavi i repu; stoga, svako ažuriranje liste traje O(1) vreme, predpostavljajući da su liste implementirane sa dvostruko povezanim listama. U našem kodu, koristimo klasu NodeList za pomoćne liste. Prema tome, za listu L veličine n, metod mergeSort(L,c) radi u vremenu O(n logn) obezbeđujući da je lista L implementirana sa dvostruko povezanom listom i komparatorom c koji može da uporedi dva elementa L u O(1) vremenu. Nerekurzivna niz-bazirana implementacija merge-sort Ovo je nerekurzivna verzija niz-baziranog merge-sort, koja radi u O(nlogn) vremenu. Ona je malo brža

od rekurzivnog list-baziranog merge-sort u praksi, pošto izbegava dodatne rekurzivne pozive i kreiranje čvora. Glavna ideja je izvođenje merge-sort bottom-up, izvođenjem spajanja nivo-po-nivo krećući se ka merge-sort stablu. Za dati niz elemenata, mi počinjemo sa spajanjem svakog par-nepar para elemenata u sortirane nizove dužine 2. Mi spajamo ove nizove u nizove dužine četiri, spajamo ove nove nizove u nizove dužine osam, i tako dalje, dok niz nije sortiran. Da bi sačuvali korišćenje prostora razumnim, mi razvijamo izlazni niz koji smešta spojene nizove (svapujući ulazne i izlazne nizove nakon svake iteracije). Java implementacija koda prikazan je u fragmentu koda 8.7, gde mi koristimo ugrađeni metod System.arraycopy da kopiramo opseg ćelija između dva niza.

/** Sortira niz sa komparatorom korišćenjem nerekurzivnog merge sort. */

public static <E> void mergeSort(E[] orig, Comparator<E> c) {

E[] in = (E[]) new Object[orig.length]; // pravi novi privrem. niz

System.arraycopy(orig,0,in,0,in.length); // kopira ulaz

E[] out = (E[]) new Object[in.length]; // izlazni niz

E[] temp; // referenca temp niza korišćenog za svapovanje

int n = in.length;

for (int i=1; i < n; i*=2) {

//svaka iteracija sortira sve dužina-2*i nizove

for (int j=0; j < n; j+=2*i)

// svaka iteracija spaja dva dužina-i parove

merge(in,out,c,j,i); // spaja od in u out dva dužina-i nizove na j

temp = in; in = out; out = temp;//svapuje nizove za sledeću iteraciju

}

// "in" niz sadrži sortirani niz, tako da ga ponovo kopira

System.arraycopy(in,0,orig,0,in.length);

}

/** Spaja dva podniza, specificirana preko start and increment. */

protected static <E> void merge(E[] in, E[] out, Comparator<E> c, int

start,

int inc) {

// merge in[start..start+inc-1] and in[start+inc..start+2*inc-1]

int x = start; // index into run #1

int end1 = Math.min(start+inc, in.length); // granica za niz #1

int end2 = Math.min(start+2*inc, in.length); // granica za niz #2

int y = start+inc; //index into run #2 (could be beyond array boundary)

int z = start; // index into the out array

while ((x < end1) && (y < end2))

if (c.compare(in[x],in[y]) <= 0) out[z++] = in[x++];

else out[z++] = in[y++];

if (x < end1) // first run didn't finish

System.arraycopy(in, x, out, z, end1 - x);

else if (y < end2) // second run didn't finish

System.arraycopy(in, y, out, z, end2 - y);

}

Fragment koda 8.7 Implementacija nerekurzivnog merge-sort algoritma

8.2.5 Merge-sort i relacija vraćanja

Postoji drugi način da se opravda da vreme rada merge-sort algoritma je O(nlogn) (predlog 2). Naime, možemo da radimo više direktno sa rekurzivnom prirodom merge-sort algoritma. U ovom delu, mi predstavljamo takvu analizu vremena rada merge-sort, i tako uvodimo matematički koncept jednačine vraćanja (takođe poznate kao relacije vraćanja). Neka funkcija t(n) označava vreme rada najgoreg slučaja merge-sort na ulaznoj sekvenci veličine n. Pošto je merge-sort rekurzivan, možemo da okarakterišemo funkciju t(n) preko jednačine gde je funkcija t(n) rekurzivno izražena u pojmovima same sebe. Da bi pojednostavili našu karakterizaciju t(n), usredsredićemo našu pažnju na slučaj gde je n stepena 2. U ovom slučaju, možemo da specificiramo definiciju t(n) kao

t n ={ b ako je n≤ 1

2t n /2 +cn drugo }

Izraz takav kao ovaj iznad zove se jednačina vraćanja (recurrence equation), pošto se funkcija pojavljuje na obe levim- i desnim stranama jednakog znaka. Mada je takva karakterizacija

ispravna i pogodna, ono što mi zaista želimo je big-Oh tip karakterizacije t(n) koji ne obuhvata samu funkciju t(n). To znači, da želimo zatvorenu-formu karakterizacije t(n). Možemo da dobijemo rešenje zatvorene-forme primenom definicije jednačine vraćanja, pretpostavljajući da je n relativno veliko. Na primer, nakon još jedne primene jednačine iznad, možemo da napišemo novo vraćanje za t(n) kao

t(n) = 2(2t(n/22) + (cn/2) + cn = 22t(n/22) + 2(cn/2) + cn = 22t(n/22) + 2cn

Ako primenimo jednačinu ponovo, dobijamo t(n)= 23t(n/23) + 3cn. U ovoj tački, mi treba da vidimo šablon koji se pojavljuje, tako da nakon primene ove jednačine i puta dobijamo

t(n) = 2it(n/2i) + icn

Stavka koja ostaje je određivanje kada da se prekine ovaj proces. Da bi videli kada da se stane,

setimo se da prelazimo na zatvorenu formu t(n)=b kada je n1, koja će se pojaviti kada je 2i=n. Drugim rečima, ovo će se pojaviti kada je i=logn. Pravljenje ove zamene, tada donosi

t(n) = 2logn t(n/2logn) + (logn)cn = nt(1) + cn logn = nb + cn logn

To znači da dobijamo alternativnu opravdanost činjenice da t(n) je O(nlogn).

30. Quick-Sort algoritam Algoritam sortiranja o kome ćemo ovde diskutovati zove se quick-sort. Kao i merge-sort algoritam, ovaj algoritam je takođe baziran na paradigmi podeli-i-osvoji, ali koristi ovu tehniku na suprotan način, pošto je sav težak rad urađen pre rekurzivnih poziva. Visoki-nivo opisa quick-sorta Quick-sort algoritam sortira sekvencu S korišćenjem jednostavnog rekurzivnog pristupa. Glavna ideja je primena tehnike podeli-i-osvoji, pomoću koje mi delimo S u podsekvence, vraćamo sortiranu svaku podsekvencu, i zatim kombinujemo smeštene podsekvence jednostavnim povezivanjem. U osnovi, quick-sort algoritam se sastoji od sledeća tri koraka (slika 8.14):

Podeli: Ako S ima najmanje dva elementa (ništa ne treba da se uradi ako S ima nula ili jedan element), selektujte specifični element x iz S, koji se zove pivot. Ono što je česta praksa, je odabir da pivot x bude zadnji element u S. Uklonite sve elemente iz S i stavite ih u tri podsekvence:

L, smeštanje elemenata iz S manjih od x

E, smeštanje elemenata iz S jednakih x

G, smeštanje elemenata iz S većih od x

Naravno, ako su svi elementi S posebni, tada E čuva samo jedan element – sam pivot.

Vrati: Rekurzivno sortira sekvence L i G

Osvoji: Stavlja nazad elemente u S po redu, prvo umetanjem elemenata iz L, zatim iz E, i konačno onih iz G.

Slika 8.14 Vizuelna šema quick-sort algoritma

Slika 8.15 Quick-sort stablo T za izvršavanje quick-sort algoritma na sekvenci sa 8 elemenata. Pivot korišćen na svakom nivou rekurzije je prikazan boldirano.

a) ulazne sekvence procesirane u svakom čvoru od T b) izlazne sekvence generisane u svakom čvoru od T

Slika 8.16 Vizuelizacija izvršavanja quick-sorta. Svaki čvor stabla predstavlja rekurzivni poziv. Čvorovi nacrtani sa isprekidanim linijama predstavljaju pozive koji nisu još napravljeni. Čvor nacrtan sa punim

linijama predstavlja objavljene pozive. Prazni čvorovi nacrtani sa tankim linijama predstavljaju završene pozive. Preostali čvorovi predstavljaju suspendovane pozive (to znači, aktivne pozive koji

čekaju da se vrati poziv deteta). Zabeležimo da su koraci podele izvršeni u b), d) i f).

Kao i merge-sort, izvršavanje quick-sorta može biti vizuelizovano preko binarnog stabla za pretraživanje, koje se zove quick-sort stablo. Slika 8.15 sažima izvršavanje quick-sort algoritma prikazivanjem ulaznih i izlaznih sekvenci procesiranih u svakom čvoru quick-sort stabla.

Ova korak-po-korak evolucija quick-sort stabla prikazana je na slikama 8.16. 8.17 i 8.18. Međutim za razliku od merge-sort, visina quick-sort stabla povezana sa izvršavanjem quick-sort je linearna u najgorem slučaju. Ovo se događa, na primer, ako se sekvenca sastoji od n odvojenih elemenata i već je sortirana. U stvari, u ovom slučaju, standardni izbor pivota kao najvećeg elementa vodi ka podsekvenci L veličine n-1, dok podsekvenca E ima veličinu 1 i podsekvenca G ima veličinu 0. Pri svakom pozivanju quick-sort na podsekvenci L, veličina opada za jedan. Stoga, visina quick-sort stabla je n-1.

Slika 8.17 Vizuelizacija izvršavanja quick-sorta. Zabeležimo da je korak osvoji izvršen u k).

Slika 8.18 Vizuelizacija izvršavanja quick-sorta. Nekoliko poziva između p) i q) su izostavljeni. Zabeležimo da su koraci osvoji izvršeni u o) i r).

Izvođenje quick-sort na nizovima i listama U fragmentu koda 8.6, dajemo pseudo-kod opis quick-sort algoritma koji je efikasan za sekvence implementirane kao nizovi ili povezane liste. Algoritam sledi šablon za quick-sort prikazan iznad, dodajući detalje skaniranja ulazne sekvence S nazad, da bi se ona podelila u liste L, E i G elemenata

koji su respektivno manji nego, jednaki, i veći nego pivot. Mi izvodimo ovo skaniranje unazad, pošto uklanjanje zadnjeg elementa u sekvenci je konstantno-vreme operacija nezavisno da li je sekvenca implementirana kao niz ili povezana lista. Mi zatim vraćamo liste L i G, i kopiramo sortirane liste L, E i G natrag u S. Mi izvodimo ovaj poslednji skup operacija u smeru napred, pošto umetanje elemenata na kraj sekvence je konstantno-vreme operacija nezavisno od toga da li je sekvenca implementirana kao niz ili povezana lista.

Algorithm Quick-sort (S): Input: Sekvenca S, implementirana kao niz ili povezana lista Output: Sekvenca S u sortiranom redu

If S.size() 1 then return {S je već sortirano u ovom slučaju}

p S.last().element() {pivot} Neka L, E i G budu prazne list-bazirane sekvence while !S.isEmpty do {skanira S unazad, deleći ga u L, E i G} if S.last().element() < p then L.addLast(S.remove(S.getLast())) else if S.last().element() = p then E.addLast(S.remove(S.getLast())) else {zadnji element u S je veći od p} G.addLast(S.remove(S.getLast())) Quicksort(L) {Vraća elemente manje od p} Quicksort(G) {Vraća elemente veće od p} while !L.isEmpty do {kopira nazad u S sortirane elemente manje od p} S.addLast(L.remove(L.getFirst())) while !E.isEmpty do {kopira nazad u S sortirane elemente jednake p} S.addLast(E.remove(E.getFirst())) while !G.isEmpty do {kopira nazad u S sortirane elemente veće od p} S.addLast(G.remove(G.getFirst())) return {S je sada sortirano}

Fragment koda 8.6 Quick-sort za ulaznu sekvencu S implementiranu sa povezanom listom ili nizom

Vreme rada quick-sort Mi možemo da analiziramo vreme rada quick-sort algoritma sa istom tehnikom korišćenom za merge-sort algoritam. Naime, možemo da identifikujemo vreme provedeno u svakom čvoru quick-sort stabla T i da sumiramo vremena rada za sve čvorove. Pregledanjem fragmenta koda 8.6, vidimo da koraci osvoji i podeli quick-sorta mogu biti implementirani u linearnom vremenu. Prema tome, vreme provedeno u čvoru v stabla T je proporcionalno ulaznoj veličini s(v) od v, definisanoj kao veličina sekvence upravljane pozivanjem quick-sorta povezanog sa čvorem v. Pošto podsekvenca E ima najmanje jedan element (pivot), suma ulaznih veličina dece od v je najviše s(v) - 1. Za dato quick-sort stablo T, neka si označava sumu ulaznih veličina čvorova na dubini i u T.

Jasno, s0 = n, pošto koren r od T je povezan sa kompletnom sekvencom. Takođe, s1 n – 1, pošto pivot nije raširen do dece od r. Razmatraćemo sledeće s2. Ako oba deteta od r imaju ulaznu veličinu

različitu od nule, tada s2 n – 3. U suprotnom (jedno dete korena ima nula veličinu, dok drugo ima

veličinu n – 1), s2 = n – 2. Prema tome, s2 n – 2. Nastavljajući ovom linijom rasuđivanja, dobijamo da

je si n – i. Kao što smo utvrdili, visina visina quick-sort stabla T je n-1 u najgorem slučaju. Prema

tome, vrema rada najgoreg slučaja quick-sorta je O ∑i=0

n− 1

si , koje je O ∑

i=0

n− 1

n− 1 , to znači

O ∑i=1

n

i . Pošto ∑i=1

n

i je O n2, quick-sort radi u O(n2) vremenu najgoreg slučaja.

U sklada sa njegovim imenom, mi očekujemo da quick-sort radi brže. Međutim, kvadratni stepen prikazan iznad ukazuje da je quick-sort spor u najgorem slučaju. Paradoksalno, ovo ponašanje najgoreg-slučaja pojavljuje se za instance problema kada sortiranje treba da budu lakše – ako je sekvenca već sortirana. Vraćajući se nazad u našu analizu, primetimo da se najbolji slučaj za quick-sort na sekvenci posebnih elemenata pojavljuje kada su događa da podsekvence L i G imaju približno istu veličinu. To

znači, da u najboljem slučaju, imamo

s0 = n

s1 = n - 1

s2 = n – (1+2) = n - 3

....

si = n – (1+2+22+...+2i-1) = n – (2i – 1)

Prema tome, u najboljem slučaju T ima visinu O(logn) i quick-sort radi u O(n logn) vremenu.

Slučajni quick-sort

Jedan opšti metod da analiziranje quick-sorta je da pretpostavimo da će pivot uvek da podeli sekvence skoro podjednako. Međutim, mi osećamo da će takva pretpostavka unapred pretpostaviti znanje o distribuciji ulaza koje tipično nije dostupna. Na primer, mi možemo pretpostaviti da ćemo retko dobiti “skoro” sortirane sekvence da ih sortiramo, koji su stvarno česte u mnogim aplikacijama. Na sreću, ova pretpostavka nije potrebna da bi prilagodili našu intuiciju quick-sort ponašanju. Slučajan izbor pivota Pošto je cilj koraka podele u quick-sort metodu da podeli sekvencu S skoro podjednako, uvešćemo slučajnost u algoritam i izabrati pivot kao slučajni element ulazne sekvence. To znači, da umesto biranja pivota kao poslednjeg elementa S, mi biramo slučajni element u S kao pivot, čuvajući ostatak algoritma nepromenjen. Ova varijanta quick-sorta se zove slučajni quick-sort. Sledeći predlog pokazuje da očekivano vreme rada slučajnog quick-sorta na sekvenci sa n elemenata je O(n logn). Ovo očekivanje je uzelo sve moguće slučajne izbore koje algoritam pravi, i nezavisno je od bilo koje pretpostavke o distribuciji mogućih ulaznih sekvenci algoritma koje će verovatno da budu date.

Predlog 3: Očekivano vreme rada slučajnog quick-sorta na sekvenci S veličine n je O(n logn).

Opravdanost: Mi pretpostavljamo da dva elementa iz S mogu biti upoređena u O(1) vremenu. Razmatraćemo jednostavan rekurzivni poziv slučajnog quick-sorta, i neka n označi veličinu ulaza za ovaj poziv. Recimo da je ovaj poziv “dobar“ ako je izabrani pivot takav da podsekvence L i G imaju veličinu najmanje n/4 i najviše 3n/4 svaka; u suprotnom, poziv je “loš“.

Sada ćemo razmatrati implikaciju našeg slučajnog odabira pivota. Primetimo da postoji n/2 mogućih dobrih odabira za pivot za neki dati poziv veličine n slučajnog quick-sorta. Prema tome, verovatnoća da je neki poziv dobar je 1/2. Primetimo dalje da će dobar poziv podeliti listu veličine n u najmanje dve liste veličine 3n/4 i n/4, i loš poziv će biti loš ukoliko proizvodi jedan poziv veličine n-1. Razmatraćemo sada trag rekurzije za slučajni quick-sort. Ovaj trag definiše binarno stablo T, takvo da svaki čvor u stablu T odgovara različitom rekurzivnom pozivu podproblema koji sortira deo originalne liste. Recimo da čvor v u T je u grupi veličine i ako je veličina v podproblema veća od (3/4)i+1n i najviše (3/4)in. Analiziraćemo očekivano vreme provedeno u radu na svim podproblemima za čvorove u grupi veličine i. Očekivano vreme za rad na svim podproblemima je suma očekivanih vremena za svaki podproblem. Neki od ovih čvorova odgovaraju dobrim pozivima i neki odgovaraju lošim pozivima. Ali zabeležimo da, pošto se dobri pozivi pojavljuju sa verovatnoćom 1/2, očekivano vreme uzastopnih poziva koje treba da napravimo pre dobijanja dobrog poziva je 2. Osim toga, primetimo da što pre imamo dobar poziv za čvor u grupi veličine i, njegova deca biće u grupama veličine veće od i. Prema tome, za neki element x iz ulazne liste, očekivani broj čvorova u grupi veličine i koja sadrži x u njihovim podproblemima je 2. Drugim rečima, očekivana ukupna veličina svih podproblema u grupi veličine i je 2n. Pošto je nerekurzivni rad koji izvodimo za neki podproblem proporcionalan njegovoj veličini, ovo ukazuje da ukupno očekivano vreme potrošeno na procesiranje podproblema za čvorove u grupi veličine i je O(n). Broj grupa veličine je log4/3n, pošto ponovno množenje sa 3/4 je isto kao ponovno deljenje sa 4/3. To znači, da je broj grupa veličine O(logn). Stoga, ukupno očekivano vreme slučajnog quick-sorta je O(n logn). To je prikazano na slici 8.19.

Slika 8.19 Vizuelna analiza vremena quick-sort stabla T. Svaki čvor je prikazan označen sa veličinom

njegovog podproblema.

U-mestu quick-sort

Algoritam sortiranja je u-mestu (in-place) ako koristi samo malu količinu memorije, u skladu sa potrebom da objekti budu sortirani. Merge-sort algoritam nije u mestu i njegovo pravljenje da bude u-mestu zahteva mnogo komplikovaniji metod spajanja nego onaj o kome smo već diskutovali. Međutim, sortiranje u-mestu nije nerazdvojivo teško. Quick-sort može biti prilagođen da bude u-mestu. Međutim, izvođenje quick-sort algoritma u-mestu zahteva delić genijalnosti, za koji moramo da koristimo samu ulaznu sekvencu da smestimo podsekvence za sve rekurzivne pozive. Mi pokazujemo algoritam inPlaceQuickSort koji izvodi u-mestu quick-sort, u fragmentu koda 8.7. Algoritam inPlaceQuickSort pretpostavlja da je ulazna sekvenca S, data kao niz posebnih elemenata.

Algorithm inPlaceQuickSort(S,a,b): Input: Niz S posebnih elemenata; intedžeri a i b Output: Niz S sa elementima početno od indeksa a do b, zaključno, sortirani u neopadajućem redu od indeksa a do b

If a b then return {najviše jedan element u podopsegu}

p S[b] {pivot}

l a {skaniraće udesno}

r b - 1 { skaniraće ulevo}

while l r do {nalazi element veći od pivota}

while l r and S[l] p do

l l + 1 {nalazi element manji od pivota}

while r l and S[r] p do

r r – 1 If l < r then zamenjuje elemente S[l] i S[r] {stavlja pivot na njegovo konačno mesto} zamenjuje elemente S[l] i S[b] {rekurzivni pozivi}

inPlaceQuickSort(S,a,l-1)

inPlaceQuickSort(S,l+1,b) {mi smo završili u ovoj tački, pošto su sortirani podnizovi već uzastopni}

Fragment koda 8.7 U-mestu quick-sort za ulazni niz S

U-mestu quick-sort modifikuje ulaznu sekvencu korišćenjem elementa zamene i ne kreira eksplicitno podsekvence. U stvari, podsekvenca ulazne sekvence je implicitno predstavljena opsegom pozicija specificiranih krajnjim levim indeksom l i krajnjim desnim indeksom r. Korak podeli je izveden skaniranjem niza istovremeno od l unapred i od r unazad, zamenjujući parove elemenata koji su u

obrnutom redu, kao što je prikazano na slici 8.20. Kada se ova dva indeksa “sretnu”, podnizovi L i G su na suprotnim stranama tačke susreta. Algoritam se završava rekurzijom na ova dva podniza.

Slika 8.20 Korak podeli u-mestu quick-sorta. Indeks l skanira sekvencu sa leva na desno, i indeks r skanira sekvencu sa desna na levo. Zamena je izvedena kada je l element veći od pivota i kada je r je

element manji nego pivot. Konačna zamena sa pivotom kompletira korak podeli.

U-mestu quick-sort smanjuje vreme rada prouzrokovano kreiranjem novih sekvenci i pomeranje elemenata između njih preko konstantnog faktora. Mi pokazujemo Java verziju u-mestu quick-sorta u fragmentu koda 8.8.

public static <E> void quickSort (E[] s, Comparator<E> c) {

if (s.length < 2) return; // niz je već sortiran u ovom slučaju

quickSortStep(s, c, 0, s.length-1); // rekurzivni metod sortiranja

}

private static <E> void quickSortStep (E[] s, Comparator<E> c,

int leftBound, int rightBound ) {

if (leftBound >= rightBound) return; // prekršteni indeksi

E temp; // temp objekta korišćen za zamenu

E pivot = s[rightBound];

int leftIndex = leftBound; // skaniraće udesno

int rightIndex = rightBound-1; // skaniraće ulevo

while (leftIndex <= rightIndex) {//skan. udesno dok nije veće od pivota

while ( (leftIndex <= rightIndex) && (c.compare(s[leftIndex],

pivot)<=0) )

leftIndex++;

// scanira ulevo da nađe element manji od pivota

while ( (rightIndex >= leftIndex) && (c.compare(s[rightIndex],

pivot)>=0))

rightIndex--;

if (leftIndex < rightIndex) { // oba elementa su pronađeni

temp = s[rightIndex];

s[rightIndex] = s[leftIndex]; // zamenjuje ove elemente

s[leftIndex] = temp;

}

} // petlja nastavlja sve dok se indeksi ne ukrste

temp = s[rightBound]; // zamenjuje pivot sa elementnom na leftIndex

s[rightBound] = s[leftIndex];

s[leftIndex] = temp; // pivot je sada na leftIndex, tako da je rekurz.

quickSortStep(s, c, leftBound, leftIndex-1);

quickSortStep(s, c, leftIndex+1, rightBound);

}

Fragment koda 8.8 Kodiranje u-mestu quick-sorta, predpostavljajući posebne elemente

Nažalost, nije garantovano da implementacija iznad bude u-mestu. Primetimo, da nam je potreban prostor za stek proporcionalan dubini stabla rekurzije, koje u ovom slučaju može biti što veće od n-1. Po opštem priznanju, očekivana dubina steka je O(logn), što je malo u poređenju sa n. Ipak, jednostavan trik dozvoljava nam da obezbedimo veličinu steka O(logn). Glavna ideja je projektovati nerekurzivnu verziju u-mestu quick-sort korišćenjem eksplicitnog steka da iterativno procesira podprobleme (od kojih svaki može biti predstavljen sa parom indeksa označavajući granice podniza). Svaka iteracija obuhvata izvlačenje podproblema na vrhu, njegovo deljenje na dva (ako je dovoljno veliko), i guranje dva nova podproblema. Trik je da kada guramo novi podproblem, mi treba da prvo gurnemo veći podproblem i zatim manji podproblem. Na ovaj način, veličine podproblema će biti najmanje duplo kako mi idemo dole u stek; stoga, stek može da ima dubinu najviše O(logn).