Sunteți pe pagina 1din 13

CAPITOLUL 2

ALGORITMI

1. CONCEPTUL DE ALGORITM

În capitolul introductiv am definit algoritmul ca fiind o multime de pasi care descriu modul de
îndeplinire a unei sarcini. Vom analiza acum mai îndeaproape acest concept fundamental. Vom începe
prin a sublinia distinctia care trebuie facuta între algoritm si reprezentarea sa - o distinctie asemanatoare
celei între subiectul unei carti si cartea propriu-zisa. Subiectul este un concept abstract, în timp ce cartea
este reprezentarea fizica a acestuia. Daca trad ucem cartea în alta limba sau o publicam în alt format, ceea
ce se schimba este reprezentarea - subiectul ramâne acelasi.
La fel, algoritmul este abstract si trebuie diferentiat de reprezentarea lui. De exemplu, algoritmul
de conversie a temperaturii din grade Celsius în grade Fahrenheit este reprezentat de obicei printr-o
formula algebrica:
F = (9/5)C + 32
Dar la fel de bine ar putea fi reprezentat si astfel:
Se înmulteste temperatura exprimata în grade Celsius cu 9/5 si se aduna 32 la produsul
obtinut.
sau chiar sub forma unui circuit electronic. În toate aceste cazuri, algoritmul este acelasi; doar
reprezentarea difera.
În acest context, al distinctiei ce trebuie facuta între algoritmi si reprezentarea lor, am dori sa
clarificam si diferenta între alte doua concepte înrudite - programe si procese. Programul este
reprezentarea unui algoritm; într-adevar, specialistii utilizeaza termenul de program pentru a desemna
reprezentarea formala a unui algoritm proiectat în vederea unei aplicatii informatice. În cap itolul 3 am
definit procesul ca fiind activitatea de executie a programului. Observati însa ca executia unui program
înseamna de fapt executia algoritmului reprezentat de programul respectiv, prin urmare am putea defini
procesul ca fiind activitatea de executie a unui algoritm. În concluzie, procesele, algoritmii si programele
sunt entitati înrudite, dar distincte.
Sa analizam acum definitia formala a algoritmului, prezentata în figura 2.1. Vom începe cu
cerinta ca pasii care formeaza un algoritm sa fie ordonati, ceea ce înseamna ca algoritmul trebuie sa aiba o
structura bine precizata, mai exact o ordine clara de executie a pasilor. Aceasta nu înseamna neaparat ca
algoritmul trebuie executat în ordinea primul pas, al doilea pas etc. Exista algoritmi, numiti algoritmi
paraleli, care contin mai multe secvente de pasi, fiecare secventa urmând a fi executata de alt procesor în
cadrul unei masini multiprocesor. În acest caz, algoritmul general nu se compune dintr-un singur fir de
executie, care sa respecte scenariul pasul unu, pasul doi etc., ci din mai multe fire care se ramifica si se
reunesc pe masura ce diferitele procesoare executa parti ale sale. Un alt exemplu -lar putea constitui
circuitele basculante bistabile, în cazul carora fiecare poarta executa un pas al algoritmului general, pasii
fiind ordonati de la cauza la efect (actiunea fiecarei porti se propaga în întregul circuit).

Un algoritm consta dintr-o multime ordonata de pasi executabili,


descrisi fara echivoc, care definesc un proces finit.

Figura 2.1 Definitia algoritmului.

În continuare vom analiza cerinta ca algoritmul sa fie compus din pasi executabili. Fie
urmatoarele instructiuni:
Pasul 1. Construiti o lista cu toti întregii pozitivi.
Pasul 2. Sortati lista în ordine descrescatoare (de la cel mai mare la cel mai mic).
Pasul 3. Extrageti primul întreg din lista rezultata.
Aceste instructiuni nu descriu un algoritm, deoarece pasii 1 si 2 sunt imposibil de executat: nu se
poate construi o lista cu toti întregii pozitivi si întregii pozitivi nu pot fi asezati în ordine descrescatoare.
Alta cerinta impusa de definitia din figura 2.1. este ca pasii algoritmului sa fie descrisi fara
echivoc. Aceasta înseamna ca în timpul executiei algoritmului, informatiile din fiecare stare a procesului
trebuie sa fie suficiente pentru a determina unic si complet actiunile necesare la fiecare pas. Cu alte
cuvinte, executia algoritmului nu trebuie sa necesite creativitate, ci doar capacitatea de a urma
instructiunile.

24
Distinctia dintre algoritm si reprezentarea sa devine aici relevanta. Adesea, ambiguitatile specifice
unei reprezentari sunt considerate în mod gresit a fi ambiguitati ale algoritmului. Un exemplu este nivelu l
de detaliere care apare la descrierea unui algoritm. Pentru un meteorolog, instructiunea „Transforma
temperatura exprimata în grade Celsus în echivalentul ei Fahrenheit” este suficienta, dar pentru un profan
e nevoie de o descriere mai detaliata; acesta din urma va considera instructiunea respectiva ca fiind
ambigua. Observati ca problema nu este ca algoritmul admite echivoc, ci ca reprezentarea lui nu este
suficient de detaliata pentru un profan. Este vorba asadar mai degraba de o ambiguitate a reprezentarii,
decât de una a algoritmului. În sectiunea urmatoare vom vedea ca astfel de ambiguitati aparute în
reprezentarea algoritmilor pot fi rezolvate prin intermediul primitivelor.
Cerinta ca algoritmul sa scrie un proces finit înseamna ca executia algoritmului trebuie sa aiba un
sfârsit. Originea acestei caracteristici este teoria informaticii, al carei scop este sa raspunda la întrebari de
genul „Care sunt limitele masinilor algoritmice?”. Informatica încearca sa faca distinctia între problemele
ale caror raspunsuri pot fi obtinute pe cale algoritmica si problemele care depasesc capacitatea sistemelor
algoritmice. În acest context se traseaza o linie de demarcatie între procesele care se termina cu un
raspuns si cele care se executa la infinit, fara a se ajunge la vreun rezultat.
Cu alte cuvinte, cerinta ca algoritmul sa determine un proces finit elimina procesele infinite, care
nu conduc la rezultate concrete. De exemplu, o instructiune de genul „Repeta acest pas” nu are nici o
utilitate practica. Totusi, procesele infinite îsi au si ele aplicatiile lor, cum ar fi monitorizarea semnelor
vitale ale unui pacient sau mentinerea unui avion la o anumita altitudine. Unii vor spune ca aceste actiuni
implica mai degraba repetar ea unor algoritmi finiti, care au un sfârsit, dupa care sunt reluati în mod
automat. Altii vor considera însa ca asemenea argumente în favoarea încadrarii într-o definitie formala
sunt fortate.
Indiferent de ce parte ne situam în aceasta disputa, cert este ca termenul de algoritm este folosit
adesea, într-o acceptiune mai putin riguroasa pentru a descrie procese care nu sunt neaparat finite. De
exemplu, algoritmul de împartire fara rest a numerelor întregi nu termina un proces finit în cazul
particular al împartirii lui 1 la 3.

