You are on page 1of 64

Strukture podataka i algoritmi vjebe

Skripta u nastajanju Zadnja promjena 16. sijenja 2011.

Saetak Sadraj ove skripte je veinom izravno pretipkan iz vjebi kolegija Strukture podataka i algoritmi koje se u skoro nepromijenjenom obliku odravaju ve nekoliko godina na Matematikom odjelu Prirodoslovno-matematikog fakulteta. Tekst vjebi nabavljen je ljubaznou asistenta Zvonimira Bujanovia koji je izradio i runo crtane skice. Molim vas da bilo ispravke, komentare ili nejasnoe poaljete kao elektronsku potu na adresu ilija.pavlic@gmail.com. Moda zajedno malo olakamo buduim generacijama. Hvala vam, Va pretipka, Ilija.

Sadraj
1 Uvod 1.1 Vanost modeliranja . . . . . . 1.2 Pojmovi . . . . . . . . . . . . . 1.3 Primjer . . . . . . . . . . . . . 1.3.1 Matematiki model . . 1.3.2 Apstraktni tip podataka 1.3.3 Struktura podataka . . 2 2 2 3 4 5 6 10 10 16 24 31 31 36 48 51 59

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

Liste 2.1 Openita lista . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Stog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3 Red . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Stabla 3.1 Openito stablo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2 Binarno stablo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Skupovi 4.1 Rjenik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Prioritetni red . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Poglavlje 1 Uvod
Strukture podataka i algoritmi se moe smatrati prijelaznim kolegijem. Na kolegiju C (Programiranje 2) uili smo sm jezik C, to jest njegovu sintaksu. Na viim godinama, ako se odluite za smjer raunarstvo, naglasak e vie biti na modeliranju problema. Ovaj kolegij je prijelaz znamo C, a uimo kako modelirati probleme, najee pristupe modeliranju i najee implementacije.

1.1

Vanost modeliranja

Modeliranje ima veliku vanost u programiranju jer se naizgled ili stvarno teki problemi dobrim modeliranjem mogu lake ili bre rijeiti. Rjeivost u nekoj formulaciji problem se moe initi nerjeiv Sloenost razni modeli, strukture podataka i algortmi mogu poveati brzinu izvoenja (smanjiti vremensku sloenost) ili smanjiti prostor potreban za izvoenje (smanjiti prostornu sloenost)

1.2

Pojmovi

Uvodimo neke pojmove koji se koriste na ovom kolegiju: Model predstavlja formulaciju problema (koji se javlja u praksi, iz ivota) u terminima matematikih objekata (niz, grafovi, stabla i slino) Apstraktni tip podataka je apstraktno zadan tip podataka (obino sloeni) zajedno sa operacijama. Implementacija operacija nije nuno zadana. Struktura podataka je eksplicitna implementacija tipa podataka iz apstraktnog tipa podataka. To moe biti skupina varijabli u nekom programskom jeziku zajedno sa vezama meu varijablama. Rjeavanje problema je postupak postepenog pro njavanja u kojem prvo postavljamo matematiki model i algoritam u neformalnom jeziku koji koristi matematike pojmove,

zatim stvaramo apstraktni tip podataka i algoritam zapisujemo u pseudo-jeziku koji koristi operacije iz ATP, te konano apstraktni tip podataka i algoritam u pseudokodu prevodimo u strukture podataka u stvarnom programskom jeziku i algoritam zapisan u stvarnom programskom jeziku, to nam predstavlja rjeenje problema. Pri rjeavanju emo se koristiti slikovitim prikazima estih struktura koji su vidljivi na slici 1.1. Postupak rjeavanja postepenim pro njavanjem vidljiv je na skici 1.2.

a)
. 0 1 2 . . . n

b)

c)

d)
i

e)

. . .

Slika 1.1: Slikoviti prikazi estih struktura a) elija, b) polje, c) zapis, d) pokaziva (pointer), e) kursor

Problem

Matematiki model . neformalno opisani algoritam

Apstraktni tipovi podataka algoritam zapisan u pseudojeziku (koriste se operacije iz ATP)

Strukture podataka algoritam zapisan u programskom jeziku (operacije iz ATP su pretvorene u potprograme)

Rjeenje problema

Slika 1.2: Skica postupka postepenog pro njavanja Suvremeni, najee objektno orijentirani, jezici poput C++, Jave ili C# jezika uobiajeno dolaze sa standardnim bibliotekama koje implementiraju razne ATP (ukljuujui i sve koji se obrauju na vjebama) koje tada moemo koristiti na nivou ATP-a, bez ulaenja u implementaciju.

1.3

Primjer

Predstavimo gore opisani postupak na jednom primjeru. Raskre izgleda kao na slici 1.3. Putevi C i E su jednosmjerni, a ostali su dvosmjerni. Postoji 13 naina za prolazak

D C A B
.

Slika 1.3: Skica raskra ulice C i E su jednosmjerne kroz raskre. Neki parovi prolazaka (npr. AB i EC) mogu se obavljati istovremeno, no neki (npr. AD i EB) ne mogu jer im se putanje sijeku. Na raskre e se postavljati semafori. Potrebno je smisliti reim rada semafora. Taj reim se sastoji od nekoliko faza. U svakoj fazi je dozvoljen neki prelazak. Zahtijeva se: Za vrijeme jedne faze ne smije biti dozvoljen prelazak dvama prelascima kojima se putanje sijeku Svaki od 13 prelazaka mora biti dozvoljen u barem jednoj fazi Broj faza treba biti to manji Ovaj problem obraujemo slijedei skicu 1.2. Prvo formuliramo matematiki model.

1.3.1

Matematiki model

Problem modeliramo neorijentiranim grafom. Vrhovi predstavljaju prelaske. Bridovi povezuju one prelaske koji se ne mogu obavljati istovremeno. Problemi s meusobnim sukobima u kojima je potrebno minimizirati koritenje dostupnih resursa esto se mogu modelirati pomou problema bojanja grafa (npr. problemi rasporeda ili problem dodjeljivanja procesorskih registara). Potrebno je obojati vrhove grafa sa to manje boja tako da susjedni (meusobni incidentni vrhovi) ne smiju biti obojanom istom bojom. Primjetimo da boje predstavljaju resurse (npr. termin, registar), a incidentnost meusobnu sukobljenost (oba vrha istodobno zahtijevaju neki ogranieni resurs). Neformalno opisani algoritam Problem bojanja grafa je poznat u teoriji algoritama kao NP-teak. Ugrubo to znai nije otkriven e kasni algoritam za njegovo rjeavanje, i da se svi problemi koji su u takvoj klasi mogu svesti na njega u razumnom vremenu. Detaljnije o tome na kolegijima Oblikovanje i analiza algoritama i Sloenost algoritama. Mi emo zato koristiti jedan priblini, heuristiki algoritam koji daje prilino dobro rjeenje, no ne jami da je ono i optimalno. Algoritam spada u tzv. pohlepne (greedy) algoritme. 4

. AB

. AC

. AD

. BA

. BC .

. BD

. DA

. DB

. DC

. EA

. EB

. EC

. ED

Slika 1.4: Skica problema raskra u obliku grafa


void greedy(COLORED_GRAPH G) { boja novaBoja; radi { novaBoja = bilo koja neupotrebljena boja; za svaki neobojeni vrh v grafa G { ako v nije susjedan niti jednom vrhu obojenom sa novaBoja oboji v sa novaBoja } } dok svi vrhovi od G nisu obojani }

1.3.2

Apstraktni tip podataka

Uvodimo apstraktni tip podataka COLORED_GRAPH kojim opisujemo matematiki pojam obojenog (neusmjerenog) grafa. Taj tip podataka treba ukljuivati sve operacije koje se pojavljuju u neformalnom algoritmu kao to su provjera obojanosti i incidentnosti ili bojanje vrha nekom bojom. ATP je zadan sljedeim popisom tipova i operacija. vrh podaci ovog tipa identi ciraju vrhove boja podaci ovog tipa oznaavaju boje. Ukljuena je i posebna boja prazno koja oznaava odsustvo boje OBOJENI_GRAH podatak ovog tipa je (neorijentirani) graf sastavljen od razliitih vrhova tipa vertex. Svaki vrh je obojan bojom tipa colortype 5

