Professional Documents
Culture Documents
Beleške za predavanja
Smer Informatika
Matematički fakultet, Beograd
2010.
2
Sadržaj
I Osnovi algoritmike 7
1 Algoritmi i rešavanje problema 9
1.1 Algoritmi — intuitivni i formalni pojam . . . . . . . . . . . . . . 9
1.2 Rešavanje problema uz pomoć računara . . . . . . . . . . . . . . 10
3 Složenost algoritama 19
3.1 Osnove analize algoritama: ,,O“ notacija; klase složenosti . . . . 19
3.2 Red algoritma; analiza najgoreg slučaja; O notacija . . . . . . . . 20
3.3 Izračunavanje složenosti algoritama . . . . . . . . . . . . . . . . . 21
3.4 NP kompletnost . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
5 Fundamentalni algoritmi 39
5.1 Pretraživanje . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
5.1.1 Pronalaženje karaktera u stringu . . . . . . . . . . . . . . 39
5.1.2 Odredivanje maksimuma . . . . . . . . . . . . . . . . . . . 39
5.1.3 Linearno pretraživanje . . . . . . . . . . . . . . . . . . . . 40
5.1.4 Binarno pretraživanje . . . . . . . . . . . . . . . . . . . . 42
5.2 Sortiranje . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
5.2.1 Sortiranje selekcijom . . . . . . . . . . . . . . . . . . . . . 50
5.2.2 Sortiranje umetanjem . . . . . . . . . . . . . . . . . . . . 51
5.2.3 Babl sortiranje . . . . . . . . . . . . . . . . . . . . . . . . 52
5.2.4 Quick sort . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
5.2.5 Korišćenje sistemske implementacije quick sort-a . . . . . 56
5.3 Jednostavni numerički algoritmi . . . . . . . . . . . . . . . . . . . 58
5.3.1 Stepenovanje . . . . . . . . . . . . . . . . . . . . . . . . . 58
5.3.2 Izračunavanje vrednosti polinoma . . . . . . . . . . . . . . 59
5.3.3 Zagradivanje nula funkcije . . . . . . . . . . . . . . . . . . 59
5.3.4 Odredivanje nula funkcije . . . . . . . . . . . . . . . . . . 61
II Dinamički objekti 65
6 Pokazivači i adresna aritmetika 67
6.1 Adresna aritmetika . . . . . . . . . . . . . . . . . . . . . . . . . . 67
6.2 Višedimenzioni nizovi . . . . . . . . . . . . . . . . . . . . . . . . 68
6.3 Inicijalizacija nizova pokazivača . . . . . . . . . . . . . . . . . . . 70
6.4 Pokazivači i višedimenzioni nizovi . . . . . . . . . . . . . . . . . . 70
6.5 Pokazivači na funkcije . . . . . . . . . . . . . . . . . . . . . . . . 71
Osnovi algoritmike
Glava 1
Algoritmi i rešavanje
problema
1. specifikovanje problema
3. realizacija
5. izrada dokumentacije
6. eksploatacija i održavanje
Ispitivanje ispravnosti
programa
{ϕ}P {ψ}
gde su {ϕ} i {ψ} logički izrazi, tvrdenja čija je vrednost ili tačno ili netačno, a
P je algoritam (tj. program). Trojku (ϕ, P, ψ) nazivamo Horova trojka.
Interpretacija ovog zapisa specifikacije programa je sledeća: ”Ako izvršenje
programa P počinje sa vrednostima ulaznih promenljivih (”u stanju”) koje zado-
voljavaju uslov {ϕ}, onda se garantuje da će se P završiti u konačnom vremenu
sa vrednostima programskih promenljivih (”u stanju”) koje zadovoljavaju uslov
{ψ}”. Uslov {ϕ} naziva se preduslov za algoritam (program, iskaz) P , a uslov
{ψ} naziva se postuslov (pauslov, posleuslov) za algoritam (program, iskaz) P .
{m = n}j := (m + n)/2; {m = j = n}
{x = X ∧ y = Y }swap{x = Y ∧ y = X}
Interpretacija ovog zapisa je da
∀X, Y, x, y{x = X ∧ y = Y }swap{x = Y ∧ y = X}.
Tačnost ovog tvrdenja sledi iz sledećeg razmatranja:
{x = X ∧ y = Y }t := x{x = X ∧ y = Y ∧ t = X}
{x = X ∧ y = Y ∧ t = X}x := y{x = Y ∧ y = Y ∧ t = X}
{x = Y ∧ y = Y ∧ t = X}y := t{x = Y ∧ y = X ∧ t = X}
godine u Kolombu (Šri Lanka), a diplomirao na univerzitetu u Oksfordu 1956. godine. Kon-
struisao je, 1960. godine, algoritam za sortiranje quicksort, jedan od najkorišcenijih algori-
tama uopšte. Razvio je tzv. Horovu logiku i formalni jezik csp za specifikovanje konkurentnih
procesa. Godine 1980. dobio je Tjuringovu nagradu za ,,svoje fundamentalne doprinose
16 2 Ispitivanje ispravnosti programa
Definicija 1.
Svojstvo koje važi svaki put kada se ispituje da li je zadovoljen uslov ulaska u
petlju naziva se invarijanta petlje.
Svaka petlja ima puno invarijanti. Tako je, na primer, x*0=0 invarijanta
svake petlje. Medutim, korisna invarijanta je samo ona koja vodi ka pauslovu
kada uslov petlje više nije ispunjen.
Formalna verifikacija programa metodom induktivne invarijante zasniva se
na dokazu da se iz preduslova može uspostaviti invarijanta pre prvog ulaska u
petlju, zatim da svaki put kada se ulazi u petlju važi dati uslov i, na kraju, da
pri prestanku važenja uslova ulaska u petlju on vodi ka ostvarivanju pauslova
programa.
Dokaz ispravnosti programa metodom induktivne invarijante se, dakle, sas-
toji iz tri koraka:
• Uspostavljanje invarijante
• Održavanje invarijante
z =n∗y =x∗y
Složenost algoritama
Razmatra se
• n2 = O(n2 )
• 3 ∗ n2 + 10 = O(n2 )
• zaustavljanje1
• korektnost2
• kompleksnost (složenost)
– vremenska
– prostorna (memorijska)
Primer 3.3. Izračunavanje vrednosti faktorijela prirodnog broja: n poredjenja
i n − 1 množenje; linearna složenost.
Primer 3.4. Odredjivanje najmanjeg elementa u nizu od n prirodnih brojeva:
linearna složenost.
Primer 3.5. Sortiranje niza od n prirodnih brojeva odredjivanjem najmanjeg u
tekućem nizu: n(n − 1)/2 vremenskih jedinica (kvadratna složenost).
onda pišemo
f = O(g)
i čitamo “f je veliko ‘o’ od g”.
NOTA BENE: O nije funkcija; O označava klasu funkcija.
Aditivne i multiplikativne konstante ne utiču na klasu kojoj funkcija pripada.
Primer 3.6. Važi:
• n2 = O(n2 )
• n2 + 10 = O(n2 )
• 10 · n2 + 10 = O(n2 )
• 10 · n2 + 8n + 10 = O(n2 )
• n2 = O(n3 )
1 Tjuring je tridesetih godina dvadesetog veka dokazao da ne postoji opšti postupak kojim
se za proizvoljan program može utvrditi da li se zaustavlja (to je tzv. halting problem). Dakle,
postoje funkcije koje nisu izračunljive.
2 Videti poglavlje o korektnosti programa u skripti za kurs Programiranje I.
3.3 Izračunavanje složenosti algoritama 21
• 2n = O(2n )
• 2n + 10 = O(2n )
• 10 · 2n + 10 = O(2n )
• 2n + n2 = O(2n )
• 3n + 2n = O(3n )
• 2n + 2n n = O(2n n)
Definicija 3.
Ako je T (n) vreme izvršavanja algoritam A (čiji ulaz karakteriše prirodan broj
n), ako važi T = O(g) i ako u najgorem slučaju vrednost T (n) dostiže vrednost
c · g(n) (gde je c pozitivna konstanta), onda kažemo da je algoritam A složenosti
(ili reda) g i da pripada klasi O(g).
Često se algoritmi ne izvršavaju isto za sve ulaze istih veličina, pa je potrebno
naći način za opisivanje i poredjenje efikasnosti različitih algoritama. Anal-
iza najgoreg slučaja zasniva procenu složenosti algoritma na najgorem slučaju
(na slučaju za koji se algoritam najduže izvršava). Ta procena može da bude
varljiva, ali ne postoji bolji opšti način za poredjenje efikasnosti algoritama. Čak
i kada bi uvek bilo moguće izračunati prosečno vreme izvršavanja algoritma, i
takva procena bi često mogla da bude varljiva. Analiziranje najboljeg slučaja,
naravno, ima još manje smisla.
Primer 3.7. Algoritam za izračunavanje vrednosti faktorijela prirodnog broja
je reda n, tj. pripada klasi O(n), tj. on je linearne složenosti.
Primer 3.8. Algoritam za sortiranje niza od n prirodnih brojeva odredjivanjem
najmanjeg u tekućem nizu je reda n2 , tj. pripada klasi O(n2 ), tj. on je kvadratne
složenosti jer važi
n(n − 1) 1
≤ n2 za sve vrednosti n ≥ 1.
2 2
i u najgorem slučaju (za ovaj algoritam svi se slučajevi mogu smatrati najgorim)
dostiže vrednost koja kvadratno zavisi od n.
T (n) = T (n − 1) + 2 = T (n − 2) + 2 + 2 = . . . = T (1) + 2 + . . . + 2 =
| {z }
n−1
= 1 + 2(n − 1) = 2n + 1 = O(n) ,
pa je algoritam A reda n (tj. pripada klasi O(n), tj. on je linearne složenosti).
Zadatak 1. Ako za vreme izvršavanja T (n) algoritma A (gde n odredjuje ulaznu
vrednost za algoritam) važi T (n) = T (n − 1) + n/2 i T (1) = 1, odrediti složenost
algoritma A.
Rešenje:
n n−1 n 2 n−1 n
T (n) = T (n − 1) + = T (n − 2) + + = T (1) + + . . . + + =
2 2 2 2 2 2
2
1 1 n(n + 1) n +n+2
= T (1) + (2 + . . . + n) = 1 + −1 = ,
2 2 2 4
pa je algoritam A kvadratne složenosti.
Zadatak 2. Ako za vreme izvršavanja T (n) algoritma A (gde n odredjuje ulaznu
vrednost za algoritam) važi T (n + 2) = 8T (n + 1) − 15T (n) (za n ≥ 1) i T (1) =
1, T (2) = 4, odrediti složenost algoritma A.
Rešenje:
Karakteristična jednačina za navedenu homogenu rekurentntu vezu drugog
reda je
t2 = 8t − 15
i njeni koreni su t1 = 3 i t2 = 5. Opšti član niza T (n) može biti izražen u obliku
tj.
T (n) = α · 3n + β · 5n ,
Iz T (1) = 1, T (2) = 4 dobija se sistem
α·3+β·5=1
α · 9 + β · 25 = 4
čije je rešenje (α, β) = (1/6, 1/10). Dakle, važi
1 n 1
T (n) = 3 + 5n ,
6 10
pa je algoritam A reda O(5n ).
3.3 Izračunavanje složenosti algoritama 23
x2 − 4x + 2 = 0
√ √
Njeni koreni su x1 = 2 + 2 i x2 = 2 − 2, pa je opšte rešenje rekurentne
jednačine (6) oblika:
√ √
A(n) = c1 (2 + 2)n + c2 (2 − 2)n
1 √ √
A(n) = ((2 + 2)n−1 + (2 − 2)n−1 )
2
Zadatak 6. Problem P ima parametar n (n je prirodan broj) i rešava se pri-
menom algoritama A i B. Algoritam A rešava problem za vrednost n (n > 1).
primenom algoritma B za vrednost n − 1, pri čemu se za svodjenje problema
troši n vremenskih jedinica. Algoritam B rešava problem za vrednost n (n > 1).
primenom algoritma A za vrednost n − 1, pri čemu se za svodjenje problema
troši n vremenskih jedinica. Problem za n = 1, algoritam A rešava trivijalno za
jednu vremensku jedinicu, a algoritam B za dve vremenske jedinice. Izračunati
vreme izvršavanja algoritma A za ulaznu vrednost n.
3.3 Izračunavanje složenosti algoritama 25
Rešenje:
Označimo sa an vreme izvršavanja algoritma A, a sa bn vreme izvršavanja
algoritma B za ulaznu vrednost n. Na osnovu uslova zadatka je:
a1 = 1
b1 = 2
an = bn−1 + n (n > 1)
bn = an−1 + n (n > 1)
Kako je bn−1 = an−2 + n − 1, zaključujemo da je
an = an−2 + n − 1 + n
Primenom matematičke indukcije dokažimo da za neparne vrednosti n važi
an = n(n+1)
2 .
Tvrdjenje važi za n = 1 jer je a1 = 1 = 1·2
2 .
Pretpostavimo da je tvrdjenje tačno za neki neparan broj n i dokažimo da
važi i za sledeći neparan broj - n + 2:
an+2 = bn+1 + n + 2 = an + n + 1 + n + 2
an+2 = n(n+1)2 +n+1+n+2
n(n+1)+2(n+1)+2(n+2)
an+2 = 2
(n+1)(n+2)+2(n+2)
an+2 = 2
(n+2)(n+1+2)
an+2 = 2
(n+2)(n+3)
an+2 = 2
Dakle, na osnovu principa matematičke indukcije tvrdjenje važi za sve neparne
brojeve.
Dokažimo da za parne vrednosti n važi an = n(n+1)
2 + 1.
2·3
Za n = 2 tvrdjenje je tačno: a2 = b1 + 2 = 2 + 2 = 2 + 1.
Pretpostavimo da je tvrdjenje tačno za neki paran broj n i dokažimo da je
tačno i za n + 2, tj. za sledeći paran broj:
an+2 = bn+1 + n + 2 = an + n + 1 + n + 2
an+2 = n(n+1)
2 +1+n+1+n+2
n(n+1)+2(n+1)+2(n+2)
an+2 = 2 + 1
(n+1)(n+2)+2(n+2)
an+2 = 2 +1
(n+2)(n+1+2)
an+2 = 2 +1
(n+2)(n+3)
an+2 = 2 +1
Dakle, tvrdjenje je tačno za sve parne brojeve.
Konačno imamo da je:
(
n(n+1)
2 +1 za n parno
an = n(n+1)
2 za n neparno
Teorema 1.
Rešenje rekurentne relacije
3.4 NP kompletnost
Primer 3.10. Šef protokola na jednom dvoru treba da organizuje bal za pred-
stavnike ambasada. Kralj traži da na bal bude pozvan Peru ili da ne bude pozvan
Katar (Qatar). Kraljica zahteva da budu pozvani Katar ili Rumunija (ili i Katar
i Rumunija). Princ zahteva da ne bude pozvana Rumunija ili da ne bude pozvan
Peru (ili da ne budu pozvani ni Rumunija ni Peru). Da li je moguće organizovati
bal i zadovoljiti zahteve svih članova kraljevske porodice?
Zadovoljivost navedenog iskaza može biti odredjena tako što bi bile ispitane sve
moguće interpretacije – sve moguće dodele varijablama p, q i r. Ako je u nekoj
interpretaciji vrednost datog logičkog iskaza true, onda je dati iskaz zadovoljiv.
Za izabranu, fiksiranu interpretaciju može se u konstantnom vremenu utvrditi
da li je istinitosna vrednost true. Za tri promenljive ima 2n interpretacija, pa
je red ovog algoritma za ispitivanje zadovoljivosti logičkih iskaza reda O(2n )
(algoritam je eksponencijalne složenosti). Pitanje je da li postoji algoritam koji
navedeni problem rešava u polinomijalnom vremenu.
Definicija 4.
Za algoritam sa ulaznom vrednošću n kažemo da je polinomijalne složenosti
ako je njegovo vreme izvršavanja O(P (n)) gde je P (n) polinom po n. Klasu
polinomijalnih algoritama označavamo sa P .
P = NP ?
Definicija 7.
Za problem X kažemo da je N P -težak problem ako je svaki N P problem poli-
nomijalno svodljiv na X.
Definicija 8.
Za problem X kažemo da je N P -kompletan problem ako pripada klasi N P i
ako je N P -težak.
Teorema 2.
Ako bilo koji N P -težak problem pripada klasi P , onda važi P = N P .
Teorema 3.
Problem X je N P -kompletan ako
• X pripada klasi N P
polynomial-time SAT solver or a proof that such an algorithm does not exist (the P vs N P
problem).
28 3 Složenost algoritama
Rekurzija, rekurzivne
matematičke funkcije i
primeri korišćenja rekurzije
• The parents of any ancestor are also ancestors of the person under con-
sideration (recursion step).
• Show that the theorem is true for the smallest case. This can usually be
done by inspection.
⇒ basis
• Show that if the theorem is true for the basis cases, it can be extended to
include the next case. If the theorem is true for the case k = n − 1, show
that it is true for the case k=n.
⇒ Inductive hypothesis assumes that the theorem is true for the case
k = n − 1.
• The base case of a recursive method is the case that can be solved without
a recursive call.
4.4 Faktorijel
int factorial(int n) {
if (n==1)
return 1;
else
return n*factorial(n-1);
}
Prva varijanta:
#include<stdio.h>
main() {
triangle(3,5);
}
if(m>n)
return;
for(i=0;i<m;i++)
printf("*");
printf("\n");
triangle(m+1, n);
for(i=0;i<m;i++)
printf("*");
printf("\n");
}
4.6 Kule Hanoja 33
Definition: Given three posts (towers) and n disks of decreasing sizes, move
the disks from one post to another one at a time without putting a larger disk on
a smaller one. The minimum is 2n-1 moves. The ”ancient legend”was invented
by De Parville in 1884.
A solution using recursion is: to move n disks from post A to post B (1)
recursively move the top n-1 disks from post A to C, (2) move the n-th disk
from A to B, and (3) recursively move the n-1 disks from C to B. A solution
using iteration is: on odd-numbered moves, move the smallest disk clockwise.
On even-numbered moves, make the single other move which is possible.
Calling the above method with n=4, i.e., tower(4,’A’,’C’,’B’) yields the
following result:
Moving a disk from peg A to peg B Moving a disk from peg A to peg C Moving a disk from peg B
Moving a disk from peg A to peg B Moving a disk from peg C to peg A Moving a disk from peg C
Moving a disk from peg A to peg B Moving a disk from peg A to peg C Moving a disk from peg B
Moving a disk from peg B to peg A Moving a disk from peg C to peg A Moving a disk from peg B
Moving a disk from peg A to peg B Moving a disk from peg A to peg C Moving a disk from peg B
4 Rekurzija, rekurzivne matematičke funkcije i primeri korišćenja
34 rekurzije
4.7 Permutacije
#include<stdio.h>
void permutation(int n) ;
main() {
int i;
for (i=1;i<=N;i++)
used[i]=0;
permutation(1);
}
void permutation(int n) {
int i;
if(n>N)
{
for(i=1;i<=N;i++)
printf("%d ",p[i]);
printf("\n");
}
else
for (i=1;i<=N;i++)
if (!used[i])
{
used[i]=1;
p[n]=i;
permutation(n+1);
used[i]=0;
}
}
1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1
Druga varijanta:
4.8 Particionisanje 35
#include<stdio.h>
void permutation(int n) ;
main() {
for (i=1;i<=N;i++)
p[i]=i;
permutation(1);
}
void permutation(int n) {
int i,tmp;
if(n==N)
{
for(i=1;i<=N;i++)
printf("%d ",p[i]);
printf("\n");
}
else
for (i=n;i<=N;i++)
{
tmp=p[n];
p[n]=p[i];
p[i]=tmp;
permutation(n+1);
tmp=p[n];
p[n]=p[i];
p[i]=tmp;
}
}
4.8 Particionisanje
A partition of a positive integer n is a sequence of positive integers that sum
to n. Write a program to print all partitions of n, e.g:
n=4 4 3 1 2 2 2 1 1 1 3 1 2 1 1 1 2 1 1 1 1
Rešenje:
4 Rekurzija, rekurzivne matematičke funkcije i primeri korišćenja
36 rekurzije
#include<stdio.h>
main() {
partition(4);
}
if(n==0)
{
for(i=0;i<k;i++)
printf("%d ",p[i]);
printf("\n");
return;
}
for(i=n;i>0;i--)
{
p[k]=i;
k++;
partition(n-i);
k--;
}
}
So far we have only considered recursive methods that call themselves. An-
other type of recursion involves methods that cyclically call each other. This
is known as cyclical or mutual recursion. In the following example, methods A
and B are mutually recursive.
4.10 Nedostaci rekurzije 37
void A(int n) {
if (n <= 0)
return;
n--;
B(n);
}
void B(int n) {
if (n <= 0)
return;
n--;
A(n);
}
Consider now the execution of this function for n=5. Note the redundant
calculations: 3 calls to fib(0), 5 calls to fib(1), and so on. Each recursive call
does more and more redundant work, resulting in exponential growth.
Cena poziva All local variables and formal parameters for the procedure are
stored on top of a stack. When a recursive call is made, new copies are pushed.
This can be very time and space consuming. In such cases, when possible,
recursion should be replaced by iterative solution.
Na primer, gore navedena funkcija fib može se zameniti iterativnom funkci-
jom koja dinamički alocira prostor (korišćenjem funkcije realloc) za elemente
Fibonačijevog niza ili iterativnom funkcijom koja ne pamti sve elemente niza do
indeksa n već samo dva prethodna:
4 Rekurzija, rekurzivne matematičke funkcije i primeri korišćenja
38 rekurzije
int fib(int n) {
int f1,f2,tmp,i;
if (n<=1)
return n;
f1 = 0;
f2 = 1;
for(i=3;i<=n;i++)
{
tmp = f2;
f2 = f1+f2;
f1 = tmp;
}
return f2;
}
Fundamentalni algoritmi
5.1 Pretraživanje
Pod pretraživanjem za dati niz elemenata (npr. data) i datu vrednost (npr. value)
podrazumevamo odredivanje indeksa elementa niza koji je jednak datoj vred-
nosti (tj. odredivanje indeksa i takvog da je data[i]==value).
Primer 5.1.
/* Pronalazi poslednju poziciju karaktera c u stringu s,
odnosno -1 ukoliko s ne sadrzi c */
int string_last_char(char s[], char c) {
Koristeci string_length :
return -1;
}
int main() {
/* Dimenzija niza,pomocna i brojacke promenljive */
int n, i, max;
max = a[1];
for(i=2; i<n; i++)
if (a[i]>max)
max = a[i];
Linear search can be used to search an unordered list. The more efficient
binary search can only be used to search an ordered list.
#include <stdio.h>
return -1;
}
main() {
/* Inicijalizacija niza moguca je i na ovaj nacin*/
int a[] = { 4, 3, 2, 6, 7, 9, 11 };
Primer 5.5. Even if the number we’re guessing can be arbitrarily large, in
which case there is no upper bound N , we can still find the number in at most
O(log k) steps (where k is the (unknown) selected number) by first finding an
upper bound by repeated doubling. For example, if the number were 11, we could
use the following sequence of guesses to find it:
Is the number greater than 1? (Yes)
Is the number greater than 2? (Yes)
Is the number greater than 4? (Yes)
Is the number greater than 8? (Yes)
Is the number greater than 16? (No, N=16, proceed as above) ( We know
the number greater than 8 )
Is the number greater than 12? (No)
Is the number greater than 10? (Yes)
Is the number greater than 11? (No)
5.1 Pretraživanje 43
Primer 5.7. There are many occasions unrelated to computers when a binary
chop is the quickest way to isolate a solution we seek. In troubleshooting a
single problem with many possible causes, we can change half the suspects, see
if the problem remains and deduce in which half the culprit is; change half the
remaining suspects, and so on.
#include<stdio.h>
#define MAXDUZ 100
int main() {
/* Dimenzija niza,pomocna i brojacke promenljive */
int n,pom,i,j;
/*Sortiranje*/
for(i=0; i<n-1; i++)
for(j=i+1; j<n; j++)
if(a[i]>a[j])
{
pom=a[i];
a[i]=a[j];
a[j]=pom;
}
donji = 0;
gornji = n-1;
pozicija = -1;
while(donji<=gornji)
{
srednji = (donji + gornji)/2;
if(a[srednji] == x)
{
pozicija = srednji;
break;
}
else
if(a[srednji] < x)
5.1 Pretraživanje 45
Primer 5.9.
46 5 Fundamentalni algoritmi
main() {
int a[] = {3, 5, 7, 9, 11, 13, 15};
int x;
int i;
if (i==-1)
printf("Elementa %d nema\n", x);
else
printf("Pronadjen na poziciji %d\n", i);
}
5.2 Sortiranje 47
Here is code which determines the index of a given value in a sorted list a
between indices left and right (if the value is not found, -1 is returned):
int binarySearch(int *data, int value, int left, int right) {
while (left <= right)
{
mid = (right-left)/2+left;
if (data[mid] == value)
return mid;
if (value < data[mid])
right = mid-1;
else
left = mid+1;
}
return -1;
}
Recursive version:
int binarySearch(int a[], int value, int left, int right) {
if (right < left)
return -1;
mid = (right-left)/2 + left;
if (a[mid] == value
return mid;
if (value < a[mid])
return binarySearch(a, value, left, mid-1);
else
return binarySearch(a, value, mid+1, right);
}
In both cases, the algorithm terminates because on each recursive call or
iteration, the range of indexes right minus left always gets smaller, and so must
eventually become negative.
Binary search is a logarithmic algorithm and executes in O(log n) time.
Specifically, 1 + / log2 n iterations are needed to return an answer. It is con-
siderably faster than a linear search. It can be implemented using recursion
or iteration, as shown above, although in many languages it is more elegantly
expressed recursively.
5.2 Sortiranje
One of the fundamental problems of computer science is ordering (ascending
or descending) a sequence of items. There’s a plethora of solutions to this
problem, known as sorting algorithms. Some sorting algorithms are simple and
intuitive, such as the bubble sort. Others, such as the quick sort are extremely
complicated, but produce lightening-fast results. Za više detalja videti http:
//linux.wku.edu/~lamonml/algor/sort/sort.html.
48 5 Fundamentalni algoritmi
• Bubble sort
• Heap sort
• Insertion sort
• Merge sort
• Quick sort
• Selection sort
• Shell sort
in the plot area is 100 seconds - these aren’t sorts that you want to use for huge
amounts of data in an interactive application. Even using the shell sort, users
are going to be twiddling their thumbs if you try to sort much more than 10,000
data items.
On the bright side, all of these algorithms are incredibly simple (with the
possible exception of the shell sort). For quick test programs, rapid prototypes,
or internal-use software they’re not bad choices unless you really think you need
split-second efficiency.
Speaking of split-second efficiency, the O(n log n) sorts (slika 5.2) are where
it’s at. Notice that the time on this graph is measured in tenths of seconds,
instead hundreds of seconds like the O(n2 ) graph.
But as with everything else in the real world, there are trade-offs. These
algorithms are blazingly fast, but that speed comes at the cost of complexity.
Recursion, advanced data structures, multiple arrays - these algorithms make
extensive use of those nasty things.
In the end, the important thing is to pick the sorting algorithm that you
think is appropriate for the task at hand. You should be able to use the source
code on this site as a ”black box”if you need to - you can just use it, without
understanding how it works. Obviously taking the time to understand how the
algorithm you choose works is preferable, but time constraints are a fact of life.
Zna se da postoje algoritmi za sortiranje koji su složenosti O(n log n), ali za
sada se ne zna da li postoji algoritam manjeg reda: za sada nije otkriven takav
algoritam niti je dokazano da on ne može da postoji.
The selection sort works by selecting the smallest unsorted item remaining
in the list, and then swapping it with the item in the next position to be filled.
The selection sort has a complexity of O(n2 ).
Pros: Simple and easy to implement.
Cons: Inefficient for large lists, so similar to the more efficient insertion sort
that the insertion sort should be used in its place.
The selection sort is in the group of n2 sorts. It yields a 60% performance
improvement over the bubble sort, but the insertion sort is over twice as fast
as the bubble sort and is just as easy to implement as the selection sort. In
short, there really isn’t any reason to use the selection sort - use the insertion
sort instead.
If you really want to use the selection sort for some reason, try to avoid
sorting lists of more than a 1000 items with it or repetitively sorting lists of
more than a couple hundred items.
Below is the basic selection sort algorithm.
5.2 Sortiranje 51
The insertion sort works just like its name suggests - it inserts each item
into its proper place in the final list. The simplest implementation of this
requires two list structures - the source list and the list into which sorted items
are inserted. To save memory, most implementations use an in-place sort that
works by moving the current item past the already sorted items and repeatedly
swapping it with the preceding item until it is in place.
Like the bubble sort, the insertion sort has a complexity of O(n2 ). Although
it has the same complexity, the insertion sort is a little over twice as efficient as
the bubble sort.
Pros: Relatively simple and easy to implement.
Cons: Inefficient for large lists.
The insertion sort is a good middle-of-the-road choice for sorting lists of a
few thousand items or less. The algorithm is significantly simpler than the shell
sort, with only a small trade-off in efficiency. At the same time, the insertion sort
is over twice as fast as the bubble sort and almost 40% faster than the selection
sort. The insertion sort shouldn’t be used for sorting lists larger than a couple
thousand items or repetitive sorting of lists larger than a couple hundred items.
Below is the basic insertion sort algorithm.
52 5 Fundamentalni algoritmi
The bubble sort is the oldest and simplest sort in use. Unfortunately, it’s
also the slowest.
The bubble sort works by comparing each item in the list with the item next
to it, and swapping them if required. The algorithm repeats this process until
it makes a pass all the way through the list without swapping any items (in
other words, all items are in the correct order). This causes larger values to
”bubble”to the end of the list while smaller values šink”towards the beginning
of the list.
The bubble sort is generally considered to be the most inefficient sorting algo-
rithm in common usage. Under best-case conditions (the list is already sorted),
the bubble sort can approach a constant O(n) level of complexity. General-case
is an abysmal O(n2 ).
While the insertion, selection, and shell sorts also have O(n2 ) complexities,
they are significantly more efficient than the bubble sort.
Pros: Simplicity and ease of implementation.
Cons: Horribly inefficient.
A fair number of algorithm purists (which means they’ve probably never
written software for a living) claim that the bubble sort should never be used
for any reason. Realistically, there isn’t a noticeable performance difference
between the various sorts for 100 items or less, and the simplicity of the bubble
sort makes it attractive. The bubble sort shouldn’t be used for repetitive sorts
or sorts of more than a couple hundred items.
Below is the basic bubble sort algorithm.
5.2 Sortiranje 53
1. If there are one or less elements in the array to be sorted, return immedi-
ately.
3. Split the array into two parts - one with elements larger than the pivot
and the other with elements smaller than the pivot.
4. Recursively repeat the algorithm for both halves of the original array.
The quick sort is by far the fastest of the common sorting algorithms. It’s
possible to write a special-purpose sorting algorithm that can beat the quick
sort for some data sets, but for general-case sorting there isn’t anything faster.
As soon as students figure this out, their immediate implulse is to use the
quick sort for everything - after all, faster is better, right? It’s important to
resist this urge - the quick sort isn’t always the best choice. As mentioned
earlier, it’s massively recursive (which means that for very large sorts, you can
run the system out of stack space pretty easily). It’s also a complex algorithm
- a little too complex to make it practical for a one-time sort of 25 items, for
example.
With that said, in most cases the quick sort is the best choice if speed is
important (and it almost always is). Use it for repetitive sorting, sorting of
medium to large lists, and as a default choice when you’re not really sure which
sorting algorithm to use. Ironically, the quick sort has horrible efficiency when
operating on lists that are mostly sorted in either forward or reverse order -
avoid it in those situations.
l_hold = left;
r_hold = right;
pivot = numbers[left];
while (left < right)
{
while ((numbers[right] >= pivot) && (left < right))
right--;
if (left != right)
{
numbers[left] = numbers[right];
left++;
}
while ((numbers[left] <= pivot) && (left < right))
left++;
if (left != right)
{
numbers[right] = numbers[left];
right--;
}
}
numbers[left] = pivot;
pivot = left;
left = l_hold;
right = r_hold;
if (left < pivot)
q_sort(numbers, left, pivot-1);
if (right > pivot)
q_sort(numbers, pivot+1, right);
}
Primer 5.11.
5.2 Sortiranje 57
main() {
int i,niz[10];
for(i=0;i<10;i++)
{
niz[i]=rand()%20;
printf("%d ",niz[i]);
}
qsort(niz,10,sizeof(int),poredi_brojeve);
printf("Sortirano:\n");
for(i=0;i<10;i++)
printf("%d ",niz[i]);
}
Izlaz programa:
1 7 14 0 9 4 18 18 2 4 Sortirano: 0 1 2 4 4 7 9 14 18 18
Primer 5.12.
58 5 Fundamentalni algoritmi
Output:
[C:\code]qsort every good boy deserves favor boy deserves every favor
good
5.3.1 Stepenovanje
for(int i=0;i<k;i++)
m *= n;
return m;
}
/* n>=0, k>=0 */ int stepen_brzo(int n, int k) {
int m;
if (k==0)
return 1;
if (k%2)
{
m = stepen_brzo(n,(k-1)/2);
return n*m*m;
}
else
{
m = stepen_brzo(n,k/2);
return m*m;
}
}
1
f (x) =
x−c
float funkcija1(float x) {
return x*x*x-2;
}
main() {
float x1,x2;
x1=5; x2=10;
printf("x1=%f, x2=%f\n",x1,x2);
if(zbrac(funkcija1, &x1, &x2)==1)
printf("x1=%f, x2=%f\n",x1,x2);
}
dobija se rezultat:
x1=5.000000, x2=10.000000 x1=-3.000000, x2=10.000000
εn+1 = εn /2
neither more nor less. Thus, we know in advance the number of iterations
required to achieve a given tolerance in the solution,
ε0
n = log2
ε
where ε0 is the size of the initially bracketing interval, ε is the desired ending
tolerance.
Bisection must succeed. If the interval happens to contain two or more
roots, bisection will find one of them. If the interval contains no roots and
merely straddles a singularity, it will converge on the singularity.
62 5 Fundamentalni algoritmi
x1=5; x2=10;
printf("x1=%f, x2=%f\n",x1,x2);
if(zbrac(funkcija1, &x1, &x2)==1)
{
root = rtbis(funkcija1, x1, x2, 0.0001);
printf("root = %f\n",root);
}
}
root = 1.259872
Zaista, (1.259872)3 ≈ 1.9997664 ≈ 2.
64 5 Fundamentalni algoritmi
Deo II
Dinamički objekti
Glava 6
Pokazivači i adresna
aritmetika
i = a; /* neispravno */
i = (int)a; /* ispravno, ali izbegavati */
a = i; /* neispravno */
a = 0; /* ispravno */
a = a+i; /* ispravno */
a = a+b; /* neispravno */
Primer 6.1.
Zero is the sole exception: the constant zero may be assigned to a pointer,
and a pointer may be compared with the constant zero. The symbolic constant
NULL is often used in place of zero, as a mnemonic to indicate more clearly
that this is a special value for a pointer. NULL is defined in <stdio.h>. We
will use NULL henceforth.
68 6 Pokazivači i adresna aritmetika
Tests like
if (allocbuf + ALLOCSIZE - allocp >= n) { /* it fits */
and
if (p >= allocbuf && p < allocbuf + ALLOCSIZE)
show several important facets of pointer arithmetic. First, pointers may be
compared under certain circumstances. If p and q point to members of the same
array, then relations like ==, !=, <, >=, etc., work properly. For example,
p < q
is true if p points to an earlier element of the array than q does. Any
pointer can be meaningfully compared for equality or inequality with zero. But
the behavior is undefined for arithmetic or comparisons with pointers that do
not point to members of the same array. (There is one exception: the address
of the first element past the end of an array can be used in pointer arithmetic.)
Second, we have already observed that a pointer and an integer may be added
or subtracted. The construction
p + n
means the address of the n-th object beyond the one p currently points to.
This is true regardless of the kind of object p points to; n is scaled according
to the size of the objects p points to, which is determined by the declaration of
p. If an int is four bytes, for example, the int will be scaled by four. Pointer
subtraction is also valid: if p and q point to elements of the same array, and
p<q, then q-p+1 is the number of elements from p to q inclusive.
onda je d[0] (kao i d[1], d[2], ...) pokazivač na int. Pokazivač d[0] sadrži
adresu elementa d[0][0], i, opštije, d[i] sadrži adresu elementa d[i][0].
Vrednost d je tipa int **, ona je pokazivač na pokazivač na int i sadrži adresu
pokazivača d[0].
explicitly, either statically or with code. Assuming that each element of b does
point to a twenty-element array, then there will be 200 ints set aside, plus ten
cells for the pointers. The important advantage of the pointer array is that the
rows of the array may be of different lengths. That is, each element of b need
not point to a twenty-element vector; some may point to two elements, some
to fifty, and some to none at all. Although we have phrased this discussion in
terms of integers, by far the most frequent use of arrays of pointers is to store
character strings of diverse lengths, as in the function month_name. Compare
the declaration and picture for an array of pointers:
char *name[] = { "Illegal month", "Jan", "Feb", "Mar" };
with those for a two-dimensional array:
char aname[][15] = { "Illegal month", "Jan", "Feb", "Mar" };
C:>primer 1 2
Manji je prvi element
C:>primer 12 2
Manji je prvi element
C:>primer 12 2 -n
Manji je drugi element
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
if (argc<3)
{
printf("Nema elemenata za poredjenje\n");
return -1;
}
if (numeric)
rezultat = poredi(argv[1],argv[2],numcmp);
else
rezultat = poredi(argv[1],argv[2],strcmp);
if (rezultat<0)
printf("Manji je prvi element\n");
else if (rezultat==0)
printf("Dva elemenata su jednaka\n");
else
printf("Manji je drugi element\n");
return 0;
}
v1 = atof(s1);
v2 = atof(s2);
if (v1 < v2)
return -1;
else if (v1 > v2)
return 1;
6.5 Pokazivači na funkcije 73
strcmp and numcmp are addresses of functions. Since they are known to be
functions, the & is not necessary, in the same way that it is not needed before
an array name.
The use of comp in the line
((*comp)(a, b]))
is consistent with the declaration: comp is a pointer to a function, *comp is
the function, and
(*comp)(a, b)
is the call to it. The parentheses are needed so the components are correctly
associated; without them,
int *comp(char *, char *) /* WRONG */
says that comp is a function returning a pointer to an int, which is very
different.
74 6 Pokazivači i adresna aritmetika
Glava 7
Dinamička alokacija
memorije
Ne sme se koristiti nešto što je već osloboe.no, ne sme se dva puta oslobaa.ti
ista memorija.
Always check the return value of malloc() and calloc(). Never assume
that memory allocation will succeed. If the allocation fails, malloc() returns
NULL. If you use the value without checking, it is likely that your program will
immediately die from a segmentation violation (or segfault), which is an attempt
to use memory not in your address space. If you check the return value, you
can at least print a diagnostic message and terminate gracefully. Or you can
attempt some other method of recovery.
#include <stdio.h>
#include <stdlib.h>
main()
{
int n, i, *a;
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
free( buffer2 );
exit( 0 );
}
Output
p = (char *) malloc(1000);
....
p = (char *) malloc(5000);
Initially, 1000 bytes are dynamically allocated and the the address of those
bytes is stored in p. later 5000 bytes are dyanmically allocated and the address
is stored in p. However, the original 1000 bytes have not been returned to the
system using free.
Memory leak actually depends on the nature of the program.
Any dynamic memory that’s not needed should be released. In particular,
memory that is allocated inside loops or recursive or deeply nested function calls
should be carefully managed and released. Failure to take care leads to memory
leaks, whereby the process’s memory can grow without bounds; eventually, the
process dies from lack of memory.
This situation can be particularly pernicious if memory is allocated per input
record or as some other function of the input: The memory leak won’t be noticed
when run on small inputs but can suddenly become obvious (and embarrassing)
when run on large ones. This error is even worse for systems that must run
continuously, such as telephone switching systems. A memory leak that crashes
such a system can lead to significant monetary or other damage.
Even if the program never dies for lack of memory, constantly growing pro-
grams suffer in performance, because the operating system has to manage keep-
ing in-use data in physical memory. In the worst case, this can lead to behavior
known as thrashing, whereby the operating system is so busy moving the con-
tents of the address space into and out of physical memory that no real work
gets done.
Većina debagere detektuje da u programu postoji curenje memorije, ali ne
može da pomogne u lociranju odgovarajuće greške u kodu. Postoje specijalizo-
vani programi (memory leaks profiler) koji olakšavaju otkrivanje curenje mem-
orije.
Freeing the same pointer twice This causes ”undefined behavior.”Once the
memory has been handed back to the allocation routines, they may merge
the freed block with other free storage under management. Freeing some-
thing that’s already been freed is likely to lead to confusion or crashes
at best, and so-called double frees have been known to lead to security
problems.
heap. Zeros indicate free memory blocks and ones indicate memory blocks that
are in use: 100101010000101010110
The above heap is highly fragmented. Allocating a memory block that con-
tains five units (i.e., five zeros) will fail, although the systems has 12 free units in
total. This is because the free memory isn’t contiguous. On the other hand, the
following heap has less free memory but it’s not fragmented: 1111111111000000
malloc() is a nightmare for embedded systems. As with stacks, figuring the
heap size is tough at best, a problem massively exacerbated by multitasking.
malloc() leads to heap fragmentation though it may contain vast amounts
of free memory, the heap may be so broken into small, unusable chunks that
malloc() fails.
In simpler systems its probably wise to avoid malloc() altogether. When
theres enough RAM allocating all variables and structures statically yields the
fastest and most deterministic behavior, though at the cost of using more mem-
ory.
When dynamic allocation is unavoidable, by all means remember that malloc()
has a return value! Fact is, it may fail, which will cause our program to crash
horribly. If were smart enough proactive enough to test every malloc() then
an allocation error will still cause the program to crash horribly, but at least we
can set a debug trap, greatly simplifying the task of finding the problem.
The programmer should concentrate on optimizing busy heap allocation
sites.
What can you do to avoid heap fragmentation? First, use dynamic memory
as little as possible. Secondly, try to allocate and de-allocate large chunks rather
than small ones. For example, instead of allocating a single object, allocate an
array of objects at once. As a last resort, use a custom memory pool.
the array may well not even have a name; it might instead be obtained by calling
malloc or by asking the operating system for a pointer to some unnamed block
of storage.
The other information needed is how much of allocbuf has been used. We
use a pointer, called allocp, that points to the next free element. When alloc
is asked for n characters, it checks to see if there is enough room left in allocbuf.
If so, alloc returns the current value of allocp (i.e., the beginning of the free
block), then increments it by n to point to the next free area. If there is no room,
alloc returns zero. afree(p) merely sets allocp to p if p is inside allocbuf.
#define ALLOCSIZE 10000 /* size of available space */
7.7 Hip
Svaki program ima prostor raspoložive memorije koju može da koristi za
vreme izvršavanja. Ovaj prostor raspoložive memorije naziva se slobodna mem-
orija ili hip. Alokacija memorije u fazi izvršavanja naziva se dinamička alokacija
memorije. Ona se postiže funkcijama kao što je malloc. Objekti koji su aloci-
rani u slobodnom memorijskom prostoru nisu imenovani. malloc ne vraća
stvarni alocirani objekat, već njegovu adresu. Preko te adrese se indirektno
manipuliše objektom. Kada završimo sa korišćenjem objekta, moramo da ek-
splicitno vratimo memoriju tog objekta u slobodnu memoriju. To se postiže
funkcijom free. free se ne poziva nad adresama objektata koji nisu alocirani
dinamički.
The heap segment provides more stable storage of data for a program; mem-
ory allocated in the heap remains in existence for the duration of a program.
The memory allocated in the heap area, if initialized to zero at program start,
remains zero until the program makes use of it. Thus, the heap area need not
contain garbage.
The heap is where dynamic memory (obtained by malloc() and friends)
comes from. As memory is allocated on the heap, the process’s address space
grows, as you can see by watching a running program with the ps command.
Although it is possible to give memory back to the system and shrink a process’s
address space, this is almost never done and this leads to memory fragmentation.
It is typical for the heap to ”grow upward.”This means that successive items
that are added to the heap are added at addresses that are numerically greater
than previous items. It is also typical for the heap to start immediately after
the data segment.
Main characteristics of the heap:
Traditionally in a process address space you would see stack growing upside
down and heap growing upwards. An application wise difference is all the mem-
ory allocated dynamically gets allocated on heap viz. malloc free, calloc etc.
The stack of an process contains stack frames( containing the return address
linkage for a function and the data required in a stack which has the qualifier
’auto’).
Although it’s theoretically possible for the stack and heap to grow into each
other, the operating system prevents that event, and any program that tries
to make it happen is asking for trouble. This is particularly true on modern
systems, on which process address spaces are large and the gap between the top
of the stack and the end of the heap is a big one. The different memory areas
can have different hardware memory protection assigned to them. The details,
of course, are hardware and operating-system specific and likely to change over
time. Of note is that both Standard C and C++ allow const items to be placed
in read-only memory.
8.1 Liste
Linked lists are the most basic self-referential structures. Linked lists allow
you to have a chain of structs with related data.
A linked list is composed of nodes, each of which contains a piece of data
and a reference to the next node.
So how would you go about declaring a linked list? It would involve a struct
and a pointer:
struct llnode
{
<type> data;
struct llnode *next;
};
The <type> signifies data of any type. This is typically a pointer to some-
thing, usually another struct. The next line is the next pointer to another
llnode struct. Another more convenient way using typedef:
88 8 Dinamičke strukture: liste i stabla
add(&head, some_data);
(Napomena: umesto (*tmp).data može da se piše tmp->data (isto važi za
bilo koji pokazivač na strukturu, dakle umesto (*a).member može da se piše
a->member).)
What’s happening here? We created a head pointer, and then sent the
address-of the head pointer into the add function which is expecting a pointer to
a pointer. We send in the address-of head. Inside add, a tmp pointer is allocated
on the heap. The data pointer on tmp is moved to point to the data_in. The
next pointer is moved to point to the head pointer (*head). Then the head
pointer is moved to point to tmp. Thus we have added to the beginning of the
list.
Removals: You traverse the list, querying the next struct in the list for the
target. If you get a match, set the current target next’s pointer to the pointer
of the next pointer of the target. Don’t forget to free the node you are removing
8.1 Liste 89
(or you’ll get a memory leak)! You need to take into consideration if the target
is the first node in the list. There are many ways to do this (i.e. recursively).
Think about it!
Traversals: Traversing list is simple, just query the data part of the node
for pertinent information as you move from next to next. What about freeing
the whole list? You can’t just free the head pointer! You have to free the list.
A sample function to free a complete list:
void freelist(llnode *head)
{
llnode *tmp;
while (head != NULL)
{
free(head->data); /* Don’t forget to free memory within the list! */
tmp = head->next;
free(head);
head = tmp;
}
}
Now we can rest easy at night because we won’t have memory leaks in our
lists!
Stacks have specific adds and removes called push and pop. Pushing nodes
onto stacks is easily done by adding to the front of the list. Popping is simply
removing from the front of the list. It would be wise to give return values when
pushing and popping from stacks. For example, pop can return the struct that
was popped.
The idea is to think of your data as a stack of plates or books where you can
only take the top item off the stack in order to remove things from it.
We can implement a stack with a linked list.
X Y ... A NULL
novi element
Y X V ... A NULL
X V ... A NULL
queue: The first jobs submitted are printed before jobs that are submitted after
them. You can think of it as a line in a grocery store. The first one in the line
is the first one to be served. A queue is called a FIFO (First In First Out) to
demonstrate the way it accesses data.
Red se može ilustrovati i na primereu čekaonice u kojoj svako zna ko je na
redu posle njega.
A queue has two basic operations:
A B ... X NULL
novi element
početak reda
novi kraj reda
A B ... X Y NULL
B ... X Y NULL
...
NULL A B C ... Z NULL
A B C ... Z
Primer 8.1. Naredni primer ilustruje rad sa listama. Lista koja se koristi
je jednostruko povezana i za nju je implementirano više funkcija nego što je
neophodno za stek i red (npr. implentirane su funkcije i za ubacivanje na početak
i za ubacivanje na kraj liste).
94 8 Dinamičke strukture: liste i stabla
#include <stdio.h>
#include <stdlib.h>
/* --------------------------------------------- */
void ispisi_listu_unazad(CVOR* l)
{
8.2 Stabla 95
8.2 Stabla
Trees are natural structures for representing certain kinds of hierarchical
data. A (rooted) tree consists of a set of nodes (or vertices) and a set of arcs (or
edges). Each arc links a parent node to one of the parent’s children. A special
root node has no parent. Every other node has exactly one parent. It is possible
to reach any node by following a unique path of arcs from the root. If arcs are
considered bidirectional, there is a unique path between any two nodes. The
simplest kind of tree is a binary tree where each parent has at most two children.
A simple binary tree involves having two types of ”next”pointers, a left and a
right pointer. You can halve your access times by splitting your data into two
different paths, while keeping a uniform data structure. But trees can degrade
into linked list efficiency. There are different types of trees, some popular ones
are self-balancing. For instance, AVL trees are a typical type of tree that can
move nodes around so that the tree is balanced without a > 1 height difference
between levels. 1
Creating a binary ordered tree from a random data set (also called binary
search tree):
1 Više o stablima pročitati na adresi: http://www.csse.monash.edu.au/ lloyd/tildeAlgDS/Tree/
96 8 Dinamičke strukture: liste i stabla
NULL NULL
NULL NULL
• Compare the second element with the root node, if the new value is less
than the parent’s value, then move left (e.g. Pointer = Pointer->Left).
Otherwise go right.
• Keep on comparing new value and keep on moving (left or right) until you
reach a NULL pointer. Append the new node at that location.
2. If the value we’re looking for == the Pointer value, return the Pointer.
8.2 Stabla 97
12 21
NULL NULL
5 15
3. If the value we’re looking for is < the Pointer value, go left. (e.g. Pointer = Pointer->Left)
And goto (2)
5. If the Pointer is ever NULL, return NULL to indicate that the value was
not found in the tree.
Most of the tree functions can be implemented using recursion. The code is
easily readable and understandable.
98 8 Dinamičke strukture: liste i stabla
Kreiranje elementa
Dodavanje elementa
Insert_Node(Root->Right, New);
}
Napomena: umesto (*New).Value može da se piše New->Value (isto važi za
bilo koji pokazivač na strukturu, dakle umesto (*a).member može da se piše
a->member).
Pronalaženje elementa
2. If the value we’re looking for == the Pointer value, return the Pointer.
3. If the value we’re looking for is < the Pointer value, go left. (e.g. Pointer = Pointer->Left)
And goto (2)
5. If the Pointer is ever NULL, return NULL to indicate that the value was
not found in the tree.
100 8 Dinamičke strukture: liste i stabla
Primer 8.2. Primer za vežbu: napraviti ispis elemenata stabla kao u stablu
koje prikazuje direktorijume (npr. u Windows Exploreru)
8.2 Stabla 101
Strukturna dekompozicija i
druga načela pisanja
programa
Ultimately,1 the only way a piece of software can impact the world is by doing
something useful. People learn that fact in colleges and try to make working
software. For them, the important aspects of code become, in the order of
priority, getting the task done, and getting it done correctly. As they get more
experience, they realize that there are other important aspects of software:
• It costs more to maintain and modify the software than develop the soft-
ware.
All in all, we feel that the primary purpose of the code is to communicate to
the other developers within the confines of a compiler specified medium. Even
though readability is an auxiliary goal, aiming for it lets us get to the primary
goal: working code that is correct.
Normally, as soon as people start a project, they agree on a set of coding
guidelines. These coding guidelines spell out, in elaborate detail, what the
syntactic conventions are: how to use underscores, camelHump style, and how
to indent. While these guidelines are of good intentions, it ends up getting
ignored for several reasons: they do not explain why these rules make sense;
they do not say which rules are more important; they do not equip the coder
with the rationale behind the rules.
1 Zasnovano na tekstu Ramarao Kanneganti: Best Practices in Writing Readable Code
106 9 Strukturna dekompozicija i druga načela pisanja programa
indent, and it will produce the indented program. It has gazillion options for
you to tinker to produce the kind of output you want. For Java, there is jalopy
that does all this and much more.
sq_n += 2*i - 1;
if (sq_n > MAX_VAL)
{
printf("Exceeded size");
exit(1);
}
}
Without Excessive blank lines:
for (i = n; i > 0; i --) {
sq_n += 2*i - 1;
if (sq_n > MAX_VAL) {
printf("Exceeded size");
exit(1);
}
}
Blank lines create the effect of paragraphs in the code; thus they should be
used to group all the statements that belong in a logical unit. If you can write
a comment on what the next group doing and why, then perhaps that group of
statements deserves to be together.
Excessive blank lines rob the legitimacy of the needed breaks. In addition,
they take up valuable real estate on the screen.
Along with the žeroštatement lines, we need to be concerned with more
than one statement for a line also. Conventional wisdom states that number
of statements to a line should be restricted to one. The exceptions are sim-
ple initializations and simple statements. For example, int i=10; int j=20
can be on one line. In fact, leaving those statements on one line focuses the
reader better. However, if the initializations need explanations, place them on
a separate line.
108 9 Strukturna dekompozicija i druga načela pisanja programa
• Avoid using generic names such as User, customer. Use specific names just
as in writing prose. If a system has several kind of users, use appropriate
name as in administrator or clerk and so on. If a function computes av-
erages, name it as "computeAverages" instead of "doTask". If a boolean
variable tests if the cart is empty, call it ”isCartEmpty”instead of ”flag”.
The real test is always this: If you can read the code over the phone
so that listener can comprehend it, then the naming has achieved its objec-
tive. For example, naming the procedure as a verb like šetupChannel”reads
better. Naming a function such as a = average(1,2,3) reads better than
a = computeAverage(1,2,3).
9.3 Komentari
Several good books have been written about this subject out of which I would
recommend Čode Complete”. The fundamental principle is that in general code
should convey all the meaning to the readers. However, syntactic constraints
may not make it possible. More over, unlike a book where we can recast the
matter for different audiences, we cannot write the code for different audience.
In several such cases, you can add comments to the code as an extension to the
code.
Writing comments should not provide an excuse to write poorly structured
code. Comments can only add information at a different level.
• Comments should tell the reader why and what you are doing it, rather
than how.
• In case the code is tricky, explain the how, and tell them explicitly why it
is tricky.
programs such as ”grepšo that they can be acted upon. Most popular markers
are:
Instead, the following works well. It is one line and it clubs all of them
into one TODO.
//TODO: Add second address field to the customer class.
• FIXME marker: In the code, there are several places, you may take short
cuts knowing fully well that it needs to be fixed later. In such cases, you
can use FIXME marker to denote that activity. While TODO denotes
unfinished activity, FIXME denotes code that needs fixing later on. For
example, consider the following:
//FIXME: Used only 50 chars for the name. Make it dynamic string.
char name[50];
• XXX Marker: This marker is private marker. It is generally used only for
the developer to be deleted by the time he is done. It is generally used as
note to the developer.
• File length should not exceed a few hundred lines. Normal recommenda-
tion is 200 or so. However, if logic demands that the file should be larger,
try using auxiliary classes or auxiliary courses.
• Make sure each file contains at least sizable information. Excessive frag-
mentation of a program through large number of files can make it difficult
to read.
• If a group of developers are working on the project, make sure that a file is
under control of one developer. That is, to understand and work with that
file it should not take knowledge from disparate areas of the the domain
or programming. For example, if a file contains knowledge about SQL,
parsing, and numerical computing, there are not many people who know
enough about these areas to own the file.
9.4.2 Modularnost
There are several ways the application can be made modular. Files, classes,
and other structural elements help to organize the program.
One important purpose in organizing the program is to isolate the moving
parts. That is, any program contains mature code and the code that is un-
dergoing rapid transformation. It contains fixed entities and the entities that
need customizing. A good coding style separates these so that customizing,
modifying, and extending programs becomes easy.
Since customization and extensions of the code typically deals with a small
subset of the code, it is a good idea to isolate that code. Use separate functions
and files that encapsulates the changing code.
Still bad. What if we use the length in multiple places and we want to change
it to 75? It would read
#define FIFTY 75.
We can make it more readable by:
#define arrayLen 50 char streetName[arrayLen];
Even this can be made better by referring to the constant by its semantic
significance. For example, it can be street name length. In which case, it be
made:
#define STREETNAMELENGTH 50 streetName char[STREETNAMELENGTH];
As the next step, you collect all these constants into a separate file and
through #include mechanism to use in other parts of the code. Of course, you
should use language facilities to define constants instead of #define. So the
next version will be:
const int STREETNAMELENGTH=50 /* To be placed in a central file.
*/ char streetName[STREETNAMELENGTH]; /* Declare it where used.
*/
ware Engineering
114 9 Strukturna dekompozicija i druga načela pisanja programa
the system to where it leaves the system can be traced. Data-flow design is
an integral part of a number of design methods and most CASE tools support
data-flow diagram creation. Different methods may use different icons to rep-
resent data-flow diagram entities but their meanings are similar. The notation
which I use is based on the following symbols:
exceptional logic and language objects into functions that do not clutter
the mainline code.
// This code gets executed when the patient visits for the first time
// Get the patient information
currentPatient = Patient.getPatient(currentSession.getSSN());
// If the patient is not there in the system, add him to the system.
if (currentPatient == null) {
currentPatient=createPatient(currentSession); }
// Obtain the purpose of the visit
currentVisit = new Visit(currentPatient);
currentVisit.recordPurpose(currentSession.getPurpose());
//Enter the name of the provider
currentVisit.recordProvider(currentSession.getProvider());
//Verify if the insurer is still current.
currentPatient.verifyInsurer(currentSession);
// Advise the rights, and record that information.
currentPatient.adviseRights(PatientRights.getRights(currentPatient),
currentSession.getOperator());
Depending on the objects you may have and the complexity of the code, this
can result in a few hundreds of lines. For example, the simple function getSSN,
may become something like:
// Get the Social Security Number from the user and validate
SSN = getInput("Enter SSN:"); if (isValid(SSN)) return SSN; for (int
i = 0; i < MAX_NUMBER_OF_TRIES, i++) {
SSN = getInput("Invalid SSN. Enter SSN:");
if (isValid(SSN)) return SSN;
} throw InvalidInput("Invalid SSN");
While this code is certainly needed, if we placed it in the main code, it would
clutter up the rest of the code. By separating it out, we have achieved our two
purposes: Reuse (in case we need to get the SSN from the user somewhere
else), and Readability. Notice that validating SSN is pushed into some other
routine to separate such non-domain logic from a domain specific constraints
(max number of times you can ask for SSN) achieves the same goals. Here are
the rules of thumb with this approach:
Have functions that are smaller than 25 lines. Best way to create new func-
tions is to abstract some part of the problem (domain, technical, or linguistic)
and provide a function for that. It always should be possible to get such an
abstraction going. Use the English description you write as you refine the code
as comments. It tells you the domain level activities that tell the reader what
you are doing. For complex logic functions, include the algorithm before the
function body and after the comment section. If the function is large, break
down into blocks, where each block is doing some unique activity. Use one line
comment describe that activity. Use appropriate formatting scheme to cut down
on excessive lines. For examples, ornate commenting scheme is not good. Plac-
ing empty lines that does not indicate some semantic separation to the reader
is not good. Also, use K&R scheme of indenting to maximize the information
118 9 Strukturna dekompozicija i druga načela pisanja programa
to lines ratio.
• The role of functions near the top of the structural hierarchy may be to
control and coordinate a set of lower-level functions.
• Each node in the structure chart should have between two and seven sub-
ordinates. If there is only a single subordinate, this implies that the unit
represented by that node may have a low degree of cohesion. The compo-
nent may not be single function. A single subordinate means that another
function has been factored out. If a node has too many subordinates, this
may mean that the design has been developed to too low a level at that
stage.
9.5 Pisanje programa: dizajniranje programa 119
Three process steps, which follow these guidelines, can be identified for the
transformation process from data-flow diagram to structure chart:
10.1 Povezivanje
U slučaju da je izvorni program sačinjen od nekoliko jedinica prevodenja1 ,
svaka jedinica prevodenja se kompilira nezavisno, a tek povezivač (eng. linker)
dobijene objektne module povezuje u jedan izvršni program. U ovom poglavlju
će biti reči o vrstama povezivanja promenljivih (eng. linkage of identifiers) i
funkcije koje odreduje medusobni odnos izmedu promenljivih i funkcija defin-
isanih u različitim jedinicama prevodenja.
Jezik C razlikuje identifikatore bez povezivanja, identifikatore sa spoljašnjim
povezivanjem (eng. external linkage) i identifikatore sa unutrašnjim poveziva-
njem. Identifikatori bez povezivanja nisu vidljivi prilikom procesa povezivanja
i mogu da se ponavljaju u različitim jedinicama prevodenja. Svaka deklaracija
identifikatora bez povezivanja odreduje jedinstveni nezavisni objekat. Sve de-
klaracije identifikatora sa spoljašnjim povezivanjem u skupu jedinica prevodenja
odredjuju jedinstveni objekat, tj. sve pojave ovakvog identifikatora u različitim
jedinicama prevodenja se odnose na jedan isti objekat. Sve deklaracije iden-
tifikatora sa unutrašnjim povezivanjem u okviru jedne jedinice prevodenja se
odnose na isti objekat. Identifikatori sa unutrašnjim povezivanjem se ne mogu
koristiti kroz različite jedinice prevodenja.
#include <stdio.h>
main()
{
int n;
n = spoljasnja_promenljiva;
while(scanf("%d",&n)==1)
printf("\n %d na kvadrat je %d\n\n",n,kvadrat(n));
}
i
int spoljasnja_promenljiva=0;
int kvadrat(int n)
{
return n*n;
}
i ako se one zajedno kompiliraju, kompilacija će biti uspešna.
Ako je sadržaj dve datoteke:
#include <stdio.h>
main()
{
int n;
n = spoljasnja_promenljiva;
while(scanf("%d",&n)==1)
printf("\n %d na kvadrat je %d\n\n",n,kvadrat(n));
}
}
i
static int spoljasnja_promenljiva=0;
int kvadrat(int n)
{
return n*n;
}
i ako se one zajedno kompiliraju, kompilacija neće biti uspešna. Razlog je u
static kvalifikatoru promenljive spoljasnja_promenljiva koji odreduje njeno
unutrašnje povezivanje i time zabranjuje njeno korišćenje u okviru drugih mod-
124 10 Programi koji se sastoje od više datoteka
main()
{
int n;
n = spoljasnja_promenljiva;
while(scanf("%d",&n)==1)
printf("\n %d na kvadrat je %d\n\n",n,kvadrat(n));
}
}
datoteku koja sadrži definiciju funkcije kvadrat:
#include "kvadrat.h"
int kvadrat(int n)
{
return n*n;
}
i datoteku kvadrat.h koja sadrži deklaraciju promenljive spoljasnja_promenljiva
i deklaraciju funkcije kvadrat:
int spoljasnja_promenljiva=0;
int kvadrat(int);
Prilikom kompilacije prve datoteke, uključuje se sadržaj datoteke kvadrat.h
i nema potrebe za extern deklaracijama. Slično važi i za drugu datoteku, a
povezivanjem njihovih prevedenih verzija dobija se ispravan izvršni program.
#include lines. There are often several #include lines at the beginning of a
source file, to include common #define statements and extern declarations, or
to access the function prototype declarations for library functions from headers
like <stdio.h>. (Strictly speaking, these need not be files; the details of how
headers are accessed are implementation-dependent.)
#include is the preferred way to tie the declarations together for a large
program. It guarantees that all the source files will be supplied with the same
definitions and variable declarations, and thus eliminates a particularly nasty
kind of bug. Naturally, when an included file is changed, all files that depend
on it must be recompiled.
For example, to make sure that the contents of a file hdr.h are included only
once, the contents of the file are surrounded with a conditional like this:
#if !defined(HDR)
#define HDR
#endif
The first inclusion of hdr.h defines the name HDR; subsequent inclusions will
find the name defined and skip down to the #endif. A similar style can be used
to avoid including files multiple times. If this style is used consistently, then
each header can itself include any other headers on which it depends, without
the user of the header having to deal with the interdependence. This sequence
tests the name SYSTEM to decide which version of a header to include:
126 10 Programi koji se sastoje od više datoteka
#endif
Glava 11
Uvod u prevodenje
programskih jezika:
poredjenje interpretera i
kompilatora; faze u
prevodjenju
Back End
Target Language
Front End:
• Lexical Analyzer
• Syntax Analyzer
• Semantic Analyzer
Front End:
• Code Optimizer
Overall structure:
Source Language
Lexical Analyzer
Syntax Analyzer
Semantic Analyzer
Intermetiate Code Generator
Intermediate Code
Code Optimizer
Target Code Generator
Target Language
11 Uvod u prevodenje programskih jezika: poredjenje interpretera i
130 kompilatora; faze u prevodjenju
Expr
Expr Op Expr Int Int 2 - 1
11.7 Primer
• Source Code:
cur_time = start_time + cycles * 60
• Lexical Analysis:
ID(1) ASSIGN ID(2) ADD ID(3) MULT INT(60)
• Syntax Analysis:
ASSIGN
ID(1) ADD
ID(2) MULT
ID(3) INT(60)
• Sematic Analysis:
11 Uvod u prevodenje programskih jezika: poredjenje interpretera i
132 kompilatora; faze u prevodjenju
ASSIGN
ID(1) ADD
ID(2) MULT
ID(3) int2real
INT(60)
• Intermediate Code:
• Optimized Code :
Step 1:
temp1 = 60.0 temp2 = id3 * temp1 temp3 = id2 + temp2 id1 = temp3
Step 2:
Step 3:
Optimized Code:
MOVF id3, R2 MULF #60.0, R2 MOVF id2, R1 ADDF R2, R1 MOVF R1, id1
Deo IV
Socijalni aspekti
informatike
Glava 12
Istorijski i društveni
kontekst računarstva kao
naučne discipline; društveni
značaj računara i Interneta;
profesionalizam, etički
kodeks; autorska prava;
intelektualna svojina,
softverska piraterija
– 1) why be ethical?
– 2) major ethical models,
– 3) definition of computing as a profession, and
– 4) codes of ethics and professional responsibility for computer pro-
fessionals.
• Ethical and Legal Issues of Data Data Protection legislation, security and
privacy of data issues.
• Ethical and Legal Issues of Information Systems Organisational and social
issues of IS, their construction and their effects. Security and privacy issues
also enter here, as do legal and issues of professional responsibility
• Professional Responsibility, Codes of Conduct
Mikro-situacije:
• Encryption Policy
• Computers in the workplace: effects on employment, telecommuting, em-
ployee monitoring, e-mail privacy.