2. PSEUDOCOD

Deocamdata, vom amâna prezentarea unui limbaj formal de programare în favoarea unui sistem
de notare mai intuitiv, care este cunoscut sub numele de pseudocod. În general, un pseudocod este un
sistem de notare în cadrul caruia ideile pot fi exprimate mai putin formal, sistem utilizat în procesul de
dezvoltare a algoritmului.
O metoda de a obtine pseudocod este de a relaxa pur si simplu regulile limbajului formal în care
va fi exprimata versiunea finala a algoritmului. Aceasta abordare este folosita de obicei atunci când se
cunoaste de la început limbajul tinta; pseudocodul utilizat în primele faze de dezvoltare a programului
consta din structuri sintactice si semantice asemanatoare celor utilizate în limbajul tinta, dar mai putin
formale.
Ceea ce ne propunem însa aici este sa discutam despre dezvoltarea si reprezentarea algoritmilor
fara a ne opri asupra unui anumit limbaj de programare, de aceea vom urmari sa dezvoltam un sistem de
notare consistenta si concisa pentru repr ezentarea structurilor semantice care se repeta. La rândul lor,
aceste structuri vor deveni primitive cu ajutorul carora vom încerca sa exprimam alte idei.
O structura foarte des întâlnita în cadrul algoritmilor este selectarea între doua activitati în functie
de îndeplinirea sau neîndeplinirea unei conditii. Iata câteva exemple:
Daca produsul intern brut a crescut, cumpara actiuni; altfel, vinde actiuni.
Cumpara actiuni daca produsul intern brut a crescut si vinde în caz contrar.
Cumpara sau vinde actiuni, în functie de cresterea sau de scaderea produsului intern brut.
Fiecare dintre aceste instructiuni poate fi rescrisa astfel încât sa respecte structura
if (conditie) then (activitate)
else (activitate)
în care am folosit cuvintele cheie if (daca), then (atunci) si else (altfel) pentru a introduce
substructurile din cadrul structurii principale si paranteze pentru a delimita granitele acestor substructuri.
Adoptând pentru pseudocodul nostru aceasta structura sintactica, am obtinut o metoda uniforma de
exprimare a unei structuri semantice des întâlnite.
Desi unii vor prefera fraza:
În functie de tipul de an (obisnuit sau bisect), se împarte totalul la 365, respectiv 366, noi vom opta
întotdeauna pentru exprimarea directa:
25
if (anul este bisect)
then (împarte totalul la 366)
else (împarte totalul la 365)
De asemenea, în cazurile în care activitatea alternativa (else) lipseste, vom sintaxa mai scurta
if (conditie) then (activitate)
În acest fel, instructiunea
În cazul în care se constata o scadere a vânzarilor, reduceti pretul cu 5%.
se va reduce la
if (vânzarile au scazut) then (reduceti pretul cu 5%)
O alta structura algoritmica des întâlnita este executarea repetata a unei instructiuni sau a unei
secvente de instructiuni atâta timp cât o anumita conditie este adevarata. De exemplu
Cât timp mai sunt bilete, vindeti bilete
si
În cazul în care mai sunt bilete, continuati sa vindeti bilete
În asemenea cazuri, vom adopta în pseudocod modelul
while (conditie) do (activity)
În esenta, o astfel de instructiune înseamna verificarea conditiei si, daca aceasta este îndeplinita,
executarea activitatii si revenirea la verificarea conditiei, în momentul în care conditia nu este îndeplinita,
se trece la executia instructiunii care urmeaza dupa structura while. Astfel, ambele instructiuni de mai
înainte se reduc la :
while (mai sunt bilete) do (vindeti bilete)
Adeseori vom dori sa ne referim la anumite valori prin intermediul unor nume sugestive. Pentru a
face astfel de asocieri, vom utiliza forma
assign nume the value expresie
unde nume este un nume descriptiv, iar expresie indica valoarea care este asociata numelui
respectiv. De exemplu, instructiunea
assign Total the value Price + Tax
asociaza rezultatul sumei dintre Price si Tax numelui Total.
Adesea, lizibilitatea programului poate fi îmbunatatita prin indentare. De exemplu, instructiunea
if (articolul este impozabil)
then [if (pret > limita)
then (plateste x)
else (plateste y)]
else (plateste z)
este mai usor de înteles decât daca este scrisa în forma echivalenta
if (articolul este taxabil) then [if (pret > limita) then (plateste x)
else (plateste y)] else (plateste z)
De aceea, în pseudocodul nostru vom utiliza indentarea (observati ca pentru a elimina confuzia
produsa de parantezele imbricate am folosit paranteze patrate).
Vrem sa folosim pseudocodul pentru a descrie activitati care sa fie utilizate ca instrumente
abstracte în alte aplicatii. Astfel de unitati de program sunt cunoscute în informatica sub diverse denumiri,
cum ar fi subprogram, subrutina, modul sau functie, fiecare având o anumita semnificatie. Noi vom
adopta termenul de procedura si vom folosi cuvântul procedure pentru a atribui fiecarui modul de
pseudocod un nume. Mai exact, vom începe modulul cu o instructiune de forma
procedura nume
unde nume este numele modulului respectiv. Aceasta instructiune introductiva va fi urmata de
instructiunile care descriu actiunea modulului respectiv. De exemplu, figura 2.2 prezinta pseudocodul
unei proceduri numite Greetings, care tipareste de trei ori mesajul „Hello”.
Atunci când este nevoie de efectuarea sarcinii executate de procedura altundeva în cadrul
pseudocodului, procedura va fi apelata prin numele ei. De exemplu, daca avem doua proceduri numite
ProcessLoan si RejectApplication, atunci pentru a beneficia de serviciile lor în cadrul unei
structuri if-then-else vom scrie
if (...) then (executa procedura ProcessLoan)
else (executa procedura RejectApplication)
ceea ce va duce la executia procedurii ProcessLoan în cazul în care conditia este îndeplinita, sau în
caz contrar la executia procedurii RejectApplication.