BOJA(v,G) funkcija koja vraa boju vrha v u grafu G INCIDENTAN(v1,v2,G) logika funkcija koja testira jesu li vrhovi v1 i v2 u grafu G susjedni POSTAVI_BOJU(c,v,G) procedura koja pridruuje boju c vrhu v u grafu G Algoritam zapisan u pseudo-jeziku
void greedy(COLORED_GRAPH *G) { bool zavrsetak , pronadeno; boja novaBoja; do { zavrsetak = true; novaBoja = bilo koja jo neiskoritena boja; for (svaki vrh v1 od G) if (BOJA(v1,G) == blank) { zavrsetak = 0; pronadeno = 0; for (svaki vrh v2 od G) { if (INCIDENTAN(v1,v2,G) && COLOR(v2,G)==novaBoja) pronadeno = true; } if (!pronadeno) POSTAVI_BOJU(novaBoja ,v1,G); } while (!finished); }

1.3.3

Struktura podataka

Ograniavamo se na grafove sa ksnim, unaprijed zadanim brojem vrhova n. Tipove boja, vrh kodiramo na sljedei nain:
typedef int vrh; typedef int boja;

Vrijednost boje prazno je 0. Nae vrhove-prijelaze AB, AC, AD,... preimenujemo u 0, 1, . . . , n 1. Nakon toga sm obojeni graf (dakle podatak tipa OBOJENI_GRAF) moemo prikazati jednom matricom susjedstva i jednim poljem.
typedef struct obojenigraf { bool susj[n][n]; boja boje[n]; } OBOJENI_GRAF;

U matrici susjedstva je incidentnost dva vrha vi i vj je zadana kao vrijednost susj[i][j]. Budui da je graf neusmjeren, matrica je nuno simetrina. i-ti element polja boje sadri boju pridruenu i-tom vrhu.

AB 0 AB 0 AC 1 AD 2 BA 3 BC 4 BD 5 DA 6 DB 7 DC 8 EA 9 EB 10 EC 11 ED 12

AC 1

AD 2

BA 3

BC 4 1

BD 5 1 1

DA 6 1 1

DB 7 1

DC 8

EA 9 1 1 1

EB 10 1 1 1 1 1

EC 11

ED 12

1 1 1

1 1 1 1 1 1 1 1 1 1 1 1

1 1 1

1 1

1 1

Tablica 1.1: Matrica susjedstva susj[][] strukture OBOJANI_GRAF


0 1 2 3 4 5 6 7 8 9 10 11 12

. 0 0
0 0 0 0 0 0 0 0 0 0 0

Tablica 1.2: Polje boje[], u kojem su zapisane boje u vrhovima Za na konkretni graf vrijednosti u poetku izvoenja algoritma su prikazane u tablicama 1.1 i 1.2. Da bi dobili kompletnu implementaciju ATP trebali bismo operacije iz ATP napisati kao potprograme. No za nae operacije je to trivijalno i svodi se na jedan logiki uvjet ili jedno pridruivanje. Algoritam zapisan u programskom jeziku
void greedy(OBOJENI_GRAF *G) { bool zavrsetak , pronadeno; boja novaBoja;

vrh i,j; novaBoja = 0; do { zavrsetak = 0; novaBoja++; for (i=0; i<n; i++) { if (G->boja[i] == 0) { zavrsetak = 0; pronadeno = 0; for (j=0; j<n; j++) if (G->susj[i][j] && G->boja[j] == novaBoja) pronadeno = 1; if (!pronadeno) G->boja[i] = novaBoja; } } while (!zavrsetak) }

Ovaj potprogram trebalo bi jo uklopiti u glavni program. Na poetku glavnog programa de nirala bi se konstanta n = 13, te svi potrebni tipovi. U tijelu glavnog programa bi se inicijalizirali podaci koji opisuju graf, a zatim bi se pozvala funkcija greedy. Na kraju bi se interpretirao rezultat pohranjen u polju boje. Rjeenje koje opisani algoritam daje za na konkretan problem raskra zapisano je u tablici 1.3: Faza u radu semafora 1 2 3 4 Dozvoljeni prolasci AB, AC, AD, BA, DC, ED BC, BD, EA DA, DB EB, EC

Tablica 1.3: Rjeenje problema postavljanja semafora Lako se moe pokazati da je ovo i optimalno rjeenje. Naime u grafu postoji tzv. klika reda 4: skup od 4 vrha koji su svi meusobno povezani (to se moe vidjeti na slici 1.5). Oito nam treba barem 4 boje da obojimo ovu kliku (razmislite zato). Postoje i druga optimalna rjeenja, dakle i drugi naini da se zadani graf oboji sa 4 boje.

. AB

. AC

. AD

. BA

. BC .

. BD

. DA

. DB

. DC

. EA

. EB

. EC

. ED

Slika 1.5: Klika reda 4 u grafu

Poglavlje 2 Liste
2.1 Openita lista

Lista je konaan niz podataka istog tipa (a1 , a2 , . . . , an ). Promatramo je kao sljedei apstraktni tip podataka: elementtype bilo koji tip (jednostavan ili sloen) LIST konani niz podataka tipa elementtype position podatak ovog tipa slui za identi ciranje elemenata u listi FIRST(L) funkcija vraa poziciju na poetku liste L END(L) funkcija vraa poziciju na kraju liste L NEXT(p,L) funkcija vraa iduu poziciju iza p u listi L PREVIOUS(p,L) funkcija vraa prethodnu poziciju ispred p u listi L RETRIEVE(p,L) funkcija vraa element na poziciji p u listi L MAKE_NULL(&L) funkcija pretvara L u praznu listu i vraa END(L) INSERT(x,p,&L) funkcija ubacuje element x na poziciju p u listi L DELETE(p,&L) funkcija izbacuje element na poziciji p u listi L Zadatak 2.1.1. Napiite potprogram oblika void PURGE (LIST *l) koji u listi L izbacuje elemente-duplikate. Potprogram treba biti neovisan o implementaciji ATP LIST. Rjeenje: Ideja rjeenja je poevi od prvog elementa usporeivati sve elemente koji slijede trenutni s trenutnim. Kada izbacimo sve elemente koji su jednaki trenutnom, trenutni element postavimo na njemu sljedei. Pretpostavimo da nakon INSERT i DELETE pozicije svih neobrisanih elemenata ostaju ouvane.

10

void PURGE(LIST* l) { position p, q; p = FIRST(*l); while (p != END(*l)) { q = NEXT(p,*l); while (q != END(*l)) { if (RETRIEVE(p,*l) == RETRIEVE(q,*l)) { position temp = q; q = NEXT(q,*l); DELETE(temp,l); } else q = NEXT(q,*l); } p = NEXT(p,*l); } }

Napomena: Objasnimo pretpostavku o ouvanju elemenata nakon operacija INSERT i DELETE. Pretpostavimo da imamo listu L = (6, 2, 9, 3) i neka su pozicije tih elemenata redom a, b, c i d. Takoer pretpostavimo da elimo ubaciti element 5 ispred pozicije c, dakle INSERT(5,c,&L). Lista e izgledati ovako: L = (6, 2, 5, 9, 3). Ali to e biti sa pozicijama a, b, c, d? Odgovor na ovo pitanje ovisi o implementaciji. Kao primjer, u implementaciji pomou polja, a, b, c, d e biti pozicije od 6, 2, 5, 9 redom. Prikaz ove situacije moe se vidjeti na slici 2.1. U implementaciji pomou pokazivaa kao u skripti, a, b, c, d e biti pozicije od 6, 2, 5, 3 redom. a b c d . 6 2 9 3 a b c d 6 2 9 3 a b c d 6 2 5 9 3

a)

b)

c)

Slika 2.1: a) Poetne pozicije u listi, b) Premjetanje elemenata kako bi se stvorilo slobodno mjesto i c) Novi element ubaen na osloboeno mjesto Dakle nee biti isto. De nicija liste nita ne govori o tome to se dogaa sa pozicijama. Ali ako kaemo da pozicije nakon svakog koritenja INSERT ili DELETE vie nemaju nikakvo znaenje, onda ATP LIST postaje vrlo nespretan za koritenje. 11