26
Procedurile trebuie sa fie cât mai generale posibil. Astfel, o procedura carei sorteaza orice lista de
nume - si nu o lista particulara - trebuie sa fie scrisa astfel încât lista respectiva sa nu fie specificata în
cadrul procedurii, ci sa fie desemnata reprezentarea acesteia sub un nume generic.
În varianta noastra de pseudocod vom adopta conventia de a scrie aceste nume generice între
paranteze, pe acelasi rând cu numele procedurii, în cazul prezentat, procedura numita Sort, proiectata sa
sorteze o lista oarecare de nume, va începe cu instructiunea
procedure Sort (List)
În continuare, în cadrul reprezentarii va fi utilizat acest nume generic ori de câte ori este necesara
o referinta la lista care trebuie sortata. În schimb, atunci când avem nevoie de serviciile procedurii Sort,
vom identifica lista care se substituie listei List în cadrul procedurii si vom scrie ceva de genul
se aplica procedura Sort asupra listei cu membrii organizatiei
sau, dupa caz
aplica procedura Sort asupra listei de invitati la nunta

procedura Greetings
assign Count the value 3;
while Count O do
(tipareste mesajul „Hello” si assign Count the value Count -1)
Figura 2.2 Procedura Greetings, exprimata în pseudocod

Nu trebuie sa pier deti din vedere faptul ca scopul în care folosim pseudocodul nu este sa scriem
un program, ci sa creionam algoritmul. De aceea, putem insera propozitii care sa exprime în limbaj
obisnuit activitati care nu sunt specificate în detaliu. (Modul de rezolvare adetaliilor nu tine atât de
algoritm, cât de caracteristicile limbajului formal în care acesta va fi transpus). Daca o anumita idee apare
însa de mai multe ori în cadrul algoritmului, vom încerca sa adoptam o sintaxa unitara pentru a o
reprezenta, extinzând astfel pseudocodul.

3. STRUCTURI ITERATIVE

În continuare vom studia câteva structuri repetitive folosite în descrierea proceselor algoritmice.
În acest subcapitol ne vom ocupa de structurile iterative, în care o multime de instructiuni se repeta
ciclic , iar în subcapitolul urmator vom prezenta tehnica recursivitatii. În plus, vom prezenta ca exemple
câtiva dintre cei mai populari algoritmi de cautare si sortare - cautarea secventiala si binara si sortarea
rapida - deoarece se constituie în aplicatii ale structurilor repetitive luate în discutie. Vom începe cu
prezentarea algoritmului de cautare secventiala.

Algoritmul de cautare secventiala


Vom lua în discutie problema ocurentei unei anumite valori - valoarea tinta - într-o lista data.
Scopul nostru este sa dezvoltam un algoritm care sa determine daca valoarea respectiva apare sau nu în
lista. Daca gasim valoarea în lista, vom considera ca procesul de cautare s-a încheiat printr-un succes, iar
altfel printr-un esec. Presupunem ca lista a fost sortata pe baza unei reguli oarecare, în functie de tipul
entitatilor componente: daca este o lista de nume vom presupune ca acestea respecta ordinea alfabetica,
iar daca este o lista de numere vom considera ca ele apar în lista în ordine crescatoare.
Pentru a face primul pas, sa ne imaginam ca avem o lista de circa 20 de invitati si ca suntem în
cautarea unui anumit nume. în acest caz, probabil ca vom parcurge lista de la început, comparând fiecare
element din lista cu numele cautat. Daca gasim numele respectiv, cautarea este încununata de succes;
altfel, daca ajungem la sfârsitul listei sau la un nume care, din punct de vedere alfabetic, este „mai mare”
decât numele tinta, cautarea se soldeaza cu un esec. (Nu uitati ca lista este ordonata alfabetic, deci daca
ajungem la un nume mai mare decât numele tinta înseamna ca numele cautat nu apare în lista.). În mare,
ideea este urmatoarea: cautam în lista atâta timp cât mai sunt nume si numele tinta este mai mic decât
numele curent.
În pseudocod, acest proces poate fi reprezentat astfel:
Selecteaza primul articol din lista ca articol de test.
while (valoarea tinta > articolul de test si
mai sunt articole de luat în considerare)
do (Selecteaza urmatorul articol din lista ca articol de test)

27
La iesirea din aceasta structura while fie articolul de test va fi mai mare sau egal cu numele
tinta, fie va fi ultimul nume din lista, în oricare din aceste cazuri, putem determina daca procesul s-a
încheiat cu succes comparând articolul de test cu valoarea tinta. Prin urmare, vom adauga al sfârsitul
rutinei în pseudocod instructiunea.
if (valoare tinta = articolul de test)
then (Declara cautarea un succes.)
else (Declara cautarea un esec.)
În sfârsit, vom observa ca prima instructiune din rutina de cautare se bazea za pe presupunerea (de
altfel foarte rezonabila) ca lista contine cel putin un articol. Pe orice eventualitate, vom plasa însa rutina
pe ramura else a instructiunii
if (Lista este goala)
then (Declara cautarea un esec.)
else (...)
În acest mod vom obtine procedura din figura 4.7. Observati ca aceasta procedura poate fi
utilizata în cadrul altor proceduri prin instructiuni de genul
Aplica procedura Search asupra listei de pasageri pentru a cauta numele Darrel Baker.
pentru a vedea daca Darrel Baker se numara printre pasageri, sau
Aplica procedura Search asupra listei de ingrediente utilizând ca valoare tinta nucsoa ra.
pentru a afla daca nucsoara apare pe lista de ingrediente.
Pe scurt, algoritmul reprezentat în figura 2.3 testeaza articolele în mod secvential, în ordinea în
care apar în lista; de aceea se numeste algoritm de secventiala (sequential search algoritm). Datorita
simplitatii lui, acest algoritm folosit pentru listele de mici dimensiuni, sau atunci când utilizarea lui este
dictata de alte considerente. Însa, asa cum vom vedea imediat, pentru liste de mari dimensiuni exista s i
alte tehnici, mai eficiente.
procedure Search (List, TargetValue)
if (List este goala)
then
(Declara cautarea un esec.)
else
[Selecteaza primul articol din List ca articol de test;
while (TargetValue articolul de test si
mai sunt articole de luat în considerare)
do (Selecteaza urmatorul articol din List ca
articol de test.);
if (TargetValue = articolul de test)
then (Declara cautarea un succes.)
else (Declara cautarea un esec.)]
Figura 2.3 Algoritmul de cautare secventiala, reprezentat în pseudocod

Controlul ciclurilor
Utilizarea repetata a unei instructiuni sau a unei secvente de instructiuni este un foarte important
concept algoritmic. O metoda de implementare a unei astfel de repetitii este structura iterativa cun oscuta
sub numele de ciclu sau bucla (loop), în care o serie de instructiuni numita corpul ciclului este executata
în mod repetat sub supravegherea unui proces de control. Un exemplu tipic este cel care apare în
algoritmul de cautar e secventiala reprezentat în figura 2.3, unde am folosit instructiunea de control
while pentru a controla repetarea unei singure instructiuni: Selecteaza urmatorul articol
din List ca articol de test. Într-adevar, instructiunea while
while (conditie) do (corpul ciclului)
este un exemplu de structura ciclica, deoarece executia ei conduce la parcurgerea ciclica a secventei
testeaza conditia
executa corpul ciclului
testeaza conditia
executa corpul ciclului
?
testeaza conditia
pâna când conditia nu este îndeplinita.
Ca regula generala, utilizarea unei structuri ciclice ofera un grad de flexibilitate mai mare decât
cel care poate fi obtinut prin scrierea de mai multe ori a corpului ciclului. De exemplu, desi structura
ciclica
Executa de trei ori instructiunea „Adauga o picatura de acid sulfuric”.
28
este echivalenta cu secventa
Adauga o picatura de acid sulfuric.
Adauga o picatura de acid sulfuric.
Adauga o picatura de acid sulfuric.
nu putem scrie o secventa similara care sa fie echivalenta cu ciclul descris prin
while (nivelul pH este mai mare de 4) do
(Adauga o picatura de acid sulfuric)
deoarece nu stim dinainte de câte picaturi de acid sulfuric va fi nevoie.
Sa analizam acum mai îndeaproape instructiunea de control. La o prima vedere, poate parea ca
aceasta parte are o mai mica importanta. În definitiv, corpul ciclului este cel care executa de fapt
activitatea propriu-zisa, de exemplu adaugarea pic aturilor de acid, iar partea de control apare doar pentru
ca am ales sa executam corpul ciclului în mod repetat. Cu toate acestea, experienta a aratat ca cel mai
adesea greselile apar în partea de control al buclei, de aceea merita sa-i acordam mai multa atentie.
Controlul unui ciclu consta din trei activitati: init ializare, testare si modificare (asa cum se vede în
figura 2.4), iar pentru ca procesul de ciclare sa se execute succes este necesara prezenta tuturor acestor
activitati. Activitatea de testare are rol de a încheia procesul de ciclare, verificând o conditie care indica
faptul ca proces trebuie sa ia sfârsit. Acesta este scopul pentru care în fiecare instructiune while din
pseudocod am inclus o conditie; corpul ciclului se executa numai atât timp conditia respectiva este
îndeplinita. Prin urmare, conditia de încheiere a procesului de ciclare este negatia conditiei din structura
while.
Celelalte doua activitati au rolul de a asigura (mai devreme sau mai târziu) aparitia conditiei de
încheiere a procesului de ciclare. Prin initializare se stabileste o conditie de pornire, iar prin modificare
aceasta conditie tr ece catre cea de încheier e De exemplu, în figura 2.3 initializarea se face în instructiunea
dinainte de while, care primul articol din lista devine articol de test, iar modificarea se face în corpul
ciclului, când se avanseaza spre sfârsitul listei. Astfel, dupa executia pasiunii initializare, aplicarea
repetata a pasului de modif icare conduce la atingerea conditiei de încheiere (daca nu gasim valoarea tinta,
atunci în cele din urma ajungem la sfârsitul listei).

Initializare: Stabileste o stare initiala care va fi modificata în