Zbog toga emo u tekst zadatka (kad to bude potrebno) dodati renicu Pretpostavite da nakon operacija INSERT i DELETE pozicije svih (neobrisanih) elemenata ostaju ouvane. Svaka uobiajena implemenatacija ATP LIST (C++ STL, Java, C# Generics) ima ovakvu konvenciju. Implementacije iz skripte ne potuju ovu konvenciju (ali one sa pointerima i kursorima su jako blizu i mogu se izmijeniti tako da potuju). Uz ovu pretpostavku, nakon INSERT(5,c,&L) vrijedi: a, b, c, d su pozicije od 6, 2, 9, 3. U alabahteru pie INSERT ubacuje podatak x na poziciju p, ispravnije je rei da INSERT ubacuje podatak x ispred podatka ija je pozicija p. Do pozicije novoubaenog elementa moe se doi pomou PREVIOUS(p,L), to je neka nova pozicija koja nije postojala u listi prije ubacivanja elementa. Nakon DELETE(c,&L) lista L = (6, 2, 9, 3) postaje L = (6, 2, 3), a, b, d su pozicije od 6, 2, 3; pozicija c vie nema smisla. Pretpostavimo da je na skupu koji sadri elemente tipa elementtype de niran ureaj . Kaemo da je lista (a1 , a2 , . . . , an ) sortirana ako vrijedi a1 a2 . . . an ili ai ai+1 , i. Drukije reeno lista L je sortirana ako vrijedi: RETRIEVE(p,L) RETRIEVE(NEXT(p,L),L) za sve pozicije p takve da su obje strane nejednakosti de nirane. Zadatak 2.1.2. Napiite potprogram oblika SINSERT(elementtype x, LIST *L) kojim se u sortiranu listu L ubacuje element x tako da lista ostane sortirana. Potprogram treba biti neovisan o implementaciji ATP LIST. Rjeenje: Ideja rjeenja je pronai ispravno mjesto za ubacivanje elementa. Kad naiemo na element koji je vei ili jednak od x, tada x moemo ubaciti ispred tog elementa.
void SINSERT(elementtype x, LIST* l) { position p = FIRST(L); bool pronadeno = FALSE; while (!pronadeno && p!=END(l)) { if (x <= RETRIEVE(p,l)) pronadeno = true; else p = NEXT(p,l); } INSERT(x,p,l); }

Napomena: Prethodni potprogram SINSERT moe posluiti kao osnovni korak u algoritmu za sortiranje liste. Neka je L1 polazna nesortirana lista. Stvaramo novu sortiranu listu tako da redom elemente iz L1 ubacujemo u L2 pomou SINSERT. Zadatak 2.1.3 (Za vjebu). Napiite algoritam iz napomene kao potprogram neovisan o implementaciji ATP LIST. Algoritam je u literaturi poznat pod nazivom insertion sort. Ukoliko je lista implementirana pomou polja, tada se sortiranje moe obaviti unutar liste koritenjem samo jo jednog dodatnog elementa (in-place). Algoritam je vremenski kvadratan, no priblino sortiranu listu e kasno sortira. Zato se ponekad koristi za zavretak rekurzivnog sortiranja (npr. u quicksort algoritmu). 12

. L1 = (5, 3, 7, 1, 4, 3) . L1 = (5, 3, 7, 1, 4, 3) . L1 = (5, 3, 7, 1, 4, 3) . . L1 = (5, 3, 7, 1, 4, 3) . L1 = (5, 3, 7, 1, 4, 3) . L1 = (5, 3, 7, 1, 4, 3) . L1 = (5, 3, 7, 1, 4, 3)


SINSERT(5,L2) SINSERT(3,L2) SINSERT(7,L2) SINSERT(1,L2) SINSERT(4,L2) SINSERT(3,L2)

. L2 = () . L2 = (5) . L2 = (3, 5) . L2 = (3, 5, 7) . L2 = (1, 3, 5, 7) . L2 = (1, 3, 4, 5, 7) . L2 = (1, 3, 3, 4, 5, 7)

Slika 2.2: Primjer koritenja funkcije SINSERT za sortiranje liste L1 = (5, 3, 7, 1, 4, 3) Zadatak 2.1.4. Napiite potprogram oblika void MERGE(LIST l1, LIST l2, LIST *l3) kojim se dvije sortirane liste L1 i L2 saimlju u novu, takoer sortiranu listu L3 . Potprogram mora biti neovisan o implementaciji ATP LIST. Rjeenje: Ideja rjeenja je da usporeujemo elemente lista L1 i L2 , poevi od prve pozicije. Ubacujemo manji element u treu listu, te se pomiemo u onoj listi iji element je ubaen u treu listu.
void MERGE(LIST l1, LIST l2, LIST* l3) { position p1, p2, p3; p1 = FIRST(l1); p2 = FIRST(l2); p3 = MAKE_NULL(l3); while (p1 != END(l1) && p2 != END(l2)) { if (RETRIEVE(p1,l1) <= RETRIEVE(p2,l2)) { INSERT(RETRIEVE(p1,l1),p3,l3); p1 = NEXT(p1,l1); } else { INSERT(RETRIEVE(p2,l2),p3,l3); p2 = NEXT(p2,l2); } p3 = NEXT(p3,*l3); } if (p1 == END(l1)) while (p2 != END(l2))

13

{ INSERT(RETRIEVE(p2,l2),p3,l3); p2 = NEXT(p2,l2); p3 = NEXT(p3,*l3); } else while (p1 != END(l1)) { INSERT(RETRIEVE(p1,l1),p3,l3); p1 = NEXT(p1,l1); p3 = NEXT(p3,*l3); } }

Napomena: Prethodni potprogram moe sluiti kao osnovni korak u algoritmu za sortiranje. Polaznu nesortiranu listu L1 razbijemo na (to vee) podliste koje su ve sortirane. Zatim redom saimljemo dvije po dvije podliste, sve dok ne ostane samo jedna (sortirana) lista. Skica postupka vidljiva je na slici 2.3. L (5), (3, 7),
MERGE

(1, 4),
MERGE

(3)

(3, 5, 7), (1, 3, 4)


MERGE

(1, 3, 3,.4, 5, 7) Slika 2.3: Primjer sortiranja liste L = (5, 3, 7, 1, 4, 3) koritenjem funkcije MERGE Ovakav algoritam je poznat u literaturi pod nazivom merge sort . Bilo koji redoslijed saimanja daje ispravan rezultat (sortiranu listu). No, o redoslijedu izvravanja ovisi vrijeme izvravanja. U skripti Strukture podataka i algoritmi promatra se implementacija liste pomou pokazivaa. Lista je prikazana kao vezana lista elija. Svaka elija sadri jedan element i pokaziva (pointer) na iduu eliju. Takoer je dodana polazna elija (header), koja ne sadri element. Lista se poistovjeuje sa pokazivaem na header. Pozicija elementa a1 je pokaziva na header, pozicija elementa a2 je pokaziva na eliju koja sadri a1 i tako dalje pozicija elementa an je pokaziva na eliju koja sadri an1 ili pozicija elementa ai je pokaziva na eliju koja sadri pokaziva na element ai . Pozicija END(L) je pokaziva na eliju koja sadri an . Ova pomalo neprirodna de nicija slui za e kasniju implementaciju operacija INSERT() i DELETE(). Ova implementacija je izravno primjenjiva samo u programskim jezicima koji podravaju pokazivae (Pascal, C, C++,...). Ukoliko programski jezik nema pokazivae (npr. 14

FORTRAN), implementaciju treba modi cirati tako da pokazivae zamijenimo sa kursorima. Zadatak 2.1.5. Razradite implementaciju liste pomou kursora, tako da u prethodno spomenutoj implementaciji pomou pokazivaa zamijenite pokazivae kursorima. Rjeenje: Slobodan prostor u memoriji raunala zauzmemo jednim velikim poljem oblika:
struct { elementtype element; int next; } SPACE[maxlength];

Ovo polje predstavlja zalihu elija od kojih e se graditi liste. Svaka lista je prikazana vezanom listom elija, s time da su veze meu elijama uspostavljane kursorima a ne pokazivaima. Vezana lista je graena isto kao i prije, dakle postoji header. Lista se poistovjeuje s kursorom na header. Sve liste s kojima radimo troe elije iz istog (jedinstvenog) polja SPACE. Npr. za maxlength = 12 i za liste L = (a, b, c), M = (d, c), prikaz bi mogao izgledati kao na slici 2.4.
SPACE element next . 6 0 3 1 9 2 b 5 3 4 5 6 available 8 7 8 9 10 11 c c d a 7 -1 10 2 11 -1 -1 1

Slika 2.4: Prikaz implementacije vezane liste koritenjem kursora. Sivom bojom oznaeni su headeri Sve slobodne elije koje nisu dio ni jedne liste povezali smo u dodatnu vezanu listu (bez headera) koju zovemo available. Kad god nam treba nova elija, uzmemo je sa poetka vezane liste available (simulacija koritenja new). Tipovi LIST i position iz ATP LIST de nirani su ovako:
typedef int LIST; typedef int position;

Da bi dovrili implementaciju, treba jo napisati potprograme koji realiziraju operacije iz ATP LIST. U tu svrhu je dovoljno prepisati potprograme iz implementacije pomou pokazivaa, tako da pokazivae zamijenimo kursorima. To emo napraviti za funkciju INSERT, dok bi za preostale funkcije postupak bio slian. 15

void INSERT(elementtype x, position p) { position temp; if (available == -1) printf(Nema slobodnog mjesta.); else { temp = SPACE[p].next; SPACE[p].next = available; available = SPACE[available].next; SPACE[SPACE[p].next].element = x; SPACE[SPACE[p].next].next = temp; } }

Ovaj zadatak ilustrira openitu ideju kako se bilo koja implementacija temeljena na pokazivaima moe preraditi u implementaciju temeljenu na kursorima. U ovom kolegiju emo izloiti mnogo struktura podataka sa pokazivaima. Iako to ne naglaavamo, podrazumijevamo da se iste strukture mogu ostvariti i kursorima.

2.2

Stog

Stog je posebna, jednostavnija vrsta liste. Jedan kraj liste nazivamo vrh stoga. Osim funkcije za pretvaranje stoga u prazni, od operacija nad listama dozvoljene su samo ubacivanje i brisanje elemenata na vrhu stoga. itanje sadraja elemenata stoga je takoer dozvoljeno samo na vrhu. a6 Ubacivanje Izbacivanje

. Vrh stoga a.5 a.4 a.3 a.2 a.1 Stog .

a.6 a.5 a.4 a.3 a.2 a.1 Stog

Slika 2.5: Skica operacija ubacivanja i izbacivanja elementa u tipu podataka stog Moemo zamisliti da imamo knjige naslagane jednu na drugu u nekoj kutiji u svakom trenutku vidimo samo onu knjigu koja je na vrhu stoga i samo nju moemo uzeti i maknuti van, te samo na nju moemo poloiti novu knjigu. Stog se jo naziva i LIFO struktura (last-in- rst-out ). Navodimo lanove ATP STOG: 16

PUSH(1,&S)

. 1

PUSH(2,&S)

. 2. 1 S

POP(&S)

. 1 S

PUSH(3,&S)

. 3 1 S

Slika 2.6: Primjer koritenja funkcija PUSH i POP elementtype bilo koji tip STACK konaan niz podataka tipa elementtype MAKE_NULL(STACK &S) pretvara S u prazni stog EMPTY(STACK S) logika funkcija koja vraa istinu ako je stog prazan, a inae la PUSH(elementtype x, STACK &S) ubacuje x na vrh stoga S POP(STACK &S) uklanja element na vrhu stoga TOP(STACK S) vraa element sa vrha stoga (element ostaje i dalje na vrhu stoga) Primjer jedne uporabe stoga je implementacija funkcijskih poziva u programskim jezicima. Ugrubo, kada se izvrava neki program, program izvrava naredbu po naredbu koristei pritom programsko brojilo koje sadri adrese naredbi i poveava se nakon svake izvrene naredbe. Pri pozivu neke funkcije adresa trenutne naredbe i poslani parametri stavljaju se na stog ako se unutar pozvane funkcije ponovno pozove neka druga funkcija, podaci se ponovno stavljaju na stog. Funkcije se onda izvravaju oekivanim redoslijedom (prvo posljednja pozvana, pa pretposljednja i tako dalje). Uporaba stoga nam omoguava simulaciju svakog rekurzivnog programa nerekurzivnim. Primjer: Neki kalkulatori omoguavaju unos u post x obliku (poznatom i kao obrnuti poljski zapis, reverse Polish notation ili RPN). U takvom zapisu unosi se potrebni broj operanada, te potom operator. Time se izbjegava uporaba zagrada. Na primjer izraz (3+4)*2 se moe zapisati kao 34+2*. Zadatak 2.2.1. Opiite evaluaciju post x izraza na stogu. Prikaite sve korake na primjeru izraunavanja (A/(B C )) D + E . Izraz prvo prebacite u post x. Rjeenje: Prvo prebacujemo izraz u post x zapis: (A/(B C )) D + E ABC /D E +. Potom zapisujemo algoritam za izraunavanje izraza:
za svaki znak ch postfix izraza (redom) { ako je ch operand // tj. ako je ch broj stavi ch na stog inae {

17

uzmi broj sa vrha uzmi broj sa vrha izvri operaciju ch rezultat stavi na

stoga i spremi ga u b1 stoga i spremi ga u b2 nad b2 i b1 // ch(b2,b1) stog

} } uzmi broj sa vrha stoga - to je rezultat

Postupak se moe vidjeti na skici 2.7. Ostaje pitanje automatizacije postupka prevoenja izraza iz in x u post x zapis. Jedan takav postupak poznat je pod imenom Dijkstrin algoritam . Mogui problemi u prevoenju su: Prioritet operatora Primjerice izraz 1 + 2 3 nije 12 + 3 ve 123 + Zagrade Njih moemo lako rijeiti rekurzivno (izraz unutar zagrada tretiramo kao potpuno novi izraz koji rjeavamo ispoetka i tako za svaku razinu ugnjeivanja) Zapiimo pojednostavljenu inaicu Dijkstrinog algoritma: 1. uitaj sljedei znak in x ulaza 2. ako je znak broj, ispii ga 3. ako je znak operator tada: (a) sve dok stog nije prazan i operator na vrhu stoga ima vei ili jednak prioritet od uitanog znaka, ispii i ukloni operator na vrhu stoga (pretpostavimo ( ima najmanji prioritet) (b) stavi znak na stog 4. ako je znak (, stavi na stog 5. ako je znak ), ispisuj i uklanjaj sve operatore sa vrha stoga sve dok ne naie na (; ( ukloni bez ispisivanja 6. ako preostaje jo znakova na ulazu, vrati se na korak 1 7. ako nema vie znakova na ulazu ispii i uklanjaj ostatak stoga Zadatak 2.2.2. Dijkstrinim algoritmom prebacite A+B*C+(D+E)*F u post x. Rjeenje: Kako bismo dobili izraz u post x obliku, provodimo opisani postupak. Provoenje postupka vidljivo je na slici 2.8. Napomena: U literaturi postoji vie Dijkstrinih algoritama. Gore opisani algoritam se naziva jo i Shunting-yard algoritam. Najuobiajenije se Dijsktrinim algoritmom naziva onaj za pronalaenje minimalnog razapinjueg stabla u grafu.
Edsger Wybe Dijkstra (19302002)

18

trenutna pozicija ABC/D*E+ ABC/D*E+

operacije koje radimo PUSH(A,*S) PUSH(B,*S)

stog S A B A C B A A

ABC/D*E+

PUSH(C,*S)

ABC/D*E+

t1 = TOP(S); POP(*S); t2 = TOP(S); POP(*S); = t2 t1; PUSH(,*S); t1 = TOP(S); POP(*S); t2 = TOP(S); POP(*S); = t2 / t1; PUSH( ,*S); PUSH(D,*S) t1 = TOP(S); POP(*S); t2 = TOP(S); POP(*S); = t2 t1; PUSH( ,*S); PUSH(E,*S) t1 = TOP(S); POP(*S); t2 = TOP(S); POP(*S); = t2 + t1; PUSH( ,*S);

ABC/D*E+

ABC/D*E+ ABC/D*E+

ABC/D*E+ ABC/D*E+

Slika 2.7: Skica postupka raunanja izraza u post x zapisu

19

A+B*C+(D+E)*F A+B*C+(D+E)*F A+B*C+(D+E)*F A+B*C+(D+E)*F A+B*C+(D+E)*F A+B*C+(D+E)*F A+B*C+(D+E)*F A+B*C+(D+E)*F A+B*C+(D+E)*F A+B*C+(D+E)*F A+B*C+(D+E)*F A+B*C+(D+E)*F A+B*C+(D+E)*F

A je operand pa ga ispisujemo + je operator pa ga stavljamo na stog B je operand pa ga ispisujemo * je operator veeg prioriteta od vrha stoga pa ga stavljamo na stog C je operand pa ga ispisujemo + je operator pa ispisujemo sve operatore veeg ili jednakog prioriteta ( je najmanjeg prioriteta pa ju stavljamo na stog D je operand pa ga ispisujemo + je operator veeg prioriteta pa ga piemo na stog E je operand pa ga ispisujemo ) je desna zagrada ispisujemo i izbacujemo sa vrha stoga do ( * je operator, a stog je prazan stavljamo na stog F je operand, ispisujemo ga ispisujemo i izbacujemo operatore sa vrha stoga
B B * C C * + + *+ ( D D ( + + + ( + E E + ( + ) + * F F * + *+

ulaz izlaz stog

A A

* +

( +

* +

Slika 2.8: Prikaz prevoenja izraza iz in x u post x oblik Zadatak 2.2.3. Problem Hanojskih tornjeva glasi ovako: Dana su 3 tapa A, B i C. Na tap A je nanizano n diskova razliitog promjera. Potrebno je prebaciti sve diskove sa tapa A na tap C koristei B kao pomo. Odjednom je mogue prebaciti samo 1 disk; ne smije se stavljati vei disk na manji. Nedostaje: skica Hanojski tornjevi, vidite npr. ovu sliku Napiite rekurzivni program koji rjeava ovaj problem. Neka move(x,y) oznaava premjetanje jednog diska sa tapa x na tap y.

Slika 2.9: Primjer Hanojskih tornjeva za n = 3 Rjeenje: to smo zapravo radili u primjeru sa n = 3? Premjestili smo gornja 2 diska (n 1 diskova) sa tapa A na B koristei C. Potom smo prebacili najvei disk sa tapa A na tap C. Na kraju smo prebaena dva diska sa tapa B prebacili na tap C koristei tap A. To nas navodi na ideju indukcije: 20

baza Za n = 1, rjeenje je trivijalno jednostavno prebacimo jedini disk na bilo koji tap korak Zamislimo da znamo prebacivati sve hrpe od n 1 diskova. Tada za n diskova konstruiramo algoritam: premjestimo gornjih n 1 diskova sa A na B koristei C prebacimo najvei disk sa A na C premjestimo n 1 diskova sa B na C koristei A Zapiimo ovo u programskom kdu:
void HANOI ( int n, char polazni , char pomocni , char zavrsni) { if (n == 1) { move(polazni ,zavrsni); return ; } HANOI(n-1,polazni , zavrsni , pomocni); move(polazni ,zavrsni); HANOI(n-1,pomocni ,polazni ,zavrsni); }

Koliko koraka ovo traje? Za premjetanje jednog diska trebamo jedan potez, pa je potrebno vrijeme jedan. Za premjetanje n diskova trebamo premjestiti hrpu od n 1 diskova, pa najvei disk, pa opet hrpu od n 1 diskova ukupno 2 puta vrijeme od n 1 premjetanja i jo jedan za najvei disk. } T (1) = 1 T (n) = 2n 1 T (n) = 2 T (n 1) + 1 O nainu raunanja rekurzija trebali biste uti na kolegiju Diskretna matematika. Na mrei su dostupni skenovi predavanja kolegija Sloenost algoritama koja detaljnije raspisuju ovo rjeenje (Saa Singer: Uvod u sloenost). Zadatak 2.2.4. Napiite nerekurzivnu verziju funkcije HANOI Rjeenje: Rekurzivne pozive simuliramo pomou stoga (i to neovisno o implementaciji). U tu svrhu je potrebno na stog spremiti trenutni opis situacije prije ulaska u rekurziju koliki je n, koji je tap poetni, pomoni,... Potrebno je znati i u kojoj smo fazi (1)-(5) ovime zapravno simuliramo adresu instrukcije na koju se vraamo nakon funkcijskog poziva. Potrebno je de nirati to je elementtype koji sprema trenutnu situaciju.
typedef struct { int n, faza; // faza simulira adresu sljedee po redu naredbe char pocetni , pomocni , zavrsni; } elementtype;

21

Nakon toga moemo zapisati nau funkciju.


void HANOI_STACK( int n, int pocetni , int pomocni , int zavrsni) { elementtype x; STACK S; MAKE_NULL(&S); f1: if (n == 1) { move(pocetni ,zavrsni); goto f5; } f2: // spremamo trenutnu situaciju na stog x.n = n; x.pocetni = pocetni; x.pomocni = pomocni; x.zavrsni = zavrsni; x.faza = 3; PUSH(x,&S); // za novi poziv funkcije n = x.n - 1; pocetni = x.pocetni; pomocni = x.zavrsni; zavrsni = x.pomocni; goto f1; // simuliramo poziv f3: move(pocetni ,zavrsni); f4: // spremamo trenutnu situaciju na stog x.n = n; x.pocetni = pocetni; x.pomocni = pomocni; x.zavrsni = zavrsni; x.faza = 5; PUSH(x,&S); n = x.n - 1; pocetni = x.pomocni; zavrsni = x.zavrsni; pomocni = x.pocetni; goto f1; f5: if (EMPTY(S)) return ; else { /* gotovi smo s jednim pozivom - treba napraviti return to znai vratiti prethodno stanje koje je na stogu */ x = TOP(S); POP(&S); n = x.n; pocetni = x.pocetni; pomocni = x.pomocni; zavrsni = x.zavrsni; switch (x.faza) { case 3: goto f3; break ; case 5: goto f5; break ; } } }

Zadatak 2.2.5 (**). Uitajte brojeve n, n1 , n2 i n3 . n je ukupan broj diskova, n1 je broj diskova na prvom tapu, n2 je broj diskova na drugom tapu, n3 na treem tapu. Uitajte 22

n1 brojeva veliine diskova na prvom tapu, a isto i za drugi i trei tap. Odredite koliko minimalno prebacivanja treba napraviti iz ove pozicije do pozicije u kojoj su svi diskovi na nekom (bilo kojem) tapu. Ispiite koji je to tap i koliko poteza treba (modulo 1000000). Neka je n 100000. Zadatak 2.2.6. Razradite implementaciju stoga pomou pokazivaa. Ispiite potrebne denicije i funkcije. Rjeenje: Zapisujemo potrebne strukture i funkcije u programskom jeziku.
typedef struct celija { elementtype element; struct celija* next; } celltype; typedef celltype* STACK; void MAKE_NULL(STACK* S) { *S = null; } bool EMPTY(STACK S) { if (S == null) return true; else return false; } void PUSH(elementtype x, STACK *S) { celltype *temp; temp = (celltype*) malloc( sizeof (celltype)); temp->element = x; temp->next = S; *S = temp; } elementtype TOP(STACK S) { if (EMPTY(S)) printf(Stog je prazan!\n); else return S->element; } void POP(STACK *S) { celltype *temp; if (EMPTY(*S)) printf(Stog je prazan!\n); else {

23

temp = S->next; free(S); S = temp; } }

Napomena: Mogli smo iskoristiti ranije izvedenu implementaciju ATP LIST pomou pokazivaa i trivijalno napisati funkcije za ATP STACK. No to je nee kasno i komplicirano jer je stog jednostavnija struktura.

2.3

Red

Red (engl. queue) je specijalna vrsta liste. Od svih operacija nad listama dozvoljene su samo: Ubacivanje elemenata na jedan kraj liste (zaelje, kraj liste) Uklanjanje elemenata sa drugog kraja liste (elo, poetak liste) Pregled sadraja elementa samo na poetku liste Intuicija za ovaj apstraktni tip podataka je ekanje u bilo kakvom redu. Primjer: Slubenik u banci obrauje klijente ispred njega je red klijenata koji ekaju. U svakom trenutku slubenik radi samo s klijentom koji je na poetku reda; on odlazi iz reda kada ga slubenik obradi. Kad dolazi posve novi klijent, staje na kraj reda. Raunalni sustavi meusobno alju poruke. Obrada vremena traje neko vrijeme. Ako u meuvremenu stignu druge poruke, stavljaju se u red ekanja. Zbog svojih svojstava red jo nazivamo i FIFO ( rst-in- rst-out ) struktura. Navodimo lanove apstraktnog tipa podataka red. elementtype bilo koji tip QUEUE konaan niz podataka tipa elementtype MAKE_NULL(QUEUE &Q) pretvara Q u prazni red bool EMPTY(STACK S) logika funkcija koja vraa istinu ako je red prazan, a inae la ENQUEUE(elementtype x, QUEUE &Q) ubacuje x na zaelje reda Q DEQUEUE(QUEUE &Q) uklanja element s ela reda Q; nije de nirano ako je red prazan (EMPTY(Q) == TRUE) elementtype FRONT(QUEUE Q) vraa vrijednost elementa na elu reda Q; nije denirano ako je red prazan

24

ENQ(2,&S)

2.

ENQ(5,&S)

. 25

ENQ(7,&S)

. 257

DEQ(&S)

. 57

Slika 2.10: Primjer koritenja funkcija ENQUEUE i DEQUEUE Red se primjenjuje u takozvanim Breadth-First-Search algoritmima (BFS). Kod tih se algoritama zadatak rjeava razlaganjem na manje podzadatke, podzadaci na jo manje podzadatke, i tako dalje. Algoritam se sastoji od formiranja reda podzadataka koje jo treba rjeiti. Program skida podzadatak sa ela i rjeava ga; ako se jave novi podzadaci oni se stavljaju na zaelje reda. Problem je rjeen kad se red isprazni.
QUEUE Q; MAKE_NULL(&Q); ENQUEUE(Z,&Q); // Z oznaava poetni zadatak while (!EMPTY(Q)) { elementtype P = FRONT(Q); DEQUEUE(&Q); za svaki podproblem PP od P radi ENQUEUE(PP, &Q); }

Slika 2.11: Skica stabla podzadataka Zadatak 2.3.1. Dan je orijentirani graf G iji vorovi su numerirani sa 0, 1, . . . , n 1. Na primjer: Napiite funkciju koja ispisuje skup dostupnih vrhova Si za dani vor i, to jest sve vorove u koje se moe doi iz vora nekim putem u G. Pretpostavite da je graf zadan poljem redova G koji predstavljaju popise susjeda pojedinih vorova. Na primjer: G[0] = (1, 2) G[1] = (5) G[2] = (1, 3) G[3] = () . . . 25

. 0

Slika 2.12: Skup dostupnih puteva za vrh 0 je S0 = 0, 1, 2, 3, 5 Rjeenje: Ideja algoritma: vorovi dostupni iz vora i su svi susjedi od i, svi susjedi od susjeda od i,... Si je znai sm i, te unija skupova dostupnih od svih susjeda od i. Slijedimo jednak postupak kao u openitom BFS algoritmu: stavimo i u red Q sve dok se Q ne isprazni: uzmemo vor s poetka reda Q i ispiemo ga (ako ve nije ispisan). na Q stavimo sve susjede od tog vora koji ve nisu bili u redu Q Stanje reda za gornji primjer

Slika 2.13: Skica stanja reda


#define N 1000 void accessible (QUEUE G[], int pocetni) { bool posjeceno[N]; int i; QUEUE Q; for (i = 0; i < N; i++) posjeceno[i] = 0; ENQUEUE(pocetni , &Q); posjeceno[pocetni] = true; while (!EMPTY(Q)) { int vrh = FIRST(Q); printf(%d, vrh); while (!EMPTY(G[vrh])) {

26

int susjed = FIRST(G[vrh]); DEQUEUE(&G[vrh]); if (!posjeceno[susjed]) { ENQUEUE(susjed ,&Q); posjeceno[susjed] = true; } } DEQUEUE(&Q); } }

Napomena: Moe li se ovaj zadatak rijeiti pomou stoga? Kojim se redoslijedom u tom sluaju obilaze vrhovi? Zadatak 2.3.2. Napiite funkciju int distance(QUEUE G[], int start, int cilj) koja vraa udaljenost izmeu vrhova start i cilj. Uputa: malo izmjenite rjeenje prethodnog zadatka. Napomena: Na predavanjima smo promatrali implementaciju reda pomou cirkularnog polja.

Slika 2.14: Skica cirkularnog polja temeljena je na strukturi podataka koju ine (cirkularno) polje i kursori na elo i zaelje reda polje se ne smije do kraja napuniti jer u protivnom ne bismo mogli razlikovati puni red od praznog alternativno, moemo dozvoliti da se red napuni do kraja, ali onda moramo imati jo jednu boolean eliju koja e oznaavati je li red prazan Zadatak 2.3.3. Razradite implementaciju reda pomou cirkularnog polja, kursora na elo i zaelje, te elije koja oznaava je li red prazan ili ne. Rjeenje: Zapisujemo kd rjeenja: 27

#define maxlength ... typedef struct { elementtype elements[maxlength]; int front, rear; bool is_empty; } QUEUE; bool EMPTY(QUEUE Q) { return Q.is_empty; } elementtype FRONT(QUEUE Q) { if (EMPTY(Q)) printf(Red je prazan!\n); else return Q.elements[Q.front]; } void MAKE_NULL(QUEUE *Q) { Q->front = 0; Q->rear = maxlength - 1; Q->is_empty = 1; } int add_one( int x) { return (x+1)%maxlength; } void DEQUEUE(QUEUE *Q) { if (EMPTY((*Q)) printf(Red je prazan!\n); else { Q->front = add_one(Q->front); if (add_one(Q->rear) == Q->front) Q->is_empty = true; } } void ENQUEUE(elementtype x, QUEUE *Q) { if (!EMPTY(*Q) && add_one(Q->rear) == Q->front) printf(Red je pun!\n); else { Q->rear = add_one(Q->rear); Q->elements[Q->rear] = x; Q->is_empty = 0;

28

} }

Napomena: Slanje tipa QUEUE bez pokazivaa (to jest, slanje po vrijednosti) u funkcijama EMPTY i FRONT izrazito je nee kasno zbog kopiranja cijelog polja. Nedostaje: dodatne napomene Zadatak 2.3.4 (zadaci za vjebu za prvi kolokvij, zadatak 11.). Napiite funkciju void queuesort(QUEUE *Q) koja prima red Q iji su elementi cijeli brojevi. Funkcija treba sortirati elemente reda Q od najveeg prema najmanjem. Na primjer, ako je na ulazu Q = 3, 1, 4, 1, 2, 6, onda na izlazu treba biti Q = 6, 4, 3, 2, 1, 1. Dozvoljeno je koritenje samo jo jednog pomonog stoga. Nije dozvoljeno koritenje drugih ATP ni polja. Funkcija treba biti napisana neovisno o implementaciji ATP STACK i QUEUE. Rjeenje: Nedostaje: objanjenje ideje rjeenja
void queuesort(QUEUE *Q) { STACK S; int N = 0, x,y,i,j; MAKE_NULL(&S); // prebroji koliko ima elemenata u Q while (!EMPTY(*Q)) { x = FIRST(*Q); DEQUEUE(Q); N++; PUSH(x,&S); } // vrati elemente iz stoga u red for (i = 0; i < N; i++) { x = TOP(S); POP(&S); ENQUEUE(x,Q); } // N puta stavi najmanji element sa Q na S for (i = 0; i < N; i++) { x = FIRST(*Q); DEQUEUE(Q); PUSH(x, &S);

29

for (j = 0; j < N-i-1; j++) { x = FIRST( *Q ); DEQUEUE( Q ); if (TOP(S) > x) { y = TOP(S); POP(&S); ENQUEUE(y, Q); PUSH(x, &S); } else ENQUEUE(x, Q); } } // kopiraj natrag S na Q for (i = 0; i < N; i++) { x = TOP(S); POP(&S); ENQUEUE(x,Q); } }

30

Poglavlje 3 Stabla
3.1 (Ureeno) stablo

31

32

33

34

35

3.2

Binarno stablo

De nicija 3.2.1. Binarno stablo T je konaan skup podataka istog tipa (vorova) tako da vrijedi: 1. T je prazno ili 2. T ima istaknuti vor (koji nazivamo korijen), a ostali vorovi su podijeljeni u 2 podskupa TL i TR (koje nazivamo lijevo odnosno desno podstablo) koji opet imaju strukturu binarnog stabla. Terminologija je slina kao kod ureenih stabala. Razlika je u tome to kod ureenih stabala razlikujemo lijevo i desno dijete i u situacijama u kojima postoji vor ima samo jedno dijete. Kod ureenih stabala vaan je samo poredak djece nekog roditelja, a ne i pozicija pojedinog djeteta. Kod binarnog stabla razlikujemo lijevo i desno dijete, bez obzira na to je li jedino dijete nekog roditelja. Navodimo lanove apstraktnog tipa podataka binarno stablo (ili BTREE): node bilo koji tip labeltype oznaka vora koja nosi korisnu informaciju LAMBDA oznaka za prazan vor BTREE binarno stablo void MAKE_NULL(BTREE *T) pretvara T u prazno binarno stablo bool EMPTY(BTREE T) logika funkcija koja vraa istinu ako je binarno stablo prazno, a inae la node CREATE(labeltype l, BTREE T_L, BTREE T_R, BTREE *T) uQ LEFT_SUBTREE(BTREE T, BTREE *T_L) nakon poziva, parametar TL pokazuje na lijevo podstablo RIGHT_SUBTREE(BTREE T, BTREE *T_R) nakon poziva, parametar TR pokazuje na desno podstablo node LEFT_CHILD(node i, BTREE T) vraa ime lijevog djeteta vora i node RIGHT_CHILD(node i, BTREE T) vraa ime desnog djeteta vora i node INSERT_LEFT_CHILD(labeltype l, node i, BTREE *T) dodaje novi vor sa oznakom l tako da on bude lijevo dijete vora i vraa ime vora node INSERT_RIGHT_CHILD(labeltype l, node i, BTREE *T) dodaje novi vor sa oznakom l tako da on bude desno dijete vora i vraa ime vora DELETE(node i, BTREE *T) brie vor i iz drva T node ROOT (BTREE T) vraa ime korijena drveta T 36

node PARENT (node i, BTREE T) vraa roditelja vora i u stablu T labeltype LABEL(node i, BTREE T) vraa oznaku vora i u binarnom stablu T void CHANGE_LABEL(labeltype l, node i, BTREE *T) postavlja oznaku vora i na l u stablu T Zadatak 3.2.1. Napiite funkciju int VISINA(BTREE T) koja vraa visinu binarnog stabla T. Pokuajte napisati funkciju sa i bez koritenja funkcija LEFT_SUBTREE i RIGHT_SUBTREE. Rjeenje: Prvo emo zapisati rjeenje koritenjem LEFT_SUBTREE i RIGHT_SUBTREE funkcija, a potom bez koritenja. pomou SUBTREE funkcija U ovom sluaju rekurzivno prolazimo kroz sve manja stabla i vraamo visinu uveanu za 1.
int VISINA(BTREE T) { if (EMPTY(T)) return -1; else { BTREE TL, TR; LEFT_SUBTREE( T, &TL ); RIGHT_SUBTREE( T, &TR ); return 1 + max(VISINA(TL), VISINA(TR)); // uz pretpostavku da smo je funkcija max(a,b) definirana } }

bez koritenja SUBTREE funkcija Kako bismo osigurali rekurzivnost po vorovima umjesto po stablima, poetni poziv samo poziva rekurzivnu funkciju koja kao argument prima vor.
int VISINA(BTREE T) { return vis(ROOT(T), T); } int vis(node n, BTREE T) // korijen vor n { if (n == LAMBDA) return -1; else { int vL, vR; vraa visinu podstabla iji je

37

vL = vis(LEFT_CHILD(n, T), T); vR = vis(RIGHT_CHILD(n, T), T); return 1 + max(vL, vR); } }

Zadatak 3.2.2. Napiite funkciju int BRAT(BTREE B) koja vraa broj vorova koji nisu listovi i iji brat ima 1 dijete, te koji imaju 5 ili vie potomaka. Rjeenje: Pokuajmo si pomoi razbijanjem traenih uvjeta na vie manjih. Za takve manje uvjete piemo manje funkcije:
int jeList(node n, BTREE B) // pretpostavimo da n nije LAMBDA { if (LEFT_CHILD(n, B) == LAMBDA && RIGHT_CHILD(n, B) == LAMBDA) return 1; else return 0; } int bratOK(node n, BTREE B) { node roditelj , brat; if (n == ROOT(B)) // korijen nema return 0; roditelj if ( n == brat else brat brae

= PARENT(n, B); LEFT_CHILD(n, roditelj)) = RIGHT_CHILD(n, roditelj); = LEFT_CHILD(n, roditelj);

if (brat != LAMBDA) { int brojDjece = (LEFT_CHILD(brat, B) != LAMBDA) + (RIGHT_CHILD(brat, B) != LAMBDA); return brojDjece == 1; } else return 0; } int brojPotomaka(node n, BTREE B) { int lijevo = 0, desno = 0;

38

if (n == LAMBDA) return 0; if (LEFT_CHILD(n, B) != LAMBDA) lijevo = 1 + brojPotomaka(LEFT_CHILD(n, B), B); if (RIGHT_CHILD(n, B) != LAMBDA) desno = 1 + brojPotomaka(RIGHT_CHILD(n, B), B); return lijevo + desno; }

Kao i u prolom zadatku jer prototip funkcije zahtijeva binarno stablo kao argument, piemo rekurzivnu funkciju koja umjesto kopija cijelog stabla koristi vorove za prolazak po stablu. Stoga u poetnu funkciju dodajemo samo poziv na takvu novostvorenu funkciju.
int BRAT(BTREE B) { return prebroji(ROOT(B), B); } int prebroji(node n, BTREE B) { int nJeTakav = 0; if (n == LAMBDA) return 0; if (!jeList(n, B) && bratOK(n, B) && brojPotomaka(n, B) >= 5) nJeTakav = 1; return nJeTakav + prebroji(LEFT_CHILD(n, B), B) + prebroji(RIGHT_CHILD(n, B), B); }

Zadatak 3.2.3. Napiite funkciju LABEL MAXBRAT(BTREE B) koja vraa najvei label meu svim vorovima iz prethodne funkcije. Rjeenje: Za rjeenje ovog zadatka iskoristit emo prethodno napisane funkcije. Novost je koritenje vanjskih spremita max i nadjen kojim moe pristupiti svaki rekurzivni poziv funkcije jer se u pozive prosljeuje adresa spremita.
labeltype MAXBRAT(BTREE B) { labeltype maxi; int nadjen = 0; obidjiStablo( ROOT(B), B, &max, &nadjen); if (!nadjen) printf(Nema takvog...);

39

return max; } void obidjiStablo(node n, BTREE B, labeltype *maxi, int *nadjen) { if (n == LAMBDA) return ; if (!jeList(n, B) && bratOK(n, B) && brojPotomaka(n, B) >= 5) if (*nadjen == 0 || *max < LABEL(n, B)) { *nadjen = 1; *max = LABEL(n, B); } obidjiStablo(LEFT_CHILD(n, B), B, max, nadjen); obidjiStablo(RIGHT_CHILD(n, B), B, max, nadjen); }

Zadatak 3.2.4 (Svojstva binarnih stabala). Dokaite sljedee tvrdnje: 1. Maksimalni broj vorova na nivou i binarnog stabla je 2i 2. Maksimalni ukupan broj vorova u binarnom stablu visine v je 2v+1 1 3. Neka je n0 broj listova, a n2 broj vorova s 2 djece. Dokaite da je n0 = n2 + 1 4. Visina binarnog stabla sa n vorova je vea ili jednaka od log2 (n + 1) 1 5. Visina binarnog stabla sa n0 listova je vea ili jednaka od log2 n0 Rjeenje: Dokazujemo tvrdnje redom: 1. Prvu tvrdnju moemo dokazati indukcijom po nivou binarnog stabla i. Na nultom nivou nalazi se samo korijen, a za i = 0 imamo 2i = 20 = 1. Pretpostavimo da na nivou i 1 imamo najvie 2i1 vorova. Jer u binarnom stablu svaki vor moe imati maksimalno 2 djeteta, na nivou i imamo maksimalno 2i1 2 = 2i vorova. 2. Prema 1, maksimalni broj vorova na nivou i je 2i . Ukupan broj vorova u binarnom stablu je zbroj vorova po svim nivoima stabla, je u binarnom stablu visine pa i=v i 0 1 v v maksimalni broj vorova 2 + 2 + . . . + 2 = i=0 2 = 2k+1 1. 3. Oznaimo sa n1 broj vorova sa 1 djetetom. vorovi mogu biti listovi (bez djece), imati jedno ili dva djeteta. Ukupan broj vorova u binarnom stablu je zato n = n0 + n1 + n2 . Do svakog vora osim korijena vodi tono jedan brid od roditelja, pa je broj bridova je b = n 1 = n0 + n1 + n2 1. S druge strane, iz vorova izlazi onoliko bridova koliko imaju djece, pa je broj bridova b = n1 + 2n2 . Konano, iz ovih identiteta dobivamo n0 + n1 + n2 1 = n1 +2n2 n0 = n2 +1. 40

. Slika 3.1: Dodavanje vorova kako bi popunili stablo 4. Neka je v visina nekog binarnog stabla Prema 2 je n 2v+1 1. Slijedimo niz nejednakosti: n 2v+1 1 n + 1 2v+1 / log2 log2 (n + 1) v + 1 v log2 (n + 1) 1 Na kraju, jer je v cijeli broj vrijedi v log2 (n + 1) 1. 5. Neka je T binarno stablo sa n0 listova visine vT . Ako u T postoji neki list (ili vor sa samo jednim djetetom) na nivou manjem od vT , stvorimo novo stablo T1 tako da tom listu dodamo dva djeteta odnosno preostalo dijete, kao na slici 3.1. Postupak ponavljamo sve dok moemo. Time dobivamo konani niz stabala T, T1 , . . . , Tk . Visina zadnjeg (potpunog) stabla je jednaka visini polaznoga, vTk = vT , jer nismo dodavali djecu u posljednjem nivou. Takoer, jer smo u svakom koraku samo dodavali djecu vrijedi n0 (Tk ) n0 (T ). Stablo Tk je potpuno pa posebno ima sve listove na posljednjem nivou vT . Prema 1 vrijedi: n0 (T ) n0 (Tk ) 2vTk = 2vT / log2 vt log2 (n0 ) Ponovno, jer je vT cijeli broj slijedi vt log2 (n0 ) Zadatak 3.2.5. Binarno stablo na slici 3.2 opisuje grau aritmetikog izraza. Ispiite inorder, preorder i postorder obilaske sa stabla na slici. Rjeenje: Traeni ispisi obilazaka prikazani su u tablici 3.1. to primjeujete pri postorPreorder: + Inorder: 32 Postorder: 32 * / 2 / 32 2 4 3 5 2 4 * 3 + 5 4 / 3 * 5 +

Tablica 3.1: Ispisi obilazaka stabla iz zadatka 3.2.5 der obilasku? Postorder obilazak stabla aritmetikog izraza daje zapis izraza u post x obliku. Zadatak 3.2.6. Dokaite da je binarno stablo u kojem svi vorovi uvaju meusobno razliite informacije jednoznano odreeno preorder i inorder obilascima. 41

. + / 32 2 4 3 5

Slika 3.2: Stablo aritmetikog izraza Rjeenje: Opisat emo algoritam (rekurzivni, induktivni) iz kojega e ovo biti oito. Prisjetimo se prvo redoslijeda obilaenja pri traenim obilascima: preorder obilazak rekurzivno posjeuje korijen, pa lijevo stablo L i na kraju desno stablo D. Inorder obilazak obilazi redom L korijen D. Ideja rjeenja je pokazati da moemo jednoznano odrediti koji vor je korijen, te koja su preostala podstabla L i D. Potom ponavljamo postupak odreivanja korijena dok ne odredimo cijelo stablo. Naime, zapis preorder obilaska nam jednoznano odreuje to je korijen stabla prvi element u obilasku. Nakon to smo odredili korijen, jer su svi vorovi meusobno razliiti po pretpostavci, moemo ga pronai i inorder obilasku. Time smo odredili preostala stabla, jer korijen razdvaja lijevo i desno podstablo u zapisu inorder obilaska. Zapiimo ovu ideju u obliku matematike indukcije po broju vorova u stablu: baza Kada je u stablu samo n = 1 vor, oba zapisa su jednaka i oba predstavljaju korijen. pretpostavka Pretpostavimo da su sva binarna stabla koja imaju manje od n vorova jednoznano odreena inorder i preorder obilaskom. korak Neka je zadano neko stablo T sa n vorova, te zapisi preorder obilaska a1 , . . . , an i inorder obilaska b1 , . . . , bn . Prvi lan u zapisu preorder obilaska a1 je korijen stabla. Svi vorovi uvaju meusobno razliite informacije pa moemo pronai i takav da vrijedi bi = a1 . Prema de niciji inorder obilaska, b1 , . . . , bi1 je zapis L a2 . . . ai+1 L b1 . . . bi1 bi D ai+2 . . . an D bi+1 . . . bn

a.1

Slika 3.3: Skica razdvajanja stabala na korijen, lijevo i desno podstablo lijevog podstabla, a bi+1 , . . . , bn je zapis desnog podstabla (skica ovog koraka moe 42

se vidjeti na slici 3.3). Primjetimo da su ti zapisi opet u inorder obliku. Time smo i u preorder zapisu odredili lijevo i desno podstablo. Odreena podstabla imaju manje od n vorova jer ne sadravaju korijen, pa su po pretpostavci indukcije jedinstveno odreena. Zadatak 3.2.7. Razmislite i dokaite moemo li jednoznano odrediti binarna stabla ako imamo zadane druge obilaske? preorder i postorder inorder i postorder Zadatak 3.2.8. Reproducirajte binarno stablo iz zapisa obilazaka: preorder ABDEFHJKLCGI inorder EDFKJLHBAGIC Rjeenje: Na slici 3.4 vidljiv je postupak rjeavanja i konano rjeenje.

Slika 3.4: Skica rjeavanja De nicija 3.2.2. Potpuno binarno stablo je binarno stablo ijim se vorovima mogu dati imena 0, 1, . . . , N tako da vrijedi: 1. lijevo dijete vora i ima ima 2i + 1 (osim u sluaju 2i + 1 > N u tom sluaju ga nema) 2. desno dijete vora i ima ime 2i + 2 (ako 2i + 2 > N , onda nema) Napomena: Postoji jednostavna implementacija za binarno stablo. U polju dimenzije 2n 1 moemo prikazati sva potpuna binarna stabla sa visinom manjom ili jednakom n. Ovaj prikaz moe posluiti i za obina (nepotpuna) binarna stabla, no puno indeksa moe ostati neiskoriteno. Zadatak 3.2.9. Razradite implementaciju binarnog stabla pomou polja i prikaite ga u polju. 43

Rjeenje: Dopunimo oznake lanim vorovima u koje napiemo primjerice - i prikaemo stablo kao u de niciji potpunog binarnog stabla. U tu svrhu prvo de niramo lanove ATP BTREE.
#define maxlength ... typedef int node; typedef struct { labeltype labels[maxlength]; } BTREE;

Potrebno je implementirati i funkcije iz ATP. Zapisujemo samo skicu koda. Potrebno je jo dodati provjere da su nam indeksi unutar maxlength i ako u polju pie -.
node ROOT (BTREE T) { return 0; } node LEFT_CHILD(node i, BTREE T) { return 2*i+1; } node RIGHT_CHILD(node i, BTREE T) { return 2*i+2; } node PARENT(node i, BTREE T) { return (i-1)/2; } labeltype LABEL(node i, BTREE T) { return T.labels[i]; } CHANGE_LABEL(labeltype l, node i, BTREE *T) { T->labels[i] = l; } INSERT_LEFT_CHILD(labeltype l, node i, BTREE *T) { T->labels[2*i+1] = l; // ili iskoristimo funkciju CHANGE_LABEL } INSERT_RIGHT_CHILD(labeltype l, node i, BTREE *T) { T->labels[2*i+2] = l; } DELETE(node i, BTREE *T) { T->labels[i] = -; }

Napomena: U poglavlju sa listama napravili smo funkciju MERGE za spajanje dvije sortirane vezane liste u jednu (zadatak 2.1.4). U napomeni nakon tog zadatka spomenut je i problem odreivanja redoslijeda spajanja vie sortiranih lista u jednu. Ovdje emo dati openitiji pristup. Redoslijed saimanja opisujemo binarnim stablom kojem su listovi veliine polaznih liste koje elimo spojiti. Unutranji vorovi su rezultati pojedinih saimanja, a korijen predstavlja ukupno vrijeme saimanja liste. 44

Uoimo jednu posebnost ovog stabla svaki vor ima tono 0 ili 2 djeteta. Stablo sa ovakvim svojstvom nazivamo 2-stablo. Cilj nam je pronai raspored (2-stablo) za koje e ukupno vrijeme saimanja biti minimalno. Primjer: Kao primjer navedimo dva mogua saimanja liste od 110 lanova. U oba sluaja poinjemo sa 3 podliste: L1 veliine 30, L2 veliine 20 i L3 veliine 10.

Slika 3.5: Prikaz redoslijeda saimanja listi u obliku stabla Uoimo da lanovi pojedine liste sudjeluju u onoliko operacija MERGE koliko je dug put od pripadnog lista do korijena stabla (tj. koliko ja razina lista). Ukupno vrijeme saimanja liste prikazanog nekim 2-stablom je suma vremena saimanja pojedinih listova po svim listovima stabla, ukupno vrijeme saimanja = wi p i
list Li ponderirana duljina

gdje wi oznaava vrijeme potrebno za saimanje lista Li , a pi oznaava razinu od Li . Na poetni problem za nalaenje optimalnog rasporeda saimanja listi se sada moe iskazati ovako: Za zadane w1 , . . . , wn pronaite 2-stablo sa n listova koje ima minimalnu ponderiranu duljinu. Ovaj problem rjeava jedna od inaica Humanovog algoritma . Zadatak 3.2.10. Naite optimalni plan saimanja za 6 sortiranih listi sa duljinama w1 = 2, w2 = 3, w3 = 5, w4 = 7, w5 = 0, w6 = 13. Rjeenje: Rjeenje nalazimo koritenjem Humanovog algoritma. Uoimo da su u ovom primjeru polazne liste ve sortirane po veliini, to openito ne mora biti sluaj. Sastavimo listu koja sadri nae liste koje shvatimo kao vorove. U svakom koraku spajamo po dvije liste kao podstabla u novo stablo iji je korijen kao oznaku ima sumu oznaka korijena dviju lista, dok ne doemo do liste koja sadri samo jedno stablo. Postupak se moe vidjeti na slici 3.6.
David Albert Human (19251999), jedan od pionira raunarstva

45

Algoritam 1: Humanov algoritam Ulaz: L, lista teina sortiranih lista (w1 , . . . , wn ); Izlaz: T , optimalni plan saimanja lista w1 , . . . , wn u obliku binarnog stabla dok lista L vie od jednog elementa radi odaberi 2 stabla T1 i T2 iz L iji korijeni imaju najmanje oznake; izbaci stabla T1 i T2 iz liste L; stvoro novo 2-stablo Tnovo u ijem korijenu pie zbroj oznaka korijena stabala T1 i T2 , kojemu je lijevo podstablo T1 i desno podstablo T2 ; ubaci Tnovo u listu; vrati jedino preostalo stablo u listi L Zadatak 3.2.11. Pokaite da Humanov algoritam daje optimalni plan saimanja sortiranih listi Rjeenje: Tvrdnju dokazujemo indukcijom po broju listi koje treba saeti. baza Za n = 1 imamo samo jednu listu pa je ne treba saimati pretpostavka Pretpostavimo da Humanov algoritam daje optimalni plan saimanja n 1 listi sa teinama w1 , . . . , wn1 . korak Neka je T neko optimalno stablo saimanja (ono ne mora biti Humanovo stablo, vano je samo da je optimalno). Tvrdimo da je tada su tada vorovi w1 i w2 na najnioj razini stabla, to jest p1 = p2 = v , gdje je v visina stabla T . Pretpostavimo suprotno: tada postoji neki vor wk , k = 1, 2 koji jest na najniem nivou. Zamijenimo li w1 ili w2 sa wk dobivamo stablo sa manjim ukupnim vremenom saimanja od T , to je kontradikcija s optimalnou stabla T . Naime vrijeme saimanja stabla T nastalog iz stabla T je: v (T ) = v (T ) pk wk p1 w1 +p1 wk + pk w1
uklonimo vorove iz stabla vratimo vorove

na svakom nivou razlike se zbraja wk w1 puta manje u ukupnu teinu, pa to moemo zapisati i ovako: = v (T ) + (p1 pk ) (wk w1 ) < v (T )
<0 0

Uoimo da se promjenom listova na istom nivou ne mijenja ukupno vrijeme. Stoga postoji optimalno stablo u kojem su spojeni vorovi s oznakama w1 i w2 , pa ih smijemo spojiti. Time dobivamo listu sa vorovima teina (w1 + w2 , w3 , . . . , wn ) koja sadri n 1 listi, a za nju po pretpostavci Humanov algoritam daje optimalno rjeenje.

46

. 2 . 5

. 3 . 5

. 5 . 7

. 7 . 9

. 9 . 13

. 13

2 . 10

3 . 7 . 9 . 13

3 . 10 . 16 . 13

3 . 23 . . 16

10

13

3 . 39

23

16

10

13

47

Slika 3.6: Skica izgradnje optimalnog stabla saimanja koritenjem Humanovog algoritma

Poglavlje 4 Skupovi
Apstraktni tip podataka skup ili set sastoji se od ovih elemenata: elementtype bilo koji tip sa de niranim totalnim ureajem to su podaci koje uvamo u skupu SET skup podataka tipa elementtype void MAKE_NULL (SET* A) funkcija pretvara skupa A u prazni skup void INSERT(elementtype x, SET* A) ubacuje element x u skup A void DELETE(elementtype x, SET* A) izbacuje element x iz skupa A (nije denirana ako x /A bool MEMBER(elementtype x, SET* A) vraa true ako vrijedi x A, a false inae elementtype MIN(SET A) funkcija vraa najmanji element u skupu A (najmanji po ureaju ) elementtype MAX(SET A) funkcija vraa najvei element u skupu A (najvei po ureaju ) bool SUBSET(SET A, SET B) funkcija vraa true ako vrijedi A B , a false inae void UNION(SET A, SET B, SET* C) rezultat funkcije je C = A B void INTERSECTION(SET A, SET B, SET* C) C = A B void DIFFERENCE(SET A, SET B, SET *C) C = A B Na predavanjima se obrauju razne implementacije ATPa SET kao to su implementacije pomou bit-polja i sortirane vezane liste. To su prilino jednostavne implementacije koje su brze kod nekih, a spore kod drugih operacija (kao to je esto sluaj kod ATPa). Sortirana vezana lista tako treba O(N ) za funkciju MEMBER, bit-polje ima veu sloenost za funkcije MIN i MAX, sortirano polje za INSERT. Postoje i neto kompliciranije implementacije koje omoguavaju dobre sloenosti svih funkcija (O(log N )). Primjer takve implementacije je crveno-crno stablo (red-black tree)
Radi se o binarnom stablu traenja koje pazi da uvijek bude balansirano. Primjenjuje se za rasporeivanja zadataka procesorima u novijim Linux kernelima.

48

Zadatak 4.0.12. Napiite funkciju kojom se implementira UNION pod pretpostavkom da je skup dan sortiranom listom svojih elemenata. Implementacija mora biti neovisna o ATP LIST Rjeenje: Ideja je slina kao kod MERGE, no ovaj put moramo paziti da isti element ne ubacujemo vie puta
void UNION(LIST A, LIST B, LIST* C) { position pA, pB, pC; pA = FIRST(A), pB = FIRST(B), pC = MAKE_NULL(C); while ((pA != END(A)) && (pB != END(B)) { if (RETRIEVE(pA,A) == RETRIEVE(pB, B)) { INSERT(RETRIEVE(pA,A), pC, C); pA = NEXT(pA, A); pB = NEXT(pB, B); } else if (RETRIEVE(pA, A) < RETRIEVE(pB, B)) { INSERT(RETRIEVE(pA, A), pC, C); pA = NEXT(pA, A); } else { INSERT(RETRIEVE(pB, B), pC, C); pB = NEXT(pB, B); } pC = NEXT(pC, *C); } while (pA != END(A)) { INSERT(RETRIEVE(pA,A), pC, C); pA = NEXT(pA, A); pC = NEXT(pC, *C)m } while (pB != END(B)) { INSERT(RETRIEVE(pB, B), pC, C); pB = NEXT(pB, B); pC = NEXT(pC, *C); } }

49

50

4.1

Rjenik

51

52

53

54

55

56

57

58

4.2

Prioritetni red

59

60

61

Nedostaje: Ostatak gradiva moe se pronai na stranici kolegija u obliku prezentacija

62

You might also like