vederea atingerii conditiei de încheiere
Testare: Compara starea curenta cu conditia de încheiere si în caz de
egalitate încheie procesul de repetare
Modificare: Schimba starea astfel încât aceasta sa avanseze spre
conditia de încheiere
Figura 2.4 Componentele controlului repetitiv

Este de subliniat faptul ca prin initializare si modificare trebuie sa se atinga conditia de încheiere;
acest aspect este foarte important si atunci când proiectam o astfel structura trebuie sa verificam cu atentie
daca asa stau lucrurile, pentru ca vom da gres chiar si în cele mai simple cazuri. De exemplu, daca
utilizam drept încheiere conditia ca o anumita variabila sa ia valoarea 6, dar initializam variabila cu 1 si o
modificam incrementând-o cu 2, prin ciclare ea va lua valorile 1, 3, 5, 7, 9 etc., dar niciodata 6, deci vom
obtine un ciclu infinit.
Exista doua structuri de tip bucla foarte raspândite, care difera doar prin ordinea în care sunt
executate componentele. Prima structura este cea pe care am folosit-o în instructiunea de pseudocod
while (conditie) do (activitate)
a carei semantica este reprezentata în figura 2.5 printr-o schema logica (flowchart).
Aceste scheme folosesc diferite figuri geometrice pentru a reprezenta pasii algoritmului, si sageti
pentru a indica succesiunea acestora. Pentru tipuri diferite de actiuni se folosesc forme diferite: printr-un
romb se figureaza o decizie, iar printr-un dreptunghi o instructiune oarecare sau o succesiune de
instructiuni. Observati ca în structurii while testarea conditiei de încheiere se face înainte de a se
executa corpul ciclului.

29
Conditie fals
de test
Figura 2.5
Structura ciclului de tip while adevarat

Activitate

În schimb, structura din figura 2.6 solicita sa se execute mai întâi corpul ciclului si de-abia dupa
aceea sa se testeze conditia de încheiere. În cazul acestei structuri alternative, corpul ciclului este
întotdeauna executat cel putin o data, în timp ce în cazul structurii while se poate întâmpla ca el sa nu se
execute niciodata, în eventualitatea ca testul de încheiere este satisfacut chiar de la început.

Activ itate
Figura 2.6
Structura ciclului de tip repeat

fals Conditie
de test

adevarat

Pentru a reprezenta structura din figura 2.6 vom utiliza în pseudocod sintaxa
repeat (activitate) until (conditie)
Observati diferenta: instructiunea
repeat (scoate o moneda din buzunar)
until (nu mai sunt monede în buzunar)
presupune ca la început aveti în buzunar cel putin o moneda, în timp ce
while (mai sunt monede în buzunar)
do (scoate o moneda din buzunar)
nu presupune acest lucru.

Algoritmul de sortare prin inserare


Ca exemplu suplimentar de utilizare a structurilor iterative vom lua în problema sortarii unei liste
de nume în ordine alfabetica. Înainte de a începe, vom preciza care sunt restrictiile pe care trebuie sa le
luam în considerare. Mai propunem sa sortam lista „prin ea însasi”, cu alte cuvinte fara sa folosim
suplimentar de stocare, ci „amestecând” elementele listei. În acest fel eliminam din discutie tehnicile de
construire a unei noi liste, ca o copie sortat a a celei initiale.
Problema este similara aceleia a sortarii unor fise de index împrastiate pe un birou aglomerat: am
facut destul loc pentru fise, dar nu putem da la o parte alte obiecte pentru a capata mai mult spatiu de
manevra. Aceasta restrictie este una tip aplicatiile informatice, nu neaparat pentru ca spatiul de memorie a
masinii ar fi aglomerat, ci pentru ca dorim sa utilizam spatiul disponibil într-un mod eficient.

30
Sa facem un prim pas analizând cum ar decurge sortarea fiselor de pe birou. Sa presupunem ca
avem urmatoarea lista de nume:
Fred
Alice
David
Bill
Carol
O posibila abordare ar fi sa observam ca sublista compusa din primul nume, Fred, este sortata, dar
sublista compusa din primele doua nume, Fred si Alice, nu este. Prin urmare, vom lua fisa care contine
numele Alice, vom introduce în locul ei fisa cu numele Fred si apoi vom depune fisa cu numele Alice în
spatiul liber din vârful listei asa cum se arata prima linie a figurii 2.7. În acest punct, ordinea va fi
Alice
Fred
David
Bill
Carol
Acum primele doua elemente ale listei sunt în ordine, dar nu si primele trei. Vom ridica asadar
cea de a treia fisa, David, vom introduce fisa Fred în spatiul eliberat si vom plasa fisa David în spatiul
lasat de fisa Fred (ca în a doua linie a figurii 2.7). Acum primele trei articole sunt sortate. Continuând în
acelasi mod, vom obtine o lista în care primele patru articole sunt sortate, ridicând fisa cu numele Bill si
împingând în jos fisele Fred si David, iar apoi introducând fisa Bill în spatiul liber (ca în linia a treia a
figurii 2.7). În sfârsit, vom încheia ridicând fisa Carol, mutând fisele Fred si David cu o pozitie în jos si
plasând în spatiul liber fisa Carol (ca în a patra linie a figurii 2.7).
Dupa ce am analizat procesul de sortare a unei liste particulare, va trebui sa generalizam
procedura la orice liste. Pentru a face aceasta vom observa ca toate liniile figurii 2.7 reprezinta cazuri
particulare ale aceluiasi proces: ridicam fisa corespunzatoare primului nume din portiunea nesortata a
listei, împingem numele mai mari decât numele respectiv cu câte o pozitie în jos si inseram fisa extrasa
anterior în spatiul care a ramas liber. Astfel, daca dam articolului extras numele de pivot, vom putea
exprima procedura respectiva în pseudocod dupa cum urmeaza:
Muta pivotul într-o locatie temporara, lasând în List un spatiu gol
while (exista un nume deasupra spatiului si acel nume este mai mare decât pivotul) do
(muta numele de deasupra spatiului în spatiul gol, lasând un spatiu deasupra sa)
Muta pivotul în spatiul gol din List
În continuare, vom observa ca acest proces trebuie executat în mod repetat. La început, pivotul va
fi al doilea element din lista, iar dupa fiecare executie se va muta cu o pozitie în jos, pâna la plasarea
ultimului articol din lista. Acest mod de avansare a pivotului este reprezentat în figura 2.7 prin umbrire: la
fiecare miscare, portiunea de lista de dupa ultimul pivot selectat este umbrita. Fiecare linie a figurii începe
cu ridicarea primului articol din portiunea umbrita; acest articol devine pivot, iar umbra de pe pozitia
respectiva a listei este eliminata. Prin urmare, vom controla repetarea rutinei precedente prin
instructiunile:
Umbreste portiunea din List de la al doilea pâna la ultimul articol
repeat
(Elimina umbra din primul articol al portiunii umbrite din List si
identifica acest articol drept pivot
? )
until (orice umbra din List a fost eliminata)
unde punctele indica locul în care trebuie plasata rutina anterioara.

Desigur, rutina noastra presupune ca lista contine cel putin doua articole, ipoteza care însa nu are
caracter general. Pe de alta parte, daca lista contine mai putin de doua articole, ea este deja sortata. Prin
urmare, pentru a extinde rutina astfel încât sa trateze si aceste cazuri este suficient sa o începem cu
instructiunea
if (List contine doua sau mai multe articole)
then (...)

31
Figura 2.7 Sortarea listei de nume Fred, Alice, David, Bill, Carol

Reprezentarea completa în pseudocod a programului nostru apare în figura 2.8. Pe scurt,


programul sorteaza o lista extragând în mod repetat câte un articol si inserându-l în pozitia
corespunzatoare ordinii dorite. Datorita procesului repetat de inserare, algoritmul poarta numele de
sortare prin inserare.
Observati ca structura din figura 2.8 contine o bucla în cadrul altei bucle, ciclul exterior fiind
exprimat printr-o instructiune repeat, iar cel interior printr-o instructiune while. La fiecare executie a
corpului buclei exterioare, bucla interioara este initializata si executata în mod repetat, pâna când se
îndeplineste conditia de încheiere. Astfel, o singura executie a corpului buclei exterioare conduce la mai
multe executii ale corpului buclei interioare.

procedure Sort(List)
if (List contine doua sau mai multe articole) then
[Umbreste portiunea din List de la al doilea pâna la ultimul articol;
repeat
(Elimina umbra din primul articol al portiunii umbrite din List si
identifica acest articol drept pivot,
Muta pivotul într-o locatie temporara, lasând în List un spatiu gol;
while ( exista un nume deasupra spatiului si acel nume este mai mare
decât pivotul) do
(muta numele de deasupra spatiului în spatiul gol, lasând un spatiu
deasupra numelui respectiv)
Muta pivotul în spatiul gol din List)
until (orice umbra din List a fost eliminata)]
Figura 2.8 Sortarea prin inserare, exprimata în pseudocod

32
Initializarea controlului buclei exterioare consta în umbrirea portiunii de la al doilea pâna la
ultimul articol din lista, iar modificarea este operata de instructiunea
Elimina umbra din primul articol al portiunii umbrite din List si
identifica acest articol drept pivot;
Conditia de încheiere apare atunci când orice umbra din lista a fost eliminata, asa cum se
precizeaza de altfel prin clauza until.
Controlul buclei interioare este initializat prin mutarea pivotului în afara listei, ceea ce conduce la
aparitia unui spatiu gol. Modificarea se realizeaza prin mutarea articolelor de deasupra spatiului cu câte o
pozitie în jos, astfel spatiul deplasându-se în sus. Conditia de încheiere este ca spatiul sa se afle imediat
sub un nume care sa nu fie mai mare decât pivotul sau ca spatiul sa ajunga în vârful listei.

4. STRUCTURI RECURSIVE

Structurile recursive reprezinta o alternativa de realizare a proceselor repetitive fara a se utiliza


cicluri. Pentru a introduce aceasta tehnica vom discuta algoritmul de cautare binara (binary search), o
aplicatie a metodologiei divide et impera în domeniul proceselor de cautare.

Algoritmul de cautare binara


Vom relua problema cautarii unui anumit articol într-o lista sortata, abordând de aceasta data
metoda pe care o folosim când cautam un cuvânt în dictionar. Este evident ca atunci când folosim
dictionarul nu efectuam o cautare secventiala, cuvânt cu cuvânt sau pagina cu pagina, ci începem prin a
deschide dictionarul la o pagina din zona în care credem ca se afla articolul cautat. Cu mult noroc, s-ar
putea sa gasim cuvântul chiar la pagina respectiva; daca nu, trebuie sa continuam cautarea. Numai ca în
acest punct am restrâns deja zona de cautare: ne putem rezuma fie la zona dinaintea pozitiei curente, fie la
cea de dupa pozitia curenta.
Figura 2.9 este o reprezentare în pseudocod a acestui mod de cautare, aplicat asupra unei liste
generice. Daca abordam problema la modul general nu avem avantajul de a sti cu aproximatie în ce zona
se afla articolul cautat, asa ca instructiunile din figura ne spun sa începem prin a deschide lista la articolul
din „mijloc”.
Motivul pentru care am pus cuvântul mijloc între ghilimele este acela ca daca lista are un numar
par de elemente, atunci nu exista un articol de mijloc. În acest caz, prin articol de mijloc vom întelege
primul articol din cea de-a doua jumatate a listei.
Daca articolul astfel selectat nu este cel cautat, rutina din figura 2.9 ofera doua alternative, ambele
implicând o cautare efectuata cu ajutorul unei proceduri numite Search. Prin urmare, pentru a completa
programul va trebui sa construim o astfel de procedura, care sa descrie modul în care se desfasoara
aceasta a doua etapa a cautarii.
Observati ca procedura trebuie sa fie suficient de robusta ca sa poata rezolva si cazul cautarii într-
o lista goala. De exemplu, daca rutinei din figura 2.9 i se furnizeaza o lista care contine un singur articol,
altul decât articolul tinta, atunci procedura va trebui sa caute fie în sublista de deasupra acestui unic
articol, fie în cea de dedesubt, iar aceste subliste sunt amândoua goale.
Desigur ca am putea sa utilizam algoritmul de cautare secventiala pe care l-am dezvoltat anterior
în cadrul acestui capitol. Dar, la cautarea unui cuvânt în dictionar nu procedam asa, ci aplicam asupra
zonei restrânse de cautare aceeasi tehnica pe care am folosit-o asupra întregului dictionar. Cu alte cuvinte,
vom selecta un articol la mijlocul zonei respective si vom îngusta din nou zona de cautar e.
Selecteaza „mijlocul” listei List ca articol de test;
Executa unul dintre urmatoarele blocuri de instructiuni,
în functie de situarea valorii tinta TargetValue fata
de articolul de test
TargetValue = articolul de test:
(Declari cautarea încheiata cu succes.)
TargetValue < articolul de test:
[Aplica procedura Search pentru a vedea
daca TargetValue este în portiunea din List
de deasupra articolului de test si
if (acea cautare este încununata de succes)
then (Declara aceasta cautare încheiata cu succes.)
else (Declara aceasta cautare încheiata cu esec.)]
TargetValue > articolul de test:
[Aplica procedura Search pentru a vedea daca
33
TargetValue este în portiunea din List
de sub articolul de test si
if (acea cautare este încununata de succes)
then (Declara aceasta cautare încheiata cu succes.)
else (Declara aceasta cautare încheiata cu esec.)]
Figura 2.9 Nucleul cautarii binare

Pentru a implementa aceasta procedura în pseudocod, vom începe prin a modifica rutina din
figura 2.9, astfel încât sa poata rezolva si situatiile când lista este goala, apoi vom da procedurii numele
Search si vom obtine programul în pseudocod prezentat în figura 2.10. Urmând aceasta rutina, atunci
când vom ajunge la instructiunea „Aplica modulul Search...” vom aplica asupra unei liste mai
mici aceeasi tehnica pe care am aplicat-o si asupra listei initiale, în cazul în care cautarea este încununata
de succes, vom reveni si vom declara cautarea initiala ca fiind încheiata cu succes; daca aceasta a doua
cautare se soldeaza cu un esec, vom declara si cautarea initiala ca fiind încheiata cu esec.
procedure Search (List, TargetValue)
if (List este goala)
then
(Declara cautarea încheiata cu esec.)
else
[Selecteaza „mjjlocul” listei List ca articol de test;
Executa una dintre urmatoarele blocuri de instructiuni,
în functie de situarea valorii tinta TargetValue
fata de articolul de test.
TargetValue = articolul de test:
(Declara cautarea încheiata cu succes.)
TargetValue < articolul de test:
[Aplica procedura Search pentru a vedea daca
TargetValue este în portiunea din List de deasupra articolului
de test si
if (acea cautare este încununata de succes)
then (Declara aceasta cautare încheiata cu succes.)
else (Declara aceasta cautare încheiata cu esec.)]
TargetValue > articolul de test:
[Aplica procedura Search pentru a vedea daca
TargetValue este în portiunea din List de sub articolul de test si
if (acea cautare este încununata de succes)
then (Declara aceasta cautare încheiata cu succes.)
else (Declara aceasta cautare încheiata cu esec.)]]
Figura 2.10 Algoritmul cautarii binare, reprezentat în pseudocod

Pentru a exemplifica, sa aplicam algoritmul din figura 2.10 la cautarea în lista Alice, Bill, Carol,
David, Evelyn, Fred, George a numelui tinta Bill. începem prin a selecta ca articol de test articolul din
mijloc - David. Deoarece numele tinta trebuie sa se afle înainte de acest articol de test, ni se spune sa
aplicam procedura Search asupra sublistei cu articolele care se afla înainte de David - este vorba de lista
Alice, Bill, Carol. Procedând astfel cream o copie a procedurii de cautare si pornim o a doua cautare.
Astfel, temporar avem în curs de executie doua copii ale procedurii de cautare. Executia primei
copii este întrerupta temporar la instructiunea
Aplica procedura Search pentru a vedea daca TargetValue este în portiunea din List
de deasupra articolului de test
iar în acest timp cea de a doua copie a procedurii executa cautarea în lista Alice, Bill, Carol. De îndata ce
vom încheia cea de a doua cautare, vom renunta la a doua copie a procedurii, întorcând rezultatul acesteia
catre prima procedura, a carei executie se va relua. Astfel, a doua copie a procedurii se executa ca o
subordonata a primei copii, efectuând cautarea solicitata de primul modul si apoi disparând.
A doua cautare selecteaza ca articol de test articolul Bill, deoarece acesta se afla în mijlocul listei
Alice, Bill, Carol. El fiind chiar valoarea tinta, cautarea este declarata a fi încheiata cu succes si ia sfârsit.
În acest punct am încheiat cea de-a doua cautare, care a fost solicitata de exemplarul initial al
procedurii, deci putem continua executia procedurii initiale. Deoarece a doua cautare s-a soldat cu un
succes, vom declara si cautarea initiala ca fiind încheiat cu succes. Procesul a determinat astfel în mod cât
se poate de corect ca articolul Bill face parte din lista Alice, Bill, Carol, David, Evelyn, Fred, George.

34
Sa vedem acum cum decurg lucrurile daca cerem rutinei noastre sa caute în lista Alice, Carol,
Evelyn, Fred, George articolul David. De aceasta data, exemplarul initial al procedurii selecteaza ca
articol de test articolul Evelyn si trage concluzia ca valoarea tinta ar trebui sa se afle în portiunea
anterioara a listei. Prin urmare, va cere unei alte copii a procedurii sa caute în lista articolelor care preced
numele Evelyn - anume în lista compusa din doua articole, Alice si Carol.
A doua copie a procedurii selecteaza ca articol de test articolul Carol si deduce ca valoarea tinta
ar trebui sa se gaseasca în a doua portiune a listei pe care a primit-o ca argument. De aceea, ea solicita
unei a treia copii a procedurii sa caute în lista de articole care urmeaza dupa articolul Carol în lista Alice,
Carol. Aceasta sublista este goala, deci a treia copie a procedurii are sarcina de a cauta într-o lista goala
valoarea David. Exemplarul initial al procedurii are sarcina de a cauta în lista Alice, Carol, Evelyn, Fred,
George, articolul de test fiind Evelyn; a doua copie cauta în lista Alice, Carol, articolul de test fiind Carol;
iar a treia copie este pe punctul de a cauta într-o lista goala.
Dupa cum e si normal, a treia copie a procedurii declara imediat cautarea încheiata cu esec si se
termina, ceea ce permite continuarea executiei copiei cu numarul doi. Aceasta constata ca operatia de
cautare pe care a solicitat-o s-a soldat cu un esec, deci declara propriul ei rezultat ca fiind un esec si se
termina. Rezultatul este transmis procedurii initiale, care îl astepta. Aceasta constata ca operatia de
cautare care a solicitat-o a esuat, deci declara propria ei cautare ca fiind încheiata cu esec si ia sfârsit. Din
nou, rutina noastra a produs rezultatul corect: articolul David nu apare în lista Alice, Carol, Evelyn, Fred,
George.
Daca revedem exemplele anterioare, putem trage urmatoarea concluzie: algoritmul din figura 2.10
împarte în mod repetat lista în doua segmente mai restrângând cautarea la unul din fragmente. Aceasta
metoda de împartire a listei în doua este de altfel s i motivul pentru care algoritmul este cunoscut sub
numele de cautare binara.

Controlul recursiv
Algoritmul de cautare binara este într-un fel similar celui de cautare secventiala: ambii implica
executia unui proces repetitiv. Însa modul în care este implementata aceasta repetare difera semnificativ:
daca în cazul cautarii secventiale ea se efectue aza ciclic, în cazul cautarii binare fiecare pas al repetarii
reprezinta o activitate sub donata pasului anterior. Tehnica poarta numele de recursivitate.
Asa cum am vazut, executia unui algoritm recursiv creeaza iluzia existentei mai multor copii ale
algoritmului, numite activari, care apar si dispar pe masura ce executia avanseaza. Dintre aceste copii, la
un moment dat este activa una singura; celelalte sunt într-o stare latenta, asteptând ca o alta activare sa-si
încheie executia pentru a putea la rândul lor continua.
Fiind procese repetitive, sistemele recursive sunt la fel de dependente de un control corect ca s i
structurile ciclice. De exemplu, la fel ca structurile ciclice, sistemele recursive trebuie sa aiba o conditie
de încheiere si sa fie proiectate încât aceasta conditie sa fie mai devreme sau mai târziu îndeplinita. De
fapt, un control corect al recursivitatii necesita aceleasi trei ingrediente ca si controlul buclelor -
initializare, modificare si test de încheiere.
În general, rutinele recursive sunt proiectate astfel încât înainte de orice activitate sa testeze
conditia de încheiere (n umita si cazul de baza sau degenerativ). Daca aceasta conditie nu este îndeplinita,
rutina atribuie unei activari sarcina de a rezolva o problema revizuita, care este mai aproape de conditia
de încheiere problema pe care trebuie sa o rezolve activarea curenta. În schimb, daca s-a atins conditia de
încheiere, procesul continua pe o ramura care evita recursivitatea, activitatea curenta terminându-se fara a
crea alte copii. În consecinta, una dintre activarile latente îsi va putea continua executia, îndeplinindu-s i
sarcina si permitând la rândul ei unei alte activari sa continue etc. În cele din urma toate activarile
generate îsi încheie pe rând execut ia, rezolvând astfel problema initiala.
Sa vedem cum sunt implementate fazele de initializare si modificare ale controlului repetitiv în
rutina de cautare binara prezentata în figura 2.10. În acest caz, crearea activarilor aditionale înceteaza fie
atunci când este gasita valoarea tinta, fie când sarcina se reduce la cautarea într-o lista goala. Procesul este
initializat în mod implicit, prin furnizarea unei liste initiale si a unei valori tinta. Din aceasta configuratie
initiala, rutina modifica sarcina care trebuie îndeplinita, solicitând cautarea într-o lista mai mica.
Deoarece lista initiala este de lungime finita si la fiecare pas lungimea listei de cautare se reduce, este
sigur ca în cele din urma fie vom gasi valoarea tinta, fie sarcina se va reduce la o cautare într-o lista goala.
Prin urmare, putem spune ca avem garantia ca la un moment dat procesul repetitiv va înceta.

35
Dupa ce ati vazut cele doua structuri de control - iterativa si recursiva - va puneti probabil
întrebarea daca ele sunt perfect echivalente. Cu alte cuvinte, daca avem un algoritm care utilizeaza o
structura ciclica, putem proiecta întotdeauna un algoritm care sa rezolve aceeasi problema utilizând doar
tehnici recursive? Si invers. În informatica, asemenea întrebari sunt foarte importante, deoarece
raspunzând la ele putem sa determinam ce caracteristici ar trebui sa includem într-un limbaj de
programare pentru a construi un sistem cât mai puternic.

36

S-ar putea să vă placă și