Sunteți pe pagina 1din 52

Lucrarea nr.

1
Determinarea experimentala a timpului de execuţie al unui program

1. Scopul lucrării - lucrarea prezintă aspecte legate de diferite modalitaţi de determinare experimentală a timpilor de
execuţie pentru diferite secvenţe de program.

2. Aspecte teoretice
2.1. Determinarea experimentală a timpului de execuţie al unui program
Determinarea timpului de execuţie al unui program prin metode analitice este foarte dificilă în practică. Aşa ca mai
exista si o alta metoda experimentală, mult mai uşoară, şi cu rezultate la fel de concludente în condiţiile în care se
porneşte de la anumite premize iniţiale. Ideea acestei metode constă din determinarea timpului efectiv (în secunde,
milisecunde, etc.) necesar rulării unui algoritm, folosindu-ne de ceasul calculatorului. Se prezintă mai jos schema
bloc a acestei metode.

Ideea acestui algoritm este aceea de a determina, folosindu-se ceasul calculatorului, două momente de timp  :
dinaintea inceputului algoritmului şi respectiv imediat de dupa terminarea acestuia. Rezultatul, ca fiind diferenţa între
cele două momente, este considerat ca fiind timpul de execuţie efectiv al algoritmului respectiv.
Este cunoscut faptul că, în general, timpul de execuţie al unui program depinde de următorii factori  :
-volumul datelor de intrare
-calitatea codului generat de compilator
-natura si viteza de execuţie a instrucţiunilor programului (dependentă şi de performanţele calculatorului pe care
acesta rulează)
-complexitatea algoritmului care stă la baza programului.
In condiţiile în care algoritmii sunt rulaţi pe calculatoare cu performanţe similare, utilizîndu-se acelaşi compilator,
practic timpul de execuţie devine funcţie doar de volumul datelor de intrare şi complexitatea algoritmului. In aceste
condiţii simplificate, evaluarea experimentală a performanţelor unui algoritm este cu adevarat relevantă.
In C++, funcţia prin care se poate determina ora curentă a calculatorului este gettime, şi se găseşte în bibioteca
<dos.h> .

2.2. Exemplu de determinarea experimentala a timpului de executie al unui program


Se prezintă în continuare modul de evaluare a timpului de execuţie a procedurii de sortare a elementelor unui tablou
de dimensiune N utilizînd algoritmul bubblesort, prezentat mai jos:

typedef int tablou[n];


void bubble_sort(tablou a)
{
int i,j,temp
(1) for (i=0;i<n;i++)
(2) for (j=n;j>=i+1;j--)
(3) if (a[j-1]>a[j])
{
(4) temp=a[j-1];
(5) a[j-1]=a[j];
(6) a[j]=temp
}
}

5
Dacă dorim să folosim metoda experimentală, şi să utilizăm funcţia gettime, programul se va modifica astfel:

typedef int tablou[n];


void bubble_sort(tablou a)
{
int i,j,temp
for (i=0;i<n;i++)
for (j=n;j>=i+1;j--)
if (a[j-1]>a[j])
{
temp=a[j-1];
a[j-1]=a[j];
a[j]=temp
}
}

void main()
{
...
(1) struct time t1,t2;
int durata;
(2) gettime(&t1);
(3) bubble_sort(a);
(4) gettime(&t2);
(5) durata=(t2.ti_sec*100+t2.ti_hund)
-(t1.ti_sec*100+t1.ti_hund);
printf(“Durata de executie=%dms”,durata);
...
}

Trebuie ţinut cont însă de faptul că funcţia gettime are nevoie ca şi parametri de variabile de tip structură de timp,
deci în cazul nostru vom avea nevoie de două variabile t1 şi t2 în care să se stocheze ora calculatorului (1). Se
stochează ora calculatorului în t1, exact înainte de a rula algoritmul (2). Se rulează algoritmul (3) dupa care, imediat
se stochează în t2 ora calculatorului (4). Pentru obţinerea diferenţei se va realiza un mic calcul matematic pentru
diferenţa între cei doi timpi t2 şi t1 (5), iar rezultatul final obţinut va fi durata de execuţie în sutimi de secunda.
Utilizînd aceasta metodă experimentală de determinare a timpului de execuţie, rezultatul se obţine mult mai uşor,
fiindu-ne oferit chiar de calculator, în comparaţie cu metoda analitică, unde tot calculul trebuie realizat manual de
catre programator, şi care implică, în anumite cazuri, cunoştinţe aprofundate de matematică. Este adevărat că metoda
analitică este mai generală.
O discuţie asemănătoare cu cea referitoare la timpul de execuţie se poate face relativ la memoria utilizată de un
program. De obicei, dacă nu este vorba de o programare defectuoasă, micşorarea spaţiului de memorie utilizat se face
pe seama timpului de execuţie. Un exemplu tipic în acest sens îl constituie reprezentarea compactă a matricilor:
economia de memorie obţinută în acest fel este însă în detrimentul timpului de execuţie, deoarece accesul la
elementele matricii este mult mai complicat.
În privinţa aspectelor referitoare la portabilitate, reutilizabilitate sau mentenanţă, estimarea performan ţelor este mult
mai puţin precisă. Evident, utilizarea în programare doar a construcţiilor standard va permite transferul uşor al
programelor dintr-un mediu în altul, dar aceasta vine în contradicţie cu cerinţele referioare la timp şi memorie
ocupată. De asemenea, pentru a permite reutilizarea modulelor este necesar ca interfa ţa oferită de acestea să fie cît
mai clară şi să ofere cît mai multe servicii posibile, astfel încît să nu fie necesar nici un acces care să ocolească acestă
interfaţă: satisfacerea acestor cerinţe intră din nou în contradicţie cu cele referitoare la eficienţă. În concluzie,
programatorul, atunci cînd elaborează un algoritm pentru o anumită problemă trebuie să aibă în vedere toate aceste
aspecte, şi să realizeze, funcţie de cerinţele aplicaţiei în sine, un compromis între acestea.

2.3. Problemă rezolvată


Să se realizeze un program care calculează n!. Pentru calculul factorialului se va scrie o funcţie fact(int n). Să
se calculeze timpii de execuţie al acestei functii pentru diferite valori ale lui n.

#include <conio.h>
#include <stdio.h>
#include <dos.h>

int nr;

6
struct time timp1,timp2;
unsigned long int fact(int n)
{unsigned long int f=1;
for (long int i=1;i<=n;i++)
{f*=i;
//fortam o intarziere pentru punere în
//evidenţă a timpului de executie
delay(100);
}
return f;
}
void main()
{ clrscr();
do
{ //citire numar
printf("Dati numarul (1..30) = ");
scanf("%d",&nr);
}
while ((nr<1)||(nr>30));

//get time start


gettime(&timp1);
//calculam si afisam factorialul
printf("%d!=%lu",nr,fact(nr));
//get time final
gettime(&timp2);

//convertim timpii in ms
unsigned long int ms1=timp1.ti_hund*10 +
(timp1.ti_sec*1000) + (timp1.ti_min*60000) +
(timp1.ti_hour*3600000);
unsigned long int ms2=timp2.ti_hund*10 +
(timp2.ti_sec*1000) + (timp2.ti_min*60000) +
(timp2.ti_hour*3600000);
//calculam diferenta dupa care o afisam
unsigned long int dif=ms2-ms1;
printf("\nTimpul de executie=%lu ms",dif);
getch();
}

3. Probleme propuse

1. Să se realizeze un program care generează un set de n numere aleatoare, care vor fi stocate într-un tablou a[n].
Se va folosi o funcţie gener(int n). Să se calculeze timpii de execuţie pentru diferite valori ale lui n.

2. Să se scrie o funcţie wait, avand sintaxa wait(int s). Scopul acestei funcţii va fi de a simula o pauză de
rulare lungă de aproximativ s secunde, folosindu-se doar funcţia gettime.

7
Lucrarea nr. 2
Tehnici de căutare în tablouri

1. Scopul lucrării - în foarte multe programe, se ajunge la un moment dat în situaţia de a fi necesară căutarea unei
anumite informaţii, după anumite criterii, dintr-un set de informaţii. Lucrarea curentă prezintă unele metode de
implementare a unor astfel de algoritmi de cautare.

2. Aspecte teoretice
2.1. Tehnica fanionului.
Vom considera că dorim să căutam dacă există un anumit număr dat, stocat într-o variabilă X ,într-un set de n numere
stocate într-un tablou dat A. Tehnica fanionului urmăreşte îmbunătăţirea performanţei algoritmului de căutare
standard, prin simplificarea condiţiei de căutare. Principiul acestuia este foarte simplu : se mai adauga la sfarsitul
tabloului înca un element (fanion) şi care se asignează cu valoarea cautata X. Astfel, la conditia de căutare nu vom
mai avea nevoie de condiţia de oprire i<n (care avea grijă ca toată căutarea să se limiteze la numărul de elemente din
tablou), fiindcă indiferent dacă acest element există sau nu în tabloul nostru, elementul în cauză tot va fi găsit şi
căutarea se va opri, fie în interiorul tabloului (dacă exista) fie pe fanion (dacă elementul nu există în tabloul iniţial).
Programul va arăta astfel:

int a[100],n,x,i;
void main()
{
clrscr() ;
(1) printf(”Dati valoarea lui n=”);
(2) scanf(“%d”,&n) ;
for (i=0 ;i<n ;i++)
{
(3) printf(“Dati a[%d]=”,i);
(4) scanf(“%d”,&a[i]) ;
}
(5) printf(“Dati valoarea de cautat (x)=”);
(6) scanf(“%d”,&x);
(7) i=0;
(8) a[n]=x;
(9) while (a[i]!=x) i++;
(10) if (i==n)
(11) printf(“Nu a fost gasit in tablou.”);
else
(12) printf(“A fost gasit pe pozitia %d.”,i);
}

Se poate observa că singurele diferenţe fata de algoritmul clasic de căutare se găsesc în liniile 8 si 9. Linia 8
pozitionează fanionul , iar linia 9 face căutarea propriu zisaă, căutare care se va derula pîna cînd se va găsi o valoare
în tablou egală cu. Deci, toată condiţia de continuare al algoritmului de căutare se rezuma la o singura expresie logica
a[i]!=x.
Ideal : cînd elementul căutat e primul element (prima poziţie) din tablou, acesta fiind găsit încă de la primul pas
Dezavantajos : cînd elementul nu exista în tablou , deci căutarea se va opri pe elementul fanion (ultimul).

2.2. Căutarea binară


Căutarea într-un tablou poate fi accelerată în mod considerabil dacă elementele unui tablou sînt în prealabil ordonate
(sortate). Necesitatea ca tabloul să fie ordonat implică faptul că elementele sale au o componentă (cheie) ce aparţine
unui tip scalar, iar căutarea se face după această componentă. În acest caz principiul cel mai des utilizat este cel al
înjumătăţirii repetate a intervalului în care elementul dorit trebuie căutat, rezultînd aşa numita tehnică a căutării
binare.
Ca principiul acestui algoritm să fie mai bine înţeles, sa ne reamintim de un mic joc din copilărie. Regula acestuia
era simplă : trebuia să se găsească un număr secret dintr-un interval de la 1 la 100, folosind cît mai putine încercări.
Principiul era simplu : este numărul acela mai mic decat 50 (jumatate)? Daca da, înseamnă că numarul se află între 1
si 50. Apoi din nou : este acel număr mai mic decît 25 (jumatate) ? Dacă nu, înseamnă ca se afla între 25 si 50. Şi aşa
mai departe, prin injumătăţiri repetate, intervalul este redus pînă se gasea (sau nu!) numărul în cauză. Acesta este şi
principiul căutarii binare.
Pseudocodul algoritmului de căutare binară este următorul:

1. Citire tablou (trebuie obligatoriu sa fie sortat)

8
2. Citire element de cautat x
3. stanga=0;dreapta=n-1;
4. repeta
5. . calculeaza mijlocul : mij=(stanga+dreapta)/2;
6. . daca (a[mij]>x) x se afla in partea stanga
7. => dreapta=mij-1
8. . altfel x se afla in partea dreapta => stanga=mij+1
9. atata timp cat [(a[mij]!=x) si (stanga<=dreapta)]
10. daca (a[mij]=x) elemental a fost gasit pe pozitia mij
11. altfel elemental nu a fost gasit

La fiecare repetiţie, intervalul inspectat, situat între indicii stanga şi dreata este înjumătăţit. Numărul necesar de
comparaţii este cel mult egal cu log 2(N), N reprezentînd numărul de elemente ale tabloului în care se face căutarea.
Ideal : elementul căutat este exact la mijlocul tabloului nostru, rezultînd că va fi găsit încă de la primul pas
Dezavantajos : elementul nu există în tablou

2.3. Cautarea binară performantă


Bazată pe căutara binară prezentată anterior, această tehnică foloseşte un alt mod de calcul a extremităţii dreapte a
intervalului curent, astfel încît se simplifică condiţia de ciclare.

1. Citire tablou (trebuie obligatoriu sa fie sortat)


2. Citire element de cautat x
3. stanga=0;dreapta=n;
4. repeta
5. . calculeaza mijlocul : mij=(stanga+dreapta)/2;
6. . daca (a[mij]>x) x se afla in partea stanga => dreapta=m ij
7. . altfel x se afla in partea dreapta => stanga=m ij+1
8. atata timp cat (stanga<dreapta)
9. daca (a[mij]=x) elemental a fost gasit pe pozitia mij
10. altfel elemental nu a fost gasit

Se poate vedea că nici acest algoritm nu diferă cu mult de cel anterior doar că, prin modificarea modului de calcul a
limitei dreapta, condiţia de oprire se reduce la o singură expresie logică, mai simplă decît cazul anterior.
Ideal : elementul căutat este exact la mijlocul tabloului nostru
Dezavantajos : elementul nu există în tablou

2.4. Căutarea prin interpolare


Este similară cu căutarea binară, dar foloseşte o altă formulă pentru calculul mijlocului, şi anume:
mij=stanga+(x-a[stanga])*(dreapta-stanga)
div (a[dreapta]-a[stanga])
ceea ce conduce la o delimitare mai rapidă a zonei din tablou în care
s-ar putea găsi x. Ca principiu, metoda este inspirată după procedeul căutarii într-o carte de telefon.
Pseudocodul unui program care foloseste această metodă este următorul :

1. Citire tablou (trebuie obligatoriu sa fie sortat)


2. Citire element de cautat x
3. stanga=0;dreapta=n-1;
4. repeta
5. . calculeaza mijlocul : mij= …….
6. . daca (a[mij]>x) x se afla in partea stanga => dreapta=m ij
7. . altfel x se afla in partea dreapta => stanga=m ij+1
8. atata timp cat (a[mij]!=x) si (stanga<dreapta) si (a[stanga]!=a[dreapta]) si (x>a[stanga]) si (x<a[dreapta])
9. daca (a[mij]=x) elemental a fost gasit pe pozitia mij
10. altfel elemental nu a fost gasit

În continuare vom explica pe larg metoda prin care s-a ajuns la formula de calcul al mijlocului. Principiul este
următorul : unde ar trebui sa fie acel numar, în cazul în care distribuţia numerelor în tablou ar fi uniformă.
Presupunem urmatoarea situaţie :

3 6 10 12 20 31
| |
stanga dreapta

9
Se poate observă că numerele din tablou nu sunt distribuite uniform (adica pasul, diferenţa între două elemente
vecine nu este tot timpul acelaşi). Pentru a calcula cum ar trebui să arate elementele în tablou astfel incît să fie
realizată o distribuţie uniformă, trebuie prima dată să calculăm pasul:

Pasul = (a[dreapta]-a[stanga]) / (dreapta-stanga) (1)


În cazul nostru:
Pasul = (31-3) / (5-0) = 28/5 = 5,6.
Deci , tabloul nostru ar arăta astfel dacă ar exista distribuţie uniformă:

3 8,6 14,2 19,8 25,4 31


| |
stanga dreapta

Deci, dacă ar fi să căutam elementul cu valoarea 16, acesta ar fi undeva în zona lui 14,2 , adică pe pozitia a 3-a
relativ la stînga. Cum se calculeaza asta? Astfel :
-presupunem ca avem o distribuţie uniformă, cu primul element 1 şi pasul 5.

1 6 11 16 21 26
| |
stanga dreapta

-dacă ar fi să calculăm pe ce poziţie e valoarea x=16, am aplica simplu următoarea formulă :

pozitie=stanga + (x-a[stanga])/pasul (2)

Adica pozitie=0+(16-1)/5=15/5=3 , ceea ce este corect.


Combinînd formulele (1) si (2) , şi modificînd numele variabilei poziţie cu mij , va rezulta tocmai formula ce
trebuia demonstrată.
Această metodă este eficientă în cazul în care numărul de elemente din tablou, N, este foarte mare, şi valorile
elementelor tabloului au o distribuţie uniformă în intervalul a[1],...,a[N]. Numărul de căutari în acest caz este de
ordinul lg (lg N). Aplicarea căutarii prin interpolare necesită însă ca elementul de căutat x să se afle în interiorul
intervalului a[1],..,a[N], altfel apare riscul ca valoarea calculată a lui mij să depăşească N.
Ideal : Elementul căutat să fie în aceeaşi poziţie şi în varianta distribuită uniform a tabloului nostru, ca şi în tabloul
dat iniţial, rezultînd în acest caz gasirea acestuia încă de la primul pas
Dezavantajos : Tabloul nostru diferă foarte mult de varianta distribuită uniform (ex: 1 – 50000 – 50001 – 50002 -
50003 – 50004 )

2.5. Problemă rezolvată


Să se realizeze un program în C++ cu ajutorul căruia se va căuta existenţa unui anumit student într-o listă de n
studenti. Pentru căutare se va folosi tehnica căutarii binare. Fiecare student va fi caracterizat prin numele şi grupa din
care face parte.

#include <conio.h>
#include <stdio.h>
#include <string.h>

struct st
{
char nume[20];
char grupa[6];
}student[51];

int nr_studenti;
char nume_cautat[20];

void citeste_studenti()
{ do
{
printf("Dati numarul de studenti (5..50)=");
scanf("%d",&nr_studenti);
}

10
while ((nr_studenti<5)||(nr_studenti>50));
for (int i=1;i<=nr_studenti;i++)
{
printf("\nDati numele studentului %d=",i);
scanf("%s",student[i].nume);
printf("Dati grupa studentului %d=",i);
scanf("%s",student[i].grupa);
}
}

void sorteaza_studenti()
{st temp;
int gasit;
do
{gasit=0;
for (int i=1;i<=nr_studenti-1;i++)
if (strcmp(student[i].nume,student[i+1].nume)>0)
{
gasit=1;
temp=student[i];
student[i]=student[i+1];
student[i+1]=temp;
}
}
while (gasit==1);
}

void cautare_binara()
{ int stanga=1,dreapta=nr_studenti,mijloc;
do
{
mijloc=(stanga+dreapta)/2;
if (strcmp(student[mijloc].nume,nume_cautat)>0)
dreapta=mijloc-1;
else
stanga=mijloc+1;
}
while
((strcmp(student[mijloc].nume,nume_cautat)!=0)
&& (stanga<=dreapta));
if (strcmp(student[mijloc].nume,nume_cautat)==0)
printf("\n\nStudentul a fost gasit in
grupa %s.",student[mijloc].grupa);
else
printf("\nStudentul NU a fost gasit!");
}

void main()
{ clrscr();
citeste_studenti();
//pentru a aplica cautarea binara, studentii vor trebui sortati
sorteaza_studenti();
//afisam studentii gata sortati
clrscr();
printf("Studentii sortati alfabetic sunt:");
for (int f=1;f<=nr_studenti;f++) printf("\nNume:%20s\tGrupa:
%5s",student[f].nume,
student[f].grupa);

//citeste numele studentului care dorim sa-l cautam


printf("\n\nDati numele studentului de
cautat = ");
scanf("%s",nume_cautat);

11
//lansam cautarea binara
cautare_binara();
getch();
}

3. Probleme propuse

1. Se consideră o matrice de numere de dimensiune MxN. Să se citească de la consolă un număr, şi să se caute


existenţa acestui număr în matrice, folosind tehnica fanionului. Evaluaţi experimental timpul de execuţie al
algoritmului de căutare, pentru diferite valori ale lui M şi N, folosind metoda descrisă în lucrarea 1.

2. Se consideră un număr de n societăţi comerciale, caracterizate prin nume, adresa şi rulaj. Dîndu-se un anumit rulaj,
citit de la tastatură, să se scrie programul care caută societatea a cărui rulaj este cel mai apropiat de valoarea
introdusă, dată. Se va folosi în acest scop căutarea prin interpolare.

Lucrarea nr. 3
Tehnici de sortare directe ale tablourilor

1. Scopul lucrării – îl reprezintă prezentarea celor mai cunoscute metode de sortare directe a tablourilor.

2. Aspecte teoretice
Metodele directe de sortare, cele mai cunoscute, şi care realizează sortarea in situ sunt următoarele:
- sortarea prin inserţie
- sortarea prin selecţie
- sortarea prin interschimbare
Performanţa acestora este O(N2).

2.1.Tehnica sortării prin inserţie


Cele N elementele de sortat sînt în mod conceptual divizate într-o secvenţă destinaţie a 1 .... ai-1 şi respectiv într-o
secvenţă sursă ai ... an. La fiecare pas, începînd cu i=2 şi incremetînd pe i cu 1, cel de-al i-lea element al secvenţei
sursă ai este luat şi transferat în secvenţa destinaţie prin inserarea sa la locul potrivit. Selectarea locului unde trebuie
inserat elementul se face parcurgînd şirul a 1 ..... ai-1 de la dreapta la stînga, oprirea realizîndu-se pe primul element a j
care are valoarea mai mică sau egală cu a i sau, dacă un astfel de element nu există ,pe a 1.

void sortare_insertie()
{
int i,j,x;
for (i=2;i<=n;i++)
{
x=a[i];
a[0]=x;
j=i-1;
while (a[j]>x)
{
a[j+1]=a[j];
j=j-1;
}
a[j+1]=x;
}
}

Algoritmul de sortare prin inserţie poate fi îmbunătăţit pornind de la observaţia că secvenţa destinaţie este deja
ordonată. În acest caz căutarea locului de inserare se poate face mai rapid utilizînd căutarea binar ă, prin împărţiri
succesive a intervalului din secvenţa destinaţie. Această metodă, (o variantă a metodei clasice de inserţie), poartă
denumirea de inserţie binară.

void insertie_binara()
{
int i,j,s,d,m,x;
{

12
for (i=1;i<=n;i++)
{
x=a[i];
s=0;
d=i-1;
while (s<=d)
{
y=div((s+d),2);
m=y.quot;
if (a[m]>x) d=m-1; else s=m+1;
}
for (j=i-1;j>=s;j--) a[j+1]=a[j];
a[s]=x;
}
}
}

2.2 Tehnica sortării prin selecţie


Acest algoritm foloseşte procedeul de a căuta elementul cu valoarea minimă din cele N elemente de sortat, şi de a
schimba între ele poziţiile acestui element cu primul element. Se repetă apoi procedeul cu cele N-1 elemente rămase,
apoi cu cele N-2, terminînd cu ultimele 2 elemente.

void sortare_selectie()
{
int i,j,k,x;
for (i=1;i<=n-1;i++)
{
k=i;
x=a[i];
for (j=i+1;j<=n;j++)
if (a[j]<x)
{
x=a[j];
k=j;
}
a[k]=a[i];
a[i]=x;
}
}
2.3. Tehnica sortării prin interschimbare (bubble-sort)
Principiul acestei metode este următorul: se compară şi se interschimbă perechile de elemente alăturate, parcurgînd
tabloul de la stînga la dreapta, pînă cînd toate elementele devin sortate. La fiecare trecere prin tablou se deplasează
cel mai mic element spre capătul din stînga al tabloului. Dacă considerăm tabloul vertical şi vom asimila elementele
sale cu nişte bule de aer în interiorul unui lichid, fiecare bulă avînd o “greutate” proporţională cu valoarea cheii,
atunci fiecare trecere prin tablou se soldează cu ascensiunea unei bile la nivelul specific de greutate. Din acest motiv,
această metodă se mai numeşte şi sortare prin metoda bulelor (bubble-sort).

void sortare_bubblesort()
{
int i,j,x;
for (i=1;i<=n;i++)
{
for (j=n;j>=i;j--)
if (a[j-1]>a[j])
{
x=a[j-1];
a[j-1]=a[j];
a[j]=x;
}
}
}

Acestui algoritm i se pot aduce o serie de îmbunătăţiri şi anume:

13
- se observă din exemplul dat că ultimele trei treceri prin tablou sunt fără efect, elementele fiind deja ordonate. Deci,
se poate memora dacă a avut loc o schimbare în ordonarea elementelor şi dacă nu, algoritmul nu se mai continuă. Şi
în acest caz însă este necesară o ultimă trecere fără modificări.
- la o analiză atentă se poate observa o asimetrie particulară: astfel un singur element "uşor" plasat la capătul "greu"
este readus la locul său într-o singură trecere, pe cînd un element "greu" plasat la capătul "uşor" va fi readus pe locul
său doar cu cîte o poziţie la fiecare trecere. Această asimetrie sugerează ca îmbunătăţire alternarea sensurilor de
parcurgere ale trecerilor consecutive. Algoritmul care conţine aceste îmbunătăţiri se numeşte shakersort sau sortare
prin amestecare.

void shakersort()
{
int j,k,l,r,x;
l=2;r=n;k=n;
do
{
for (j=r;j>=l;j--)
if (a[j-1]>a[j])
{
x=a[j-1];a[j-1]=a[j];a[j]=x;
k=j;
}
l=k+1;
for (j=l;j<=r;j++)
if (a[j-1]>a[j])
{
x=a[j-1];a[j-1]=a[j];a[j]=x;
k=j;
}
r=k-1;
} while (l<=r);
}

O a treia îmbunătăţire care se vede în implementarea acestui algoritm este aceea că, în variabila k nu se memorează
dacă a avut loc o interschimbare ci indicele (indexul j) al ultimei interschimbări. Este evident că toate perechile de
elemente situate sub acest indice sunt deja ordonate, deci trecerile următoare pot fi terminate la acest indice, în loc să
fie terminate la indicele obişnuit (capătul tabloului).

2.4. Problemă rezolvată


Se consideră un set de n beţişoare, fiecare avînd o culoare şi o lungime. Să se sorteze crescător sau descrescător
beţisoarele după lungimea lor, folosindu-se sortarea prin insertie, si să se afiseze rezultatele sub forma «  culoare » 
« lungime »

14
#include <stdio.h>
#include <conio.h>

struct bat_struct
{ int lungime;
char culoare[15];
}bat[51];

int nr_bete;

void afis_bete()
{
for (int i=1;i<=nr_bete;i++)
printf("\n%s ->
%d",bat[i].culoare,bat[i].lungime);
}

void sort_insertie()
{
int i,j;
bat_struct x;
for (i=2;i<=nr_bete;i++)
{
x=bat[i];
j=i-1;
while (bat[j].lungime>x.lungime)
{
bat[j+1]=bat[j];
j--;
}
bat[j+1]=x;
}
}

void main()
{
clrscr();
//citim numarul de bete
do
{
printf("Dati numarul de bete (5..50) = ");
scanf("%d",&nr_bete);
}
while ((nr_bete<5)||(nr_bete>51));
for (int i=1;i<=nr_bete;i++)
{
printf("\nDati lungimea batului nr.
%d = ",i);
scanf("%d",&bat[i].lungime);
printf("Dati culoarea batului nr. %d = ",i);
scanf("%s",bat[i].culoare);
}

//afisam varianta initiala inainte de sortare


clrscr();
printf("Betisoarele nesortate sunt:");
afis_bete();

//sortam betisoarele
sort_insertie();

//afisa varianta de dupa sortare


printf("\n\nBetisoarele sortate sunt:");

15
afis_bete();
getch();
}

3. Probleme propuse

1. Să se realizeze un program interactiv care realizeaza sortarea betisoarelor din problema rezolvata de la punctul 2.4.
prin toate metodele de sortare directe prezentate în această lucrare. Programul va prezenta următorul meniu:
- T – iniţializează tabloul de betisoare cu valori generate aleator
- A - afiseaza tabloul
- R - reiniţializează tabloul: aducerea tabloului la forma iniţială dupa ce acesta a fost sortat, în vederea aplicării unei
alte sortări
- I - sortare prin inserţie
- B - sortare prin inserţie binară
- S - sortare prin selecţie
- M - sortare prin interschimbare
- H - sortare prin metoda Shakersort
- X - părăsirea programului.
Pentru acelaşi tablou iniţial se vor evalua şi compara timpii de execuţie a diferitelor metode de sortare.

2. Se consideră un set de 49 numere. Calculatorul va extrage aleator 6 numere din setul de mai sus, şi va afisa aceste
6 numere în ordine crescătoare. Se va folosi sortarea prin selecţie.

3. Se consideră un set de n planete, caracterizate prin nume şi cele 3 coordonate în spatiu  : X,Y,Z. Să se afişeze cea
mai apropiată pereche de planete, precum şi cele mai îndepărtate două planete.

Lucrarea nr. 4
Tehnici de sortare avansate ale tablourilor

1. Scopul lucrării – îl reprezintă prezentarea celor mai cunoscute metode de sortare avansate a tablourilor.

2. Aspecte teoretice
2.1 Tehnica sortării prin inserţie cu diminuarea incrementului.
Secvenţa de program care urmează implementează algoritmul pentru o secvenţă de incremenţi descrescători formată
din t elemente, h1,h2,......h t şi care îndeplinesc condiţia ht=1 şi hi+1>hi, pentru i>=1.
Fiecare sortare-h este programată ca o sortare prin inserţie (poate fi folosita si o alta sortare directa la nivelul acestor
elemente) utilizînd tehnica fanionului în vederea simplificării condiţiei de terminare a procesului de căutare.
Deoarece fiecare sortare necesită propriul său fanion, pentru o căutare cît mai simplă, tabloul iniţial (A) se extinde în
stînga nu numai cu o componentă a[0] ci cu h1 componente.
În limbajul C a algoritmului shellsort apare o problem ă legată de faptul că limbajul C nu permite existenţa unor indici
negativi de tablou. Din acest motiv, tabloul este “deplasat” înspre indici pozitivi, cu un deplasament egal cu h1
(incrementul maxim, care reprezintă în algoritmul anterior indicele negativ maxim). Astfel, pozi ţiile 0..h1 din tablou
vor fi utilizate pentru memorarea fanioanelor (corespunzătoare poziţiilor -h1..0 în varianta Pascal) iar cele de la
h1..h1+n pentru memorarea tabloului propriu-zis. Din acest motiv, citirea şi afişarea tabloului este cea prezentată în
programul principal, care utilizează funcţia shellsort astfel modificată.

# define n 8
# define h1 9
# define t 4

int a[n+h1+1];
int i;

void shellsort()
{
int i,j,k,s,m,x;
int h[t+1];
h[1]=h1;h[2]=5;h[3]=3;h[4]=1;
for (m=1;m<=t;m++)
{
k=h[m];

16
s=h1-k;
for (i=k+h1+1;i<=n+h1;i++)
{
x=a[i];
j=i-k;
if (s==h1) s=h1-k;
s=s+1;
a[s]=x;
while (x<a[j])
{
a[j+k]=a[j];j=j-k;
}
a[j+k]=x;
}
}
}

void main()
{
for(i=h1+1;i<=n+h1;i++)
{
printf("elementul %d = ",i);
scanf("%d",&a[i]);
}
shellsort();
for(i=h1+1;i<=n+h1;i++)
{
printf("%d",a[i]);
printf("\n");
}
}

2.2 Tehnica sortării arborilor prin metoda ansamblelor (heapsort)


Metoda sortării prin selecţie clasică se bazează pe selecţia repetată a celui mai mic dintre cele N elemente, apoi a
celei mai mic elemente dintre cele N-1 elemente, etc.
Astfel, un ansamblu (heap) este definit ca o secvenţă de valori :
hs,hs+1,.....h d
astfel încît hi<h2i şi hi<h2i+1pentru toti i= s...d/2.
În particular, elementul h1 este cel mai mic al unui ansamblu.
În principiu metoda de construcţie a unui ansamblu este următoarea: evident, într-un tablou h 1....h n, elementele
hn/2....h n formează un ansamblu deoarece nu există nici o pereche de indici i şi j care să satisfacă relaţia j=2i (sau
j=2i+1). În procesul de construire al unui ansamblu in situ, se ia fiecare element de la jumătate în jos şi se deplasează
în tablou pînă la locul său (se compară elementul i cu elementele 2 i respectiv 2i+1 , adică cu cel mai mic dintre ele, şi
dacă elementul i este mai mare decît vreunul dintre acestea, atunci se schimbă între ele cele două elemente). Operaţia
se repetă pînă cînd nu se mai face nici o schimbare şi apoi tot procesul se reia pentru elementul următor. Deplasarea
unui element în tablou poate fi descrisă de functia deplasare prezentată în continuare:

#define TRUE 1
#define FALSE 0

void deplasare(int s, int d)


{
int i,j,ret,x;
i=s;j=2*i;x=a[i];ret=FALSE;
while ((j<=d) && (ret==FALSE))
{
if (j<d)
if (a[j]>a[j+1]) j=j+1;
if (x>a[j])
{
a[i]=a[j];i=j;j=2*i;
}

17
else
ret=TRUE;
}
a[i]=x;
}

Utilizînd procedura de deplasare al unui element în tablou descrisă mai sus, procesul de generare in situ al unui
ansamblu poate fi descris astfel:

s=(n/2)+1;
while (s>1)
{
s=s-1;
deplasare(s,n);
}

După ce ansamblul a fost generat, în vederea sortării elementelor se execută N paşi de deplasare, după fiecare pas
selectîndu-se vîrful ansamblului. La fiecare pas se ia ultima componentă a ansamblului şi se memorează vîrful
ansamblului în locul ei. Componenta se lasă apoi să alunece în ansamblu, la locul ei (procedura deplasare).

d=n;
while (d>1)
{
x=a[1];a[1]=a[d];a[d]=x;
d=d-1;
y=div(d,2);
s=d/2+1;
while (s>1)
{
s=s-1;
deplasare(s,d);
}
}
}

Elementele se obţin sortate în ordine inversă, lucru care se poate uşor remedia modificînd sensul relaţiilor de compa -
raţie din procedura deplasare.

2.3. Tehnica sortării prin partiţionare (quicksort)


C.A.R.Hoare a conceput o metodă de sortare cu performanţe spectaculoase pe care a denumit-o Quicksort. Ideea de
bază este următoarea: fie x un element oarecare al tabloului de sortat a 1,....a n. Se parcurge tabloul de la stînga la
dreapta pînă se găseşte primul element a i>x. Apoi se parcurge tabloul de la dreapta la stînga pînă cînd se găseşte
primul element a j<x. Se schimbă între ele elementele a i şi aj, după care parcurgerea continuă. Procedeul se termină
cînd cele două parcurgeri se "întîlnesc" undeva în interiorul tabloului. Efectul final este că acum tabloul iniţial este
partiţionat într-o parte stîngă cu chei mai mici decît x şi o parte dreapta cu chei mai mari decît x.
După o primă partiţionare a secvenţei de elemente se aplică aceeaşi procedură celor două parti ţii rezultate, apoi celor
patru partiţii ale acestora, ş.a.m.d, pînă cînd fiecare partiţie se reduce la un singur element. Tehnica sortării pe bază
de partiţionare este ilustrată de procedura sortare, care se apelează recursiv pe ea însăşi.

void sortare(int s,int d)


{
int i,j;
element x,w;
i=s;j=d;
x=a[(i+j)/2];
do
{
while (a[i].cheie<x.cheie) i=i+1;
while (x.cheie<a[j].cheie) j=j-1;
if (i<=j)
{
w=a[i];a[i]=a[j];a[j]=w;

18
i=i+1;j=j-1;
}
} while (i<=j);
if (s<j) sortare(s,j);
if (i<d) sortare(i,d);
}

void quicksort()
{
int m=1;
int p=n;
sortare(1,n);
}

Este de reţinut însă că efortul de calcul este proporţional cu N1,2 în cazul tehnicii shellsort şi c n N log2N în cazul
tehnicii heapsort şi quicksort.

2.4 Problemă rezolvată


Se va citi de la consola o propozitie. Se vor extrage toate cuvintele distincte din aceasta propozitie, si vor fi afisate in
ordine alfabetica.

Pentru sortare va fi folosit algoritmul shellsort .

#include <stdio.h>
#include <conio.h>
#include <string.h>
#include <ctype.h>

#define t 4
#define h1 9
typedef char cuvant[20];
int h[t+1],nr_cuvinte=0,i;
char propozitia[200];
cuvant cuv[21],cuv_work[21+h1];

int exista_deja(char c[20])

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


if (strcmp(cuv[i],c)==0) return 1;
return 0;
}

void get_cuvinte()
{ int cuvant=0,f;
char temp[20];
strcpy(temp,"");
for (f=0;f<strlen(propozitia);f++)
propozitia[f]=toupper(propozitia[f]);
for (f=0;f<strlen(propozitia);f++)
{if (((propozitia[f]>='A')&&(propozitia[f]<='Z'))||
((propozitia[f]>='0')&&(propozitia[f]<='9')))
{if (cuvant==0)
{strcpy(temp,"");
cuvant=1;
}
char lit[2];
lit[0]=propozitia[f];
lit[1]='\0';
strcat(temp,lit);
}
else
if (cuvant)

19
{cuvant=0;
if (!exista_deja(temp))
{nr_cuvinte++;
strcpy(cuv[nr_cuvinte],temp);
}
}
}
}

void sort_shell()
{ int f,m,k,s,i,j;
char x[20];
for (f=1;f<=nr_cuvinte;f++)
strcpy(cuv_work[f+h1],cuv[f]);
for (m=1;m<=t;m++)
{ k=h[m];
s=h1-k;
for (i=k+h1+1;i<=nr_cuvinte+h1;i++)
{ strcpy(x,cuv_work[i]);
j=i-k;
if (s==h1) s=h1-k;
s++;
strcpy(cuv_work[s],x);
while (strcmp(x,cuv_work[j])<0)
{strcpy(cuv_work[j+k],cuv_work[j]);
j-=k;
}
strcpy(cuv_work[j+k],x);
}
}
for (f=1;f<=nr_cuvinte;f++)
strcpy(cuv[f],cuv_work[f+h1]);
}

void main()
{h[1]=9;h[2]=5;h[3]=3;h[4]=1;
clrscr();
//citim propozitia
printf("Dati propozitia=");
gets(propozitia);
strcat(propozitia," ");
//scoatem cuvintele distincte
get_cuvinte();
//afisam cuvintele nesortate
printf("\n\nCuvintele distincte
nesortate sunt:");
for (i=1;i<=nr_cuvinte;i++)
printf("\n%s",cuv[i]);

//sortam cuvintele respective


sort_shell();
//afisam cuvintele sortate
printf("\n\n\Cuvintele distincte
sortate sunt:");
for (i=1;i<=nr_cuvinte;i++)
printf("\n%s",cuv[i]);
getch();
}

3. Probleme propuse

1. Să se realizeze un program interactiv care implementează metodele de sortare avansate prezentate în această
lucrare. Programul va prezenta următorul meniu:

20
- C - iniţializează tabloul de sortat cu valori generate aleator
- V - vizualizează tablou
- R - reiniţializează tabloul: vezi lucrarea 3.
- S - sortare shellsort
- H - sortare heapsort
- Q- sortare quicksort
- X - parasirea programului.
Pentru acelaşi tablou iniţial se vor evalua şi compara timpii de execuţie a diferitelor metode de sortare.

2. Se consideră un vector de n numere. Să se sorteze în ordine crescătoare elementele de pe pozitiile pare, folosind
algoritmul shellsort, şi elementele de pe poziţiile impare în ordine descrescătoare folosind algorimul heapsort. Să se
afişeze vectorul în noua formă.

3. Se consideră un set de n abonati la un serviciu de telefonie. Fiecare abonat va fi caracterizat prin nume, adresa,
număr telefon şi valoare factură. Să se sorteze descrescator abonaţii după valoarea facturii de telefon folosind
algoritmul de sortare quicksort, şi apoi să se afişeze primii trei abonati cu cele mai mari facturi.

Lucrarea nr. 5
Sortarea fişierelor secvenţiale (externă)

1. Scopul lucrării – îl reprezintă prezentarea celor mai cunoscute metode de sortare a fişierelor.

2. Aspecte teoretice
Metodele aplicate la sortarea tablourilor nu pot fi aplicate pentru date care nu încap în memoria calculatorului
(internă), dar care pot fi de exemplu memorate pe dispozi tive periferice secvenţiale. În acest caz datele pot fi
descrise cu ajutorul unei structuri tip fişier, avînd drept caracteristică esenţială faptul că, în fiecare moment este
accesibilă doar o singură componentă. Această restricţie face ca metodele de sortare a fişierelor să fie total diferite de
cele ale tablourilor.

2.1. Tehnica sortării prin interclasare


Interclasarea presupune combinarea a două sau mai multe secvenţe ordonate într-o singură secvenţă, prin selecţii
repetate ale componentelor curent accesibile. O metodă bazată pe interclasare ar fi următoarea:
1. Se împarte secvenţa a în două jumătăţi b şi c.
2. Se interclasează b cu c combinînd cîte un element din fiecare în perechi ordonate, obţinîndu-se o nouă
secvenţă.
3. Se repetă cu secvenţa interclasată a paşii 1 şi 2, de data aceasta combinînd perechile ordonate în quadru -
plele ordonate
4. Se repetă paşii iniţiali intercalînd quadruplele în 8-uple, ş.a.m.d., de fiecare dată dublînd lungimea
subsecvenţelor de interclasare, pînă la sortarea întregii secvenţe.
Fiecare operaţie care tratează întregul set de date se numeşte fază iar subprocesele prin repetarea cărora se realizează
procesul de interclasare se numesc treceri.

#define FALSE 0
#define TRUE 0
#define n 10

int x;
FILE *a, *b, *c;
int p,k,i;
int temp;

void injumatatire(int *nr)


{
a=fopen(“…\a.dat”,”r”);
b=fopen(“…\b.dat”,”w”);
c=fopen(“…\c.dat”,”w”);
if (!feof(a)) fscanf(a,”%d”,&x);
while (!feof(a))
{
i=0;
while ((i<(*nr))&&(!feof(a)))
{

21
fprintf(b,”%d\n”,x);
fscanf(a,”%d”, &x);
i++;
}
i=0;
while ((i<(*nr))&&(!feof(a)))
{
fprintf(c,”%d\n”,x);
fscanf(a,”%d”, &x);
i++;
}
}
fclose(a); fclose(b); fclose(c);
}

void interclasare(int *nr)


{
int i,j,x,y,termb, termc;
b=fopen(“…\b.dat”,”r”);
c=fopen(“…\c.dat”,”r”);
a=fopen(“…\c.dat”,”w”);
termb=feof(b);
termc=feof(c);
if (termb==FALSE)
{fscanf(b,”%d”, &x); termb=feof(b);}
if (termc==FALSE)
{fscanf(c,”%d”, &y); termc=feof(c);}
do
{
i=0 ; j=0 ;
while ((i<*nr)&&(j<*nr)&&(termc==FALSE) && (termb==FALSE))
{
if (x<y)
{
fprintf(a,”%d\n”, x); i++;
if feof(b) termb=TRUE;
else
{
fscanf(b,”%d”,&x); termb=feof(b);
}
}
else
{
fprintf(a,”%d\n”, y); j++;
if feof(c) termc=TRUE;
else
{
fscanf(c,”%d”,&y); termc=feof(c);
}
}
}
while ((i<*nr)&&(termb==FALSE))
{
fprintf(a,”%d\n”,x); i++;
if (feof(b) termb=TRUE;
else
{
fscanf(b,”%d”,&x); termb=feof(b);
}
}
while ((j<*nr)&&(termc==FALSE))
{
fprintf(a,”%d\n”,y); j++;

22
if (feof(c) termc=TRUE;
else
{
fscanf(c,”%d”,&y); termc=feof(c);
}
}
if ((termb==FALSE)||(termc==FALSE)) k++;
} while ((termb==FALSE)||(termc==FALSE));
fclose(a); fclose(b); fclose(c);
}
void main()
{
a=fopen(“…\a.dat”,”w”);
for (i=1;i<=n;i++)
{
scanf(“%d”, &temp);
fprintf(a, ”%d\n”, temp);
}
fclose(a); //crearea fisierului
a=fopen(“…\a.dat”,”r”);
for (i=1;i<=n;i++)
{
fscanf(a,“%d”, &temp);
printf(”%d\n”, temp);
}
fclose(a); //tiparirea fisierului creat
//incepe inteclasarea
p=1;
do
{
injumatatire(&p);
k=0;
interclasare(&p);
p=2*p;
}while (k!=0);
a=fopen(“…\a.dat”,”r”);
for (i=1;i<=n;i++)
{
fscanf(a,“%d”, &temp);
printf(”%d\n”, temp);
}
fclose(a); } //tiparirea fisierului sortat

2.2. Tehnica sortării prin interclasare naturală.


Tehnica sortării prin interclasare prezentată anterior nu ia în considerare faptul că datele pot fi parţial sor tate,
lungimea subsecvenţelor fiind nedeterminată. De fapt, oricare două secvenţe sortate de lungime M şi respectiv N pot
fi interclasate într-o singură secvenţă de lungime M+N. Tehnica de interclasare, care de fiecare dată combină cele
mai lungi două secvenţe, se numeşte sortare prin interclasare naturală. În cadrul acestei tehnici, un rol central îl
joacă noţiunea de monotorie. Se înţelege prin monotorie orice secvenţă parţială a i...a j, care satisface următoarele
condiţii:
1. 1<i<j<n;
2. ak < ak+1 pentru orice i <k<j-1;
3. ai-1>ai sau i=1;
4. aj>aj+1 sau j=n;

Practic, diferenţa dintre sortarea echilibrată şi cea naturală este aceea că lungimea subssecvenţelor de interclasat nu
este constantă (1,2,4,8, ş.a.m.d.) ci variază, fiind egală cu lungimea unei monotonii. Deci, sortarea naturală
interclasează monotorii. Fiecare trecere constă în două faze alternative: defalcare şi interclasare. În faza de
defalcare monotoriile fişierului c se defalcă alternativ pe fişierele a şi b. În faza de interclasare se recombină în c
monotoriile de pe fişierele a şi b. Sortarea se termină în momentul în care numărul monoto riilor din fişierul c este 1.
Pentru numărarea monotoriilor se foloseşte variabila l.

23
Prin această metodă la distribuire se depune fie un număr egal de monotorii pe fişierele a respectiv b, fie cu o mono -
torie mai mult pe fişierul a. După interclasarea monotoniilor perechi, monotoria nepereche trebuie recopiată în c.

2.3 Problemă rezolvată


Se consideră un fişier « sursa.txt » . Acest fişier conţine ca şi text un proverb. Să se sorteze caracterele în ordine
alfabetică din acest fişier într-un alt fisier , numit « dest.txt »

#include <conio.h>
#include <stdio.h>

FILE *sursa,*dest,*a,*b;
char ch;
int file_size=0,a_size=0,b_size=0;

void transfera_sursa_in_destinatie()
{ sursa=fopen("sursa.txt","rt");
dest=fopen("dest.txt","wt");
while (!feof(sursa))
{ ch=fgetc(sursa);
if (!feof(sursa))
{fputc(ch,dest);
file_size++;
}
}
fclose(sursa);
fclose(dest);
}

void split_destinatie(int nr)


{ int ch_scrise=0;
dest=fopen("dest.txt","rt");
a=fopen("a.tmp","wt");
b=fopen("b.tmp","wt");
a_size=0;b_size=0;
while (ch_scrise<file_size)
{ int i;
for (i=1;((i<=nr)&&(ch_scrise<file_size));i++)
{ ch=fgetc(dest);
fputc(ch,a);
a_size++;
ch_scrise++;
}
for (i=1;((i<=nr)&&(ch_scrise<file_size));i++)
{ch=fgetc(dest);
fputc(ch,b);
b_size++;
ch_scrise++;
}
}
fclose(dest);
fclose(a);
fclose(b);
}

void interclaseaza(int nr)


{a=fopen("a.tmp","rt");
b=fopen("b.tmp","rt");
dest=fopen("dest.txt","wt");
int na=1,nb=1,ch_scrise=0,read_next_a=1;
read_next_b=1,a_citit=0,b_citit=0,gata=0;
char ca,cb;
while (ch_scrise<file_size)
{ do

24
{if (read_next_a)
{ca=fgetc(a);
read_next_a=0;
a_citit++;
if (a_citit>a_size) {gata=1;na=nr+1;}
}

if (read_next_b)
{cb=fgetc(b);
read_next_b=0;
b_citit++;
if (b_citit>b_size) {gata=1;nb=nr+1;}
}

if (gata==0)
if (ca<cb)
{
fputc(ca,dest);
ch_scrise++;read_next_a=1;na++;
}
else
{
fputc(cb,dest);
ch_scrise++;read_next_b=1;nb++;
}
}
while ((na<=nr)&&(nb<=nr));

if (nb<=nr)
{ fputc(cb,dest);ch_scrise++;
for
(int i=nb;((i<nr)&&(b_citit<b_size));i++)
{ cb=fgetc(b);b_citit++;
fputc(cb,dest);ch_scrise++;
}
}
else
{ fputc(ca,dest);ch_scrise++;
for
(int i=na;((i<nr)&&(a_citit<a_size));i++)
{ ca=fgetc(a);a_citit++;
fputc(ca,dest);ch_scrise++;
}
}
na=1;read_next_a=1;
nb=1;read_next_b=1;
}
fclose(a);
fclose(b);
fclose(dest);
}

void sorteaza_destinatie()
{
int n=1;
while (n<=file_size)
{ split_destinatie(n);
interclaseaza(n);
n*=2;
}
}

void main()

25
{ clrscr();
transfera_sursa_in_destinatie();
sorteaza_destinatie();
}

3. Probleme propuse

1. Se consideră un fişier « an.txt » în care sunt stocate informaţiile a N studenţi : nume, prenume, grupa. Să se
sorteze aceşti studenţi în ordine alfabetică, după nume, într-un al doilea fisier « an_sort.txt »

2. Să se modifice problema rezolvată din cadrul acestei lucrări, prin utilizarea sortării naturale în locul sortării
echilibrate.

Lucrarea nr. 6
Algoritmi recursivi

1. Scopul lucrării – îl reprezintă prezentarea conceptului de recursivitate precum şi a cîtorva categorii tipice de
algoritmi recursivi.

2.Aspecte teoretice
2.1. Definirea recursivităţii
Recursivitatea presupune de asemenea execuţia repetată a unei porţiuni de program. În contrast cu iteraţia însă, în
cadrul recursivităţii condiţia este verificată în decursul execuţiei programului (nu la sfîrşitul ei ca la iteraţie) şi, în caz
de rezultat satisfăcător, întreaga porţiune de program este apelată din nou ca subprogram a ei însăşi, în particular ca
un subprogram a porţiunii de program originale care însă nu şi-a terminat execuţia. În momentul satisfacerii condiţiei
de revenire, se reia execuţia programului apelant exact din punctul din care s-a apelat pe el însuşi. Acest lucru este
valabil pentru toate apelurile anterioare satisfacerii condiţiei.
Structurile de program necesare şi suficiente în exprimarea recursivităţii sunt subrutinele care pot fi apelate prin
nume. Dacă o subrutină P conţine o referinţă directă la ea însăşi se spune că este direct recursivă; dacă P conţine o
referinţă cu o altă subrutină Q, care la rîndul ei conţine o referinţă (directă sau indirectă) la P, se spune că P este
indirect recursivă.
De regulă, unei subrutine i se asociează un set de obiecte ale subrutinei (variabile, constante, tipuri, func ţii şi
proceduri), care sunt definite local în subrutină şi care nu există sau nu au înţeles în afara acesteia. De fiecare dată
cînd o astfel de subrutină este apelată recursiv, se creează un nou set de astfel de obiecte locale, specifice apelului.
Deşi aceste obiecte au acelaşi nume ca şi cele corespunzătoare lor din instanţa anterioară a subrutinei (în calitate de
program apelant), ele au valori distincte şi orice conflict de nume este evitat prin regulile care stabilesc domeniul
identificatorilor: identificatorii se referă întotdeauna la setul cel cel mai recent creat de obiecte. Aceleaşi reguli sunt
valabile şi în cazul parametrilor subrutinei, asociaţi prin definiţie cu setul de variabile.
Ca şi în cazul structurilor repetitive, procedurile recursive necesită evaluarea unei condiţii de terminare, fără de care
un program recursiv duce la o buclă de program infinită. Aplicaţiile practice au demonstrat că, deşi teoretic
recursivitatea poate fi infinită, practic ea nu numai că este finită, dar adîncimea sa este relativ mică. Motivul este că,
fiecare apel recursiv al unei subrutine necesită alocarea unui volum de memorie (in stiva) destinat obiectelor
(variabilelor) sale curente. În plus, alături de acestea mai trebuie memorată şi starea curentă a programului, cu scopul
de a fi refăcută atunci cînd noua activitate a subrutinei se termină şi urmează ca cea veche să fie reluată.
Algoritmii recursivi sînt potriviţi a fi utilizaţi atunci cînd problema care trebuie rezolvată sau datele care trebuiesc
prelucrate sunt definite în termeni recursivi (algoritmii implementează în acest caz definiţia recursivă). Utilizarea
recursivităţii trebuie însă evitată ori de cîte ori stă la dispoziţie o rezolvare bazată pe interaţie. De fapt,
implementarea recursivităţii pe elemente nerecursive dovedeşte faptul că, practic, orice algoritm recursiv poate fi
transformat într-unul pur interativ. La transformare putem distinge două cazuri:
a). Cazul în care apelul recursiv al procedurii apare la sfîrşitul ei, drept ultima instrucţiune a procedurii ( tail
recursion). În această situaţie, recursivitatea poate fi înlocuită cu o buclă simplă de iteraţie. Acest lucru este posibil
deoarece, în acest caz, revenirea dintr-un apel încuibat presupune şi terminarea instanţei respective a subrutinei,
motiv pentru care contextul apelului nu trebuie salvat. Astfel, dacă o procedură P(x) conţine ca şi ultim pas al său un
apel recursiv la ea însăşi P(y), atunci acest apel poate fi înlocuit cu instrucţiunea de atribuire x:=y, urmată de un salt
la începutul codului lui P. Dacă P are mai mulţi parametri, ei pot fi trataţi fiecare în parte ca x şi y. Aici y poate fi o
expresie dar x trebuie să fie o variabilă transmisibilă prin adresă, astfel încît valoarea sa să fie memorată într-o locaţie
specifică apelului.
b). Cazul în care apelurile recursive se realizează în interiorul procedurii, varianta iterativă a acestei situaţii
presupune tratarea explicită de către programator a stivei apelurilor recursive în care se salvează contextul fiecărei
instanţă de apel. Eliminarea recursivităţii poate fi făcută, teoretic, în orice situaţie. Eliminarea recursivităţii poate

26
duce la creşterea performanţelor însă, în cele mai multe cazuri, algoritmul devine mult mai complicat şi mai greu de
înţeles.
În general, atunci cînd nu avem “tail recursion”, pentru eliminarea recursivităţii se foloseşte o structură de date de
tip stivă, definită de utilizator. Un nod al acestei stive va conţine următoarele elemente:
- valorile curente ale parametrilor procedurii
- valorile curente ale tuturor variabilelor locale ale procedurii
- o indicaţie referitoare la adresa de retur, adică referitoare la locul în care revine controlul execuţiei în momentul în
care apelul curent al instanţei procedurii se termină.

2.2. Tehnica divizării (divide and conquer)


Una dintre metodele fundamentale de proiectare a algoritmilor se bazează pe tehnica divizării (divide and conquer).
Principiul de bază este acela de a descompune o problemă în mai multe subprobleme a căror rezolvare este mai
simplă şi din soluţiile cărora se poate determina soluţia problemei iniţiale. Acest mod de lucru se repetă recursiv pînă
cînd subproblemele devin banale iar soluţiile lor, evidente. O aplicaţie tipică a tehnicii divizării are următoarea
structură:

procedure rezoiva (x)


begin
if x este divizibil in subprobleme then
begin
divide pe x in doua sau mai multe parti: x1.....xk;
combina cele k solutii partiale intr-o solutie pentru x
end
else
rezolva pe x direct
end;

Dacă recombinarea soluţiilor parţiale este substanţial mai simplă decît rezolvarea întregii prob leme, această tehnică
duce la proiectarea unor algoritmi extrem de eficienţi.

2.3. Algoritmi recursivi pentru determinarea tuturor soluţiilor unor probleme.

A. Algoritm pentru evidenţierea tuturor posibilităţilor de împărţire a unei cantităţi de valoare întreagă dată (N) în
părţi de valoare l1 sau l2 (cu posibilitate de generalizare pentru l 1,.l 2,….l k).
Se bazează pe următoarea tehnică de lucru:
- pentru N >min(l1, l1) există două posibilităţi
1). Când la început se ia o parte de valoare l1 şi restul cantităţii de N- l1 se imparte în toate modurile posibile
(apel recursiv la funcţia de impărţire, cu parametru N- l1);
2). Când la început se ia o parte de valoare l2 şi restul cantităţii N- l2 se imparte în toate modurile posibile
posibile (apel recursiv la funcţia de impărţire, cu parametru N- l1);
- pentru N= min(l1, l1) avem cazul banal de o tăietură de lungime egala cu minimul dintre l 1 şi l2;
- pentru N< min(l1, l1) nu există nici o posibilitate.

B. Algoritm pentru determinarea tuturor soluţiilor de ieşire


dintr-un labirint
Algoritmul următor presupune un labirint descris cu ajutorul unui tablou tridimensional de caractere de dimensiuni
[N+1] x [N+1], în care cu ajutorul caracterul ‘*’ sunt reprezenta ţi pereţii iar cu ajutorul caracterului ' ' (blanc)
culoarele; punctul de start este centrul labirintului (dar nu este obligatoriu). Căutarea se execută astfel: dacă valoarea
caracterului din poziţia curentă este ' ', se intră pe o linie posibilă de ieşire şi se machează poziţia cu caracterul '+'.
Dacă s-a ajuns la ieşire, se execută tipărirea tabloului (labirintul şi drumul găsit). În caz contrar se apelează recursiv
procedura P pentru cele patru poziţii din vecinătatea imediată a poziţiei curente.
Este important să subliniem că marcajul de drum se şterge, de îndată ce s-a ajuns la o fundătură. Ştergerea se execută
prin generarea unui caracter ' ' (blanc) pe poziţia curentă înainte de părăsirea procedurii.

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#define n 5

char m[n+1][n+1];
int i,j;
div_t a,b;

27
void tipar()
{
for (i=0;i<=n;i++)
{
printf(" ");
for (j=0;j<=n;j++) printf("%c",m[i][j]);
printf("\n");
}
printf("\n");
}
void p(int x,int y)
{
if (m[x][y]==' ')
{
m[x][y]='+';
a=div(x,n);
b=div(y,n);
if ((a.rem==0) || (b.rem==0))
tipar();
else
{
p(x+1,y); p(x,y+1); p(x-1,y); p(x,y-1);
}
m[x][y]=' ';
}
}
void main()
{
for (i=0;i<=n;i++)
for (j=0;j<=n;j++)
m[i][j]='*';
for (j=2;j<=5;j++) m[1][j]=' ';
m[2][1]=' ';
m[2][2]=' ';
m[2][4]=' ';
m[3][2]=' ';
m[3][3]=' ';
m[3][4]=' ';
m[4][2]=' ';
m[5][2]=' ';
tipar();
a=div(n,2);
p(a.quot,a.quot);
}

În exemplul de mai sus, structura iniţială a labirintului a fost furnizată prin setarea elementelor tabloului m din
programul principal. Acest lucru însă se poate realiza şi într-o altă manieră, de exemplu prin citire de la tastatură sau
dintr-un fişier.

2.4 Problemă rezolvată


Se considera un set de N cuvinte. Sa se afişeze toate permutările posibile care se pot realiza între aceste cuvinte.

#include <conio.h>
#include <stdio.h>

int a[20],n,i;
int pr[20];

void perm(int i)
{ int j,f;
if (i==n+1)
{ printf("\n");
for (f=1;f<=n;f++) printf("%d ",pr[f]);

28
}
else
for (j=1;j<=n;j++)
if (pr[j]==-1)
{pr[j]=a[i];
perm(i+1);
pr[j]=-1;
}
}

void main()
{clrscr();
printf("Dati numarul de elemente = ");
scanf("%d",&n);
for (i=1;i<=n;i++)
{printf("\n\nDati valoarea
elementului %d = ",i);
scanf("%d",&a[i]);
pr[i]=-1;
}
perm(1);
getch(); }
3. Probleme propuse

1. Pentru problema de determinare a tuturor soluţiilor de ieşire dintr-un labirint, prezentată în cadrul acestei lucrări:
a). să se scrie programul de aflare a optimului (drumul cel mai scurt de iesire);
b). să se scrie un program pentru determinarea tuturor soluţiilor şi a optimului de ieşire dintr-un labirint
tridimensional.

2. Se consideră un set de N caractere. Să se afişeze toate combinaţiile de N luate cîte M ale acestor caractere.

3. Se consideră un set de N studenţi. Să se grupeze studenţii în grupuri de cîte 5, şi să se afişeze pentru fiecare grupă
toate permutările posibile.

4. Să se scrie cîte o funcţie recursivă pentru:


a). transformarea unui întreg din baza 10 în altă bază data
b). tipărirea nodurilor unei liste în ordine inversă
c). analog cu b), dar pentru elementele unui tablou
d). copierea în ordine inversă a liniilor dintr-un fisier text, în altul.

5. Sa se generalizeze programul TAIETURA (prezentat în curs) astfel încît să se gasească toate variantele,
precum şi cea optimă pentru tăierea unui fir de lungime L în părţi de lungimi L 1,L2,...,L n date, în condiţiile:
a). nu există nici un fel de restricţie în ce priveşte numărul de bucăţi din fiecare lungime;
b). se taie cel mult o bucată de fiecare lungime;
c). numărul de bucăţi de fiecare lungime să difere prin cel mult o unitate.
Lucrarea nr. 7
Algoritmi recursivi (continuare)

1. Scopul lucrării – îl reprezintă prezentarea în continuare a altor cîtorva clase tipice de algoritmi recursivi, respectiv
algoritmii cu revenire (backtracking) şi algoritmii de tipul “înlăţuie şi limitează” (branch and bound algorithms).

2.Aspecte teoretice
2.1. Algoritmi cu revenire (backtracking)
Principiul acestei tehnici este aceea de descompunere a obiectivului în obiective par ţiale (task-uri parţiale). De regulă
acestea sunt exprimate în mod recursiv şi constă în exploatarea unui număr finit de sub-task-uri. Întregul proces poate
fi privit ca un proces de încercare şi căutare care construie şte în mod gradat şi parcurge în acelaşi timp un arbore de
subprobleme.
Obţinerea unor soluţii parţiale sau finale care nu satisfac provoacă revenirea recursivă în cadrul procesului de calcul,
până la obţinerea soluţiei dorite, motiv pentru care astfel de algoritmi se numesc algoritmi cu revenire
(backtracking algorithms)
O formă generală a algoritmului este următoarea :

29
function incearca;
begin
iniţializează selecţia candidatilor;
repeat
selectează următorul candidat;
if acceptabil then
begin
înregistrează-l;
if soluţie incompletă then
begin
încearcă pasul următor;
if nu este reuşit then sterge inregistrarea
end
end
until (pas reuşit) or (nu mai sunt candidaţi)
end;
Acest model principial, general, poate fi însă concretizat în mai multe forme. Astfel, dacă presupunem că la fiecare
pas numărul candidatilor de examinat este fix (M=8 în cazul problemei calului prezentată la curs) precum şi că
procedura este apelată iniţial prin incearca(i), unde i reprezintă numărul mutării următoare, rezultă următoarea
formă particulară a algoritmului:

function incearca (i:integer);


var k: integer;
begin
k:=0;
repeat
k:=k+1;{selecteaza al k-lea candidat}
if acceptabil then
begin
înregistrează-l;
if i<n_total_solutie_completa then
begin
incearca(i+1);
if nereusita then
şterge înregistrarea
end
end
until reuşită or (k=m)
end;

Forma de mai sus a algoritmului generează însă o singură soluţie pentru problema dată; pentru determinarea tuturor
soluţiilor unor astfel de probleme (dacă există) este necesar ca generarea candida ţilor să se facă într-o manieră
ordonată, ceea ce garantează că un candidat nu poate fi generat decît o singură dată. De îndată ce o soluţie a fost
găsită şi înregistrată (de exemplu, tipărită), se trece la determinarea soluţiei următoare pe baza unui proces de selecţie
sistematică, pînă la epuizarea tuturor posibilităţilor. Schema generală derivată din cea anterioară (cu k mergînd pînă
la un m fix) care rezolvă această problemă este următoarea:

function incearca(i:integer);
var k:integer;
begin
for k:=1 to m do
begin
selectează cel de-al i-lea candidat;
if acceptabil then
begin
înregistrează-l;
if i< n_total_solutie_completa
then incearca(i+1)
else tipăreşte soluţia;
şterge înregistrarea
end
end

30
end;

Din cauza simplificării la un singur termen a condiţiei de terminare a procesului de selecţie (k=M în acest caz), ciclul
repeat a putut fi înlocuit cu unul for.

2.2. Algoritmi pentru realizarea selecţiei optime


Se propune în continuare un algoritm pentru aflarea soluţiei optime din mai multe soluţii posibile. În acest scop, este
necesar să fie generate toate soluţiile posibile, şi pe parcursul procesului de generare, să fie reţinută cea optimă, într-
un anumit sens.
Astfel, variabila optim înregistrează cea mai bună soluţie întîlnită pînă în momentul curent şi ea este iniţializată în
mod corespunzător.
Problema generală aflare a solţiei optime ar putea fi formulată în felul următor: se cere să se găsească maniera optima
de selecţie a unei submulţimi de obiecte dintr-o mulţime de bază dată, ţinînd cont de anumite constrîngeri.
Selecţionările care constituie soluţii acceptabile se construiesc în mod gradat, examinînd obiectele individuale ale
mulţimii de bază. Examinarea fiecărui obiect se poate termina în două moduri: fie cu includerea obiectului investigat
în soluţia curentă, fie cu excluderea sa. Un model general de algoritm ar fi următorul:

function incearca(i:integer);
{încearcă includerea /excluderea celui de-al i-lea obiect}
begin
if incluziunea este acceptabilă then
begin
include cel de-al i-lea obiect;
if i< n_total_solutie_completa
then incearca (i+1)
else verifică optimalitatea;
elimină cel de-al i-lea obiect;
end

if excluzimea este acceptabilă then


if i<n then incearca(i+1)
else verifică optimalitatea
end;

Algoritmii care se bazează pe tehnica revenirii şi care utilizează un factor de limitare care restrînge creşterea
arborelui potenţial de căutare se mai numesc şi algorimi de tip “înlăţuie şi limitează” (branch and bound
algorithms).

2.3. Problemă rezolvată


Se consideră un lac îngheţat de dimensiune NxM. Vom avea o broscuţă la poziţia (1,1) şi o insectă la poziţia (N,M).
Luînd în considerare ca broscuţa poate sări doar în L , să se afle drumul cel mai scurt pe care trebuie broscuţa sa-l
parcurgă de la poziţia initială pînă la insectă. OBS : în locul de unde a sărit broscuţa, gheaţa se sparge, aşa că în acel
loc broscuţa nu va mai putea sări.

#include <conio.h>
#include <stdio.h>

struct drum
{ int x,y;
} d[100],d_b[100];

int a[50][50],n,m,i,j;
int nr_pasi_b=-1,nr_pasi=0;

void afis_tabla()
{ clrscr();
int i,j;
for (i=3;i<=n+2;i++)
for (j=3;j<=m+2;j++)
{ gotoxy(i-2,j-2);
switch (a[i][j])
{ case -1:printf("°");break;

31
case 1:printf("˛");break;
case 2:printf("B");break;
case 3:printf("M");break;
}
}
}

void tratare()
{ if ((nr_pasi<nr_pasi_b)||(nr_pasi_b==-1))
{ nr_pasi_b=nr_pasi;
for (int i=0;i<nr_pasi;i++)
{d_b[i].x=d[i].x;
d_b[i].y=d[i].y;
}
}
}

void go(int x,int y)


{ if ((x==n+2)&&(y==m+2))
{ d[nr_pasi].x=x;
d[nr_pasi].y=y;
nr_pasi++;
tratare();
nr_pasi--;
}
else
{ d[nr_pasi].x=x;
d[nr_pasi].y=y;
nr_pasi++;
int temp=a[x][y];
a[x][y]=-1;
if (a[x-1][y-2]!=-1) go(x-1,y-2);
if (a[x-2][y-1]!=-1) go(x-2,y-1);
if (a[x-2][y+1]!=-1) go(x-2,y+1);
if (a[x-1][y+2]!=-1) go(x-1,y+2);
if (a[x+1][y+2]!=-1) go(x+1,y+2);
if (a[x+2][y+1]!=-1) go(x+2,y+1);
if (a[x+2][y-1]!=-1) go(x+2,y-1);
if (a[x+1][y-2]!=-1) go(x+1,y-2);
a[x][y]=temp;
nr_pasi--;
}
}

void main()
{ clrscr();
printf("Dati N=");scanf("%d",&n);
printf("Dati M=");scanf("%d",&m);
for (i=1;i<=n+4;i++)
for (j=1;j<=m+4;j++)
a[i][j]=-1;
for (i=3;i<=n+2;i++)
for (j=3;j<=m+2;j++)
a[i][j]=1;
a[n+2][m+2]=3;

go(3,3);

for (i=1;i<=n+4;i++)
for (j=1;j<=m+4;j++)
a[i][j]=-1;
for (i=3;i<=n+2;i++)
for (j=3;j<=m+2;j++)

32
a[i][j]=1;
a[n+2][m+2]=3;

for (i=0;i<nr_pasi_b;i++)
{a[d_b[i].x][d_b[i].y]=2;
afis_tabla();
getch();
a[d_b[i].x][d_b[i].y]=-1;
}
}

3. Probleme propuse :

1. Se consideră un set de NxN piese, fiecare avînd 4 valori pe acestea. Să se aranjeze şi să se rotească piesele în aşa
fel încît valorile vecine între acestea să coincidă.
Exemplu :

2. Se consideră un labirint tridimensional de dimensiune NxNxN. Să se realizeze programul cu ajutorul căruia se va


calcula un anumit drum prin acest labirint de la o anumită poziţie de start la o anumită destinaţie.

3. Numai 12 din cele 92 de soluţii ale problemei celor 8 regine diferă în mod esenţial, celelalte pot fi deduse din cele
12 prin considerente de simetrie faţă de axe sau faţă de centru. Să se scrie programul care determină cele 12 soluţii.

Lucrarea nr. 8
Structura de date lista

1. Scopul lucrării – îl reprezintă prezentarea structurii de date tip listă înlănţuită precum şi a operaţiilor care se
realizează asupra acesteia.

2.Aspecte teoretice
2.1. Implementarea listelor cu ajutorul tipului pointer
În această implementare, o listă liniară este o structură dinamică şi deci, ea poate fi definită în termeni recursivi
utilizînd următoarele structuri de date:

struct nod {
int cheie;
struct nod *urm;
...info;
};

O astfel de structură ar putea fi reprezentată schematic în felul următor:

33
inceput
cheie 1 2 3 Ν
urm … Νull
info … … … …

Figura 1: Exemplu de listă implementată cu ajutorul tipului pointer


Se observă existenţa unei variabile pointer P care indică începutul listei (primul nod).

2.1.1. Crearea listelor înlănţuite. Inserţia unui nod într-o listă înlănţuită

q=(nod*)malloc(sizeof(nod));
q->urm=inceput;
inceput=q;

Trebuie menţionat că această secvenţă de program nu asignează şi valorile cîmpurilor cheie şi info pentru noul
nod (cel inserat).
Inserţia unui nod la începutul listei se prezintă grafic astfel:

… …
inceput inceput

q->urm=inceput q->urm=inceput
q
inceput=q

Figura 2: Inserţia unui nod la începutul unei liste


Inserţia unui nod la sfirşitul unei liste se poate realiza prin secvenţa următoare:

r =(nod*)malloc(sizeof(nod));
r->urm=NULL;
sfirsit->urm=r;
sfirsit=r;

unde sfirsit este un pointer care indică întotdeauna sfirşitul listei (ultimul nod al acesteia).
Referitor la această secvenţă trebuie observat că aceasta nu poate insera un nod într-o listă vidă, deoarece sfirsit-
>urm nu există în acest caz. Deci, dacă se doreşte crearea unei liste în ordine naturală, primul nod trebuie inserat
printr-un alt procedeu, de exemplu printr-o inserţie la început de listă. Schematic, inserţia la sfîrşitul unei liste se
prezintă în felul următor:

sfirsit Null
sfirsit=r
sfirsit->urm=r

r
Null

Figura 3: Inserţia unei nod la sfirşitul unei liste


Pe lîngă cele două situaţii prezentate anterior (inserţia unui nod la începutul unei liste şi respectiv inserţia unui nod la
sfirşitul unei liste), mai rămîne de arătat modalitatea de a insera noduri într-o poziţie oarecare a unei liste. Aici putem
avea două cazuri şi anume cînd se doreşte ca inserţia noului nod să aibă loc după un anumit nod, indicat de un pointer
p, sau înaintea acestuia.
Astfel, fie p pointerul care indică un nod al listei iar q o variabilă pointer ajutatoare, care va indica nodul care se
inserează. Inserţia acestuia după nodul indicat de pointerul p, p->, se realizează prin urmatoarea secvenţă:
Grafic, inserţia se prezintă în felul următor:

34
q 25 q->urm=p->urm

p->urm=q

10 20 30 …

Figura 4: Inserţia unui nod după nodul indicat de pointerul p


q =(nod*)malloc(sizeof(nod));
q->urm=p->urm;
p->urm=q;

Dacă se doreşte inserţia noului nod înaintea nodului indicat de p, *p, apare o complicaţie generată de imposibilitatea
practică de a afla simplu adresa predecesorului lui *p. Această problemă însă se poate rezolva simplu printr-un
artificiu şi anume: se inserează un nou nod după nodul *p; în acest nod, se asignează toate cîmpurile, mai puţin cel
de înlănţuire (urm), cu valorile nodului *p; în acest moment, nodul *p va exista în dublu exemplar în listă, pe poziţia
iniţială şi în poziţia imediat următoare; după care se asignează cu noile valori cîmpurile cheie şi info
corespunzatoare vechiului nod *p. Acest lucru se poate ilustra prin următoarea diagramă:

q 20

*q=*p

10 20 30 …

Figura 5a: Crearea unui nod şi copierea vechiului nod peste acesta

20

p->urm=q

10 15 30 …

Figura 5b: Inlănţuirea în listă a noului nod

Secvenţa de program corespunzătoare este următoarea:

q =(nod*)malloc(sizeof(nod));
*q=*p;
p->urm=q;

2.1.2. Suprimarea nodurilor dintr-o listă înlănţuită

35
Să presupunem că avem un pointer p care indică un nod al unei liste liniare înlănţuite, şi se cere să se suprime
succesorul nodului indicat de p, şi anume nodul p->urm. Aceasta se poate face prin următoarea secvenţă:

r=p->urm;
p->urm=r->urm;

În secvenţa de program mai sus se poate evita utilizarea variabilei pointer r prin înlocuirea fragmentului de mai sus
cu instrucţiunea:

p->urm=p->urm->urm;

Utilizarea variabilei r are însă avantajul că, prin intermediul ei, programatorul poate avea acces ulterior la nodul
suprimat, acces care altfel se pierde.
Dacă programatorul nu mai are nevoie ulterior de nodul suprimat, se recomand ă utilizarea procedurii standard
dispose(r) pentru a elibera zona de memorie ocupată de *r.
Daca însă se doreşte suprimarea chiar a nodului indicat de pointerul p dintr-o listă, şi anume chiar nodul *p,
observăm că şi aici apare aceeaşi dificultate, generată de imposibilitatea de a afla simplu adreasa predecesorului lui
*p. Soluţia se bazează pe aceeaşi tehnică: se aduce succesorul în locul lui *p şi apoi se suprimă vechiul succesor.
Secvenţa de program care realizează acest lucru este următoarea:

r=p->urm;
*p=*r;
free(r);

Se remarcă faptul că ambele tehnici de suprimare se pot aplica numai daca p nu este ultimul nod al listei, adică în
cazul în care r->urm!=NULL.

2.1.3. Traversarea unei liste înlănţuite


Se înţelege prin traversarea unei liste parcurgerea secvenţială a acesteia în scopul executării unei anumite operaţii
asupra tuturor nodurilor listei.
Astfel, fie inceput pointerul care indică începutul listei şi q o variabilă pointer auxiliară. Traversarea se va face
astfel:

q=inceput;
while (q!=NULL)
{
prelucrare(*q);
q=q->urm;
}

O operaţie frecvent utilizată o reprezintă căutarea unui nod dat într-o listă: secvenţa prin care se caută nodul cu cheia
x dată într-o listă înlănţuită unde inceput este pointerul de început de listă este următoarea:

#define 0 FALSE
#define 1 TRUE

b=FALSE;
q=inceput;
while ((q!=NULL) && (b==FALSE))
if (q->cheie==x) b=TRUE;
else q=q->urm;

Dacă după terminarea secvenţei avem b=true atunci *q este nodul căutat; în caz contrar, nodul nu s-a găsit şi
avem q=NULL.

2.2. Problemă rezolvată


Se se realizeze un program cu ajutorul caruia se va putea prelucra o listă simplu înlanţuită, în care fiecare nod va
contine informaţii specifice unui student : nume, prenume, grupa. Programul va fi realizat folosind un meniu, avînd
următoarele opţiuni : Adaugare student, Ştergere student, Afişare student (informaţiile referitoare la studentul cu un
anumit nume), Afişare lista studenţi, Ieşire program.

36
#include <conio.h>
#include <stdio.h>
#include <string.h>

struct student_struct
{ char nume[20];
char prenume[20];
char grupa[6];
student_struct *urm;
};

student_struct *prim,*p,*t;
int o;

void adauga_student()
{ char nume[20],prenume[20],grupa[6];
printf("\n\nAdaugare student");
printf("\n\nDati numele studentului : ");
scanf("%s",nume);
printf("Dati prenumele studentului : ");
scanf("%s",prenume);
printf("Dati grupa studentului : ");
scanf("%s",grupa);
if (prim==NULL)
//daca nu avem o lista, se crează primul nod
{ prim=new student_struct;
strcpy(prim->nume,nume);
strcpy(prim->prenume,prenume);
strcpy(prim->grupa,grupa);
prim->urm=NULL;
}
else //daca avem deja o lista, adaugam la capat
{ p=prim;
while (p->urm!=NULL) p=p->urm;
t=new student_struct;
strcpy(t->nume,nume);
strcpy(t->prenume,prenume);
strcpy(t->grupa,grupa);
t->urm=NULL;
p->urm=t;
}
}

void afis_lista()
{printf("\n\nAfisare lista studenti\n\n");
if (prim==NULL)
printf("Nu avem nici un student in lista");
else
{ p=prim;
while (p!=NULL)
{printf("\nNume:%s\nPrenume:%s\nGrupa:%s\n",p->nume,p->prenume,p->grupa);
p=p->urm;
}
}
getch();
}

void afis_student()
{char nume[20];
printf("\n\nAfisare student");
printf("\n\nDati numele studentului
cautat = ");
scanf("%s",nume);

37
int gasit=0;
p=prim;
while (p!=NULL)
{if (strcmp(p->nume,nume)==0)
{gasit=1;
printf("\nNume:%s\nPrenume:%s\nGrupa:%s\n",
p->nume,p->prenume,p->grupa);
}
p=p->urm;
}
if (!gasit)
printf("\nNu am gasit studentul %s.",nume);
getch();
}

void sterge_student()
{ printf("\n\nStergere student");
char nume[20];
printf("\n\ndati numele studentului ce se sterge : ");
scanf("%s",nume);
p=prim;int gasit=0;
while (p!=NULL)
{if (strcmp(p->nume,nume)==0)
{gasit=1;
printf("\nNume:%s\nPrenume:%s\nGrupa:%s\n",
p->nume,p->prenume,p->grupa);
break;
}
p=p->urm;
}
if (!gasit)
printf("Nu am gasit studentul %s.",nume);
else
{if (p==prim) //daca e tocmai primul din lista
prim=prim->urm;
else
if (p->urm==NULL) //daca e ultimul
{t=prim;
while (t->urm!=p) t=t->urm;
delete p;
t->urm=NULL;
}
else //daca e undeva prin lista
{t=prim;
while (t->urm!=p) t=t->urm;
t->urm=p->urm;
delete p;
}
}
getch();
}
void main()
{do
{clrscr();
printf("Meniu\n");
printf("1.Adaugare student\n");
printf("2.Stergere student\n");
printf("3.Afisare student\n");
printf("4.Afisare lista studenti\n");
printf("5.Iesire\n\n");
printf("Alegeti optiunea : ");scanf("%d",&o);
switch (o)
{ case 1:adauga_student();break;

38
case 2:sterge_student();break;
case 3:afis_student();break;
case 4:afis_lista();break;
}
}
while (o!=5);
}

3. Probleme propuse

1. Se consideră o listă simplu inlanţuită în care fiecare nod va conţine un cuvînt. Să se sorteze aceste cuvinte în
ordine alfabetică folosind algoritmul de sortare Quicksort. Evaluaţi performanţele unui astfel de algoritm de sortare,
pentru diferite valori ale numărului de cuvinte (10, 1000, 10000) şi realizaţi o comparaţie faţa de utilizarea unui
tablou în locul unei liste. Comentaţi rezultatul.

2. Se dă o listă înlănţuită L care conţine ca şi elemente şiruri de caractere (cuvinte), şi o altă listă P, care conţine
numere întregi în ordine crescătoare. Să se scrie funcţia prin care se şterg din lista L toate elementele care se află pe
poziţiile specificate prin numerele din lista P, ca o funcţie cu parametri de tipul sterge(L,P). Funcţia va trata prin
mesaje sugestive situaţiile de eroare.

3. Se consideră două fraze oarecare, formate din cuvinte separate prin caracterul spaţiu, virgulă sau punct-virgulă,
care se citesc de la tastatură. Frazele se termină cu caracterul punct. Să se scrie un program care, utilizînd o structură
de listă simplu înlănţuită pentru memorarea frazelor, determină cite cuvinte comune au cele două fraze.

4. Să se scrie o funcţie care realizează operaţia de cut-and-paste a unei porţiuni dintr-o listă înlănţuită L1 în altă listă
înlănţuită L2, conţinînd şiruri de caractere, astfel încît numărul de mutări de elemente să fie cît mai mic posibil.
Funcţia va conţine ca şi parametri : poziţia de început respectiv de sfîrşit din prima lista L1, pentru care se face cut, şi
poziţia din a doua lista L2 după care se face paste. Să se scrie un program care utilizează funcţia astfel creată, şi care
va afişa cele două liste în formă iniţială şi apoi, după realizarea operaţiei cut-and-paste.

5. Problema Josephus : Se aşează un număr de N copii, numerotaţi de la 1 la N, într-un cerc. Pornind de la copilul cu
numărul 1, se trece o minge de la un copil la altul, şi fiecare al M-lea copil la care ajunge mingea este eliminat din
cerc. Jocul continuă cu persoana care urmează după copilul eliminat. Cîştigătorul este ultimul copil rămas cu mingea.
Să se scrie programul care rezolvă acest joc, pentru valori date ale lui M respectiv N.

Lucrarea nr. 9
Liste ordonate. Utilizarea tehnicii fanionului în structura de listă

1. Scopul lucrarii – îl reprezintă prezentarea modalităţilor de creare a listelor ordonate precum şi a utilizării tehnicii
fanionului în cadrul structurii de listă înlănţuită

2.Aspecte teoretice
2.1. Crearea unei liste ordonate utilizînd tehnica celor doi pointeri
Deosebirea fată de variantele anterioare de construcţie a listelor înlănţuite, constă în aceea că în acest caz inserţia
unui nou nod nu se mai face întotdeauna la începutul listei, sau la sfirşitul ei, ci în acea poziţie astfel încît lista să se
păstreze ordonată.
Crearea unei liste ordonate se realizează deosebit de simplu deoarece înainte de inserţia unui nod, acesta trebuie
oricum căutat în listă. Dacă lista este ordonată (să presupunem crescător), atunci căutarea se va termina cu prima
cheie mai mare decat cea căutată. Inserţia unui nod într-o lista ordonată reprezintă o aplicaţie a problemei inserţiei
unui nod înaintea celui indicat de un pointer, metodă care a fost descrisă în adrul lucrarii 8.
O altă tehnică de inserţie care poate fi utilizată în acest caz o reprezintă aşa numita tehnică a celor doi pointeri.
Aceasta se bazează pe utilizarea a doi pointeri, de exemplu q1 şi q2, care indică tot timpul două noduri consecutive
ale listei.

39
Inceput

Q2 Q1

Figura 6: Traversarea unei liste înlănţuite utilizând tehnica celor doi pointeri
Cei doi pointeri avansează simultan de-a lungul listei pînă cînd cheia lui *q1 devine mai mare sau egala cu x (x
reprezentînd cheia căutată în listă), lucru care se va întampla cu certitudine, în cel mai rău caz în momentul în care
*q1 devine egal cu fanionul (se utilizează tehnica fanionului şi în acest caz). Dacă în acel moment cheia lui *q1 este
strict mai mare decat x sau q1=fanion, atunci înseamnă că nodul nu există de fapt în listă şi deci trebuie inserat;
inserţia noului nod se face întotdeauna între nodurile indicate de cei doi pointeri q1 şi q2. În caz contrar, adică în
cazul în care cheia lui q1 este egală cu x, înseamnă că nodul cu cheia x a fost găsit în cadrul listei şi atunci, funcţie
de specificul aplicaţiei în care se utilizează lista, se va permite sau nu inserarea de noduri cu aceeaşi cheie.
Funcţionarea corectă a algoritmului care utilizează tehnica celor doi pointeri presupune existenţa iniţială în listă a cel
puţin două noduri, deci cel puţin încă un nod în afara fanionului. Din acest motiv lista se va iniţializa cu două noduri
fictive, astfel:

inceput=(nod *)malloc(sizeof(nod));
fanion=(nod *)malloc(sizeof(nod));
inceput->urm=fanion;

Ca principiu, *inceput va fi un aşa zis fanion de început , în timp ce *fanion va reprezenta nodul fanion obişnuit,
plasat la sfirşitul listei.
Funcţia de insertie a unui nod cu cheia x într-o listă ordonată crescător , în care nu se admit inserţii multiple, va avea
următoarea formă :

void insert_ord(int x)
{
struct nod *q1, *q2, *q3;
q2=inceput;
q1=q2->urm;
fanion->cheie=x;
while (q1->cheie<x)
{
q2=q1;
q1=q2->urm;
}
if ((q1->cheie==x) && (q1!=fanion))
q1->numar++;
// contorizeaza numarul de aparitii ale aceleiasi chei
else
{
q3=(nod *)malloc(sizeof(nod));
q3->cheie=x;
q3->numar=1;
q3->urm=q1;
q2->urm=q3;
}
}

2.2. Căutarea în listă cu reordonare


Căutarea în listă cu reordonare reprezintă o metodă de căutare şi inserţie a nodurilor într-o listă înlănţuită care asigură
o performanţă mai bună la căutarea ulterioară a unui nod. Ea constă în aceea că, ori de cîte ori un identificator se
caută şi se găseşte în listă, el se va muta la începutul listei, astfel încît la proxima sa căutare el va fi găsit imediat. Cu
alte cuvinte lista se reordonează după fiecare căutare finalizată cu găsirea nodului. Dacă un nod nu este găsit în listă,
el se va insera la începutul acesteia.
Utilizarea reordonării presupune existenţa unui nod fanion. Astfel, în programul principal sunt necesare în acest caz
următoarele iniţializări:

40
inceput=(nod *)malloc(sizeof(nod));
fanion=inceput;
Funcţia de căutare/inserţie în listă cu reordonare a unui nod cu cheia x este următoarea:

void caut_reord(int x)
{
struct nod *q1, *q2, *q3;
q1=inceput;
fanion->cheie=x;
if (q1==fanion)
{
inceput=(nod *)malloc(sizeof(nod));
inceput->cheie=x;
inceput->numar=1;
inceput->urm=fanion;
}
else
if (q1->cheie==x) q1->numar++;
else
{
do
{
q2=q1;
q1=q2->urm;
} while (q1->cheie!=x);
if (q1==fanion)
{
q2=inceput;
inceput=(nod *)malloc(sizeof(nod));
inceput->cheie=x;
inceput->numar=1;
inceput->urm=q2;
}
else
{
q1->numar++;
q2->urm=q1->urm;
q1->urm=inceput;
inceput=q1;
}
}
}

2.3. Problemă rezolvată


Se considera o listă simplu înlanţuită, în care fiecare nod conţine informaţiile specifice ale unui client la o banca:
nume,prenume,adresa,suma cont. Să se realizeze un program cu ajutorul căruia se va citi un anumit număr de clienţi,
şi va afişa apoi lista de clienţi în ordinea descrescătoare a valorii contului. (Obs  : Introducerea unui client în lista
afişată în final se va face astfel încît lista să fie tot timpul sortată, aşa încat la afisarea listei nu va mai fi nevoie de
înca o sortare finală.)

#include <conio.h>
#include <stdio.h>
#include <string.h>

struct client_struct
{ char nume[20];
char prenume[20];
char adresa[50];
long int suma_cont;
client_struct *urm;
};

client_struct *prim,*p1,*p2,*t;

41
int o;

void afisare()
{ if (prim==NULL)
printf("\n\nNu exita nici un client
in lista");
else
{ p1=prim;
while (p1!=NULL)
{printf("\nNume : %s",p1->nume);
printf("\nPrenume : %s",p1->prenume);
printf("\nAdresa : %s",p1->adresa);
printf("\nCont : %ld$\n",
p1->suma_cont);
p1=p1->urm;
}
}
getch();
}

void introducere()
{ char nume[20],prenume[20],adresa[50];
long int suma_cont;
printf("\nDati numele : ");scanf("%s",nume);
printf("Dati prenumele : ");
scanf("%s",prenume);
fflush(stdin);
printf("Dati adresa : ");gets(adresa);
printf("Dati valoarea contului : ");
scanf("%ld",&suma_cont);
if (prim==NULL)
{ prim=new client_struct;
strcpy(prim->nume,nume);
strcpy(prim->prenume,prenume);
strcpy(prim->adresa,adresa);
prim->suma_cont=suma_cont;
prim->urm=NULL;
}
else
{t=new client_struct;
strcpy(t->nume,nume);
strcpy(t->prenume,prenume);
strcpy(t->adresa,adresa);
t->suma_cont=suma_cont;
p2=prim;
while((p2->suma_cont>suma_cont)&&(p2!=NULL))
p2=p2->urm;
if (p2==prim)
{t->urm=p2;
prim=t;
}
else
{p1=prim;
while (p1->urm!=p2) p1=p1->urm;
t->urm=p2;
p1->urm=t;
}
}
}

void main()
{do
{ clrscr();

42
printf("Meniu\n");
printf("\n1.Introducere client");
printf("\n2.Afisare lista");
printf("\n3.Iesire");
printf("\n\nAlegeti optiunea : ");
scanf("%d",&o);
switch (o)
{case 1:introducere();break;
case 2:afisare();break;
}
}
while (o!=3);
}

3. Probleme propuse

1. Se considera o lista simplu inlantuita cu 50 de noduri în care fiecare nod va conţine o valoare de la 1 la 50. Se vor
citi apoi un set de N numere, şi folosindu-se principiul căutării cu reordonare, să se afiseze în final lista sub noua
formă.

2. Se dau două polinoame de forma :


P(x) = c1xe1 + c2xe2 + ….+c nxen, unde e1>e2>…>e n>=0
care se citesc de la tastatură, sub forma exponent-coeficient. Se utilizează structura de listă înlanţuită ordonată
(descrescător, funcţie de valoarea exponentului) pentru memorarea polinoamelor.
Să se scrie un program care determină polinomul sumă şi polinomul produs a celor două polinoame citite iniţial.
Polinoamele rezultate vor fi afişate în ordinea descrescătoare a exponenţilor.

3. Să se realizeze un meniu cu următoarele opţiuni  : Adăugare nod, Stergere nod, Afişare listă. Să se scrie un
program care, utilizînd meniul de mai sus, va crea o listă înlănţuită cu chei intregi, cu proprietatea că valorile de pe
poziţiile pare vor fi sortate întotdeauna crescător, iar valorile de pe poziţiile impare vor fi sortate descrescător.

4. Informaţiile despre rezultatele concursului de admitere, la care au fost admişi un număr de N studenţi
(N=NO1+NO2+NO3) sunt păstrate într-o listă odonată descrescător funcţie de media cu care fiecare student a fost admis.
Pentru fiecare student se cunoaşte numele, media şi secvenţa de opţiuni alese (O1 sau O2 sau O3).Această listă se
consideră data de intrare. Să se scrie programul care repartizeaza pe opţiuni cei N studenţi admişi, ştiind că există N Oi
locuri disponibile la fiecare opţiune, şi că repartiţia se face în ordinea descrescătoare a mediilor şi a opţiunilor.
Programul va afişa lista iniţială a studeţilor admişi precum şi listele (înlănţuite  !) obţinute în urma repartizării
acestora pe opţiuni, în ordine alfabetică pentru fiecare opţiune.
Lucrarea nr. 10
Liste dublu înlănţuite

1. Scopul lucrării – îl reprezintă prezentarea particularităţilor listelor dublu înlănţuite prin comparaţie cu cele simplu
înlănţuite.

2. Aspecte teoretice
2.1. Caracteristicile listei dublu înlănţuite
Structura de date tip listă dublu înlăţuită se poate defini utilizînd următoarele tipuri de date:

struct nod {
tipelement element;
struct nod *anterior, *urmator;
};

unde tipelement reprezintă tipul, definit de utilizator, elementelor listei. Opera ţiile de bază care se execută asupra
listelor dublu înlănţuite sunt aceleaşi ca şi pentru listele simplu înlănţuite, diferenţa constînd în aceea că, la
implementarea acestora trebuie ţinut cont şi de cel de-al doilea cîmp de înlănţuire (cîmpul anterior).

… …
… …

Figura 7: Listă dublu înlănţuită

43
Utilizarea listelor dublu înlănţuite este recomandată în aplicaţiile care necesită traversarea listei în ambele sensuri.
Preţul care se plăteşte este prezenţa unui cîmp suplimentar de tip pointer în fiecare nod şi o oarecare creştere a
lungimii unor funcţii care implementează operatorii de bază care prelucrează astfel de liste.

2.2. Problemă rezolvată


Să se realizeze un program cu ajutorul căruia se va putea prelucra o listă dublu înlănţuita , în care fiecare nod va
conţine informaţii specifice unui student : nume, prenume, grupa. Programul va fi realizat folosind un meniu, avînd
următoarele opţiuni : Adaugare student, Stergere student, Afisare student, Afisare lista student, Iesire program.
#include <conio.h>
#include <stdio.h>
#include <string.h>

struct student_struct
{ char nume[20];
char prenume[20];
char grupa[6];
student_struct *urm,*prec;
};

student_struct *prim,*p,*t;
int o;

void adauga_student()
{ char nume[20],prenume[20],grupa[6];
printf("\n\nAdaugare student");
printf("\n\nDati numele studentului : ");
scanf("%s",nume);
printf("Dati prenumele studentului : ");
scanf("%s",prenume);
printf("Dati grupa studentului : ");
scanf("%s",grupa);
if (prim==NULL) //daca nu avem lista,se crează primul nod
{ prim=new student_struct;
strcpy(prim->nume,nume);
strcpy(prim->prenume,prenume);
strcpy(prim->grupa,grupa);
prim->urm=NULL;
prim->prec=NULL;
}
else
//daca avem deja o lista, adaugam la capat
{ p=prim;
while (p->urm!=NULL) p=p->urm;
t=new student_struct;
strcpy(t->nume,nume);
strcpy(t->prenume,prenume);
strcpy(t->grupa,grupa);
t->urm=NULL;
p->urm=t;
t->prec=p;
}
}

void afis_lista()
{printf("\n\nAfisare lista studenti\n\n");
if (prim==NULL)
printf("Nu avem nici un student in lista");
else
{ p=prim;
while (p!=NULL)
{printf("\nNume:%s\nPrenume:%s\nGrupa:%s\n",
p->nume,p->prenume,p->grupa);
p=p->urm;

44
}
}
getch();
}

void afis_student()
{char nume[20];
printf("\n\nAfisare student");
printf("\n\nDati numele studentului
cautat = ");
scanf("%s",nume);
int gasit=0;
p=prim;
while (p!=NULL)
{if (strcmp(p->nume,nume)==0)
{gasit=1;
printf("\nNume:%s\nPrenume:%s\nGrupa:%s\n",
p->nume,p->prenume,p->grupa);
}
p=p->urm;
}
if (!gasit) printf("\nNu am gasit studentul %s.",nume);
getch();
}

void sterge_student()
{ printf("\n\nStergere student");
char nume[20];
printf("\n\ndati numele studentului ce
se sterge : ");
scanf("%s",nume);
p=prim;int gasit=0;
while (p!=NULL)
{if (strcmp(p->nume,nume)==0)
{gasit=1;
printf("\nSterg\nNume:%s\nPrenume:%s\n
Grupa:%s\n",p->nume,p->prenume,p->grupa);
break;
}
p=p->urm;
}
if (!gasit)
printf("Nu am gasit studentul %s.",nume);
else
{if (p==prim) //daca e tocmai primul din lista
{prim=prim->urm;
prim->prec=NULL;
}
else
if (p->urm==NULL) //daca e ultimul
{t=p->prec;
delete p;
t->urm=NULL;
}
else //daca e undeva prin lista
{t=p->prec;
t->urm=p->urm;
p->urm->prec=t;
delete p;
}
}
getch();
}

45
void main()
{do
{clrscr();
printf("Meniu\n");
printf("1.Adaugare student\n");

printf("2.Stergere student\n");
printf("3.Afisare student\n");
printf("4.Afisare lista studenti\n");
printf("5.Iesire\n\n");
printf("Alegeti optiunea : ");
scanf("%d",&o);
switch (o)
{ case 1:adauga_student();break;
case 2:sterge_student();break;
case 3:afis_student();break;
case 4:afis_lista();break;
}
}
while (o!=5);
}

3. Probleme propuse

1. a). Realizaţi implementarea operaţiilor de bază pentru liste dublu înlănţuite  : inserţie, traversare în ambele sensuri,
căutare, ştergere.
b). Scrieţi o funcţie care schimbă între ele două elemente adiacente ale unei liste dublu înlănţuite. Poate fi realizată
această funcţie numai prin modificare înlănţuirilor, fără a face mutări de date ?

2. Realizaţi implementarea operaţiilor de bază pentru liste dublu înlănţuite circulare  : inserţie, traversare în ambele
sensuri, căutare, ştergere.

3. Se consideră o listă dublu înlănţuită în care fiecare nod va conţine un cuvant. Să se sorteze aceste cuvinte în ordine
alfabetică folosind un algoritm de sortare, la alegere.

Lucrarea nr. 11
Stive şi cozi

1. Scopul lucrării – îl reprezintă prezentarea structurilor de coadă şi stivă, ca structuri derivate din structura de listă.
De asemenea, se prezintă şi implementări alternative ale acestora, în varianta de utilizare a tablourilor în locul listelor
înlănţuite.

2. Aspecte teoretice
2.1. Stive
O stivă este un tip special de listă în care toate inserţiile şi suprimările se execută la un singur capăt care se numeşte
vîrful stivei. Din acest motiv, stivele se mai numesc şi structuri lista de tip LIFO
(Last-In-First-Out).
Structura de stivă poate fi considerată o listă cu un caracter mai special; în consecinţă, toate implementările listelor
descrise pînă în prezent sunt valabile şi în cazul stivei. Particularitatea stivei în implementarea cu liste constă în aceea
că inserţiile şi suprimările sunt permise la un singur capăt, vîrful stivei, conform principiului LIFO.
Pentru implementarea stivelor se poate însă utiliza şi structura de tablou, în cadrul căruia se poate obţine o utilizare
mai eficientă a acestuia, dacă se ţine cont de faptul că inserţiile şi suprimările se fac numai la un singur capăt. Astfel,
se poate considera drept bază a stivei sfîrşitul tabloului (indexul maxim), stiva crescînd în sensul descreşterii
indexului de tablou. Un întreg numit virf indică poziţia curentă al primului element al stivei:

46
1

..
.
VIRF PRIMUL ELEMENT
AL DOILEA ELEMENT
..
.
LUNGIME_MAXIMĂ ULTIMUL ELEMENT

Figura 8: Implementarea stivei cu ajutorul tipului tablou


Structura de date utilizată este următoarea:

#include <stdio.h>
#define TRUE 1
#define FALSE 0
#define lungime_maxima 10

typedef struct{
int virf;
int elemente[lungime_maxima];
}stiva;

Implementarea celor cinci operaţii de bază asupra stivelor este următoarea:

int er;
stiva s;

void initilizare()
{
s.virf=lungime_maxima;
}

int stivid(stiva s)
{
if(s.virf>lungime_maxima) return TRUE;
else return FALSE;
}

tipelement virfst(stiva s)
{
if (stivid(s)==TRUE)
{
er=TRUE;
printf("stiva este vida");
}
else
return s.elemente[s.virf];
}

tipelement pop()
{
if (stivid(s)==TRUE)
{
er=TRUE;
printf("stiva este vida");
}
else
{
return s.elemente[s.virf++];
}
}

47
void push(tipelement x)
{
if (s.virf==1)
{
er=TRUE;
printf("stiva este plina");
}
else
{
s.virf--;
s.elemente[s.virf]=x;
}
}

2.2. Cozi
Cozile sunt o categorie specială de liste în care elementele sunt întotdeauna inserate la un capăt (spatele cozii) şi sunt
suprimate la celălalt capăt (faţa cozii). Din acest motiv, cozile se mai numesc şi liste FIFO (First-In-First-Out). Ca şi
în cazul stivelor, orice implementare al listelor este valabil ă şi pentru cozi. Structurile de date care se utilizează sunt
urmatoarele:

#define TRUE 1
#define FALSE 0

struct nod {
tipelement element;
struct nod *urm;
};

Pe baza acestora se poate defini o structură de tip coadă care constă din doi pointeri indicînd faţa respectiv spatele
unei liste înlănţuite. Astfel, se defineşte structura de coadă:

typedef struct{
struct nod *fatza, *spate;
}coada;

Primul nod al cozii este un nod fictiv în care cîmpul element este ignorat. Acestă convenţie permite o reprezentare
şi o manipulare mai uşoară a cozii vide.
Secvenţele de program care implementează cele 5 operaţii de bază care se efectuează asupra cozilor sunt
urmatoarele:

coada c;
int er;
void initializare()
{
c.fatza=(nod*)malloc(sizeof(nod));
c.fatza->urm=NULL;
c.spate=c.fatza;
}

int vid(coada c)
{
if (c.fatza==c.spate) return TRUE;
else return FALSE;
}

tipelement fatza(coada c)
{
if (vid(c)==1)
{
er=TRUE;
printf("coada este vida");

48
}
else
return c.fatza->urm->element;
}

void adauga(tipelement x)
{
c.spate->urm=(nod*)malloc(sizeof(nod));
c.spate=c.spate->urm;
c.spate->element=x;
c.spate->urm=NULL;
}

void scoate()
{
if (vid(c)==TRUE)
{
er=TRUE;
printf("coada este vida");
}
else
c.fatza=c.fatza->urm;
}

O altă variantă de implementare o reprezintă tablourile circulare. Un tablou circular este un tablou în care prima
poziţie urmează ultimei poziţii, după cum arată figura următoare:
LUNGIME MAXIMĂ 1

.
.
.
.
.
.
.
.
.
.
.
.

C, SPATE C, FATZA
COADA

Figura 9: Implementarea cozii cu ajotorul tablourilor circulare

În această implementare coada se găseşte undeva în jurul cercului (tabloului circular), în poziţii consecutive, avînd
spatele undeva înaintea feţei la parcurgerea cercului în sensul acelor de ceasornic. Înlănţuirea unui element presupune
mişcarea pointerului c.spate cu o poziţie în sensul acelor de ceasornic şi introducerea elementului în această
poziţie iar scoaterea presupune simpla mutare a pointerului c.fatza cu o poziţie în sensul acelor de ceasornic.
Astfel, coada se roteşte în sensul acelor de ceasornic după cum se adaugă sau se scot elemente din ea.
Pentru sesizarea corectă a cozii vide (şi respectiv pline), în tablou se vor introduce doar lungime_maxima-1
elemente (deşi există lungime_maxima poziţii). Astfel, testul de coadă plina conduce la o valoare adevarată dacă
c.spate devine egal cu c.fatza după două înaintări succesive, iar testul de coadă vidă conduce la o valoare
adevărată dacă c.spate devine egal cu c.fatza după înaintarea cu o poziţie. Structura de date utilizată în
această implementare este:

#define TRUE 1
#define FALSE 1
#define lungime_maxima 10

typedef struct{
tipelement elemente[lungime_maxima];

49
int fatza, spate;
} coada;

Implementarea operaţiilor de bază este următoarea:

coada c;
int er;

int avanseaza(int i)
{
div_t x;
x = div(i,lungime_maxima);
return x.rem;
}
int vid(coada c)
{
if (avanseaza(c.spate)==c.fatza) return TRUE;
else return FALSE;
}
tipelement fatza(coada c)
{
if (vid(c)==TRUE)
{
er=TRUE;
printf("coada este vida");
}
else
return c.elemente[c.fatza];
}

void adauga(tipelement x)
{
if (avanseaza(avanseaza(c.spate))==c.fatza)
{
er=TRUE;
printf("coada este plina");
}
else
{
c.spate=avanseaza(c.spate);
c.elemente[c.spate]=x;
}
}

void scoate()
{
if (vid(c)==TRUE)
{
er=TRUE;
printf("coada este vida");
}
else
c.fatza=avanseaza(c.fatza);
}

2.3. Problemă rezolvată


Sa se realizeze un program cu ajutorul caruia se va putea simula funcţionarea unei stive folosind o listă simplu
inlănţuită. Ca şi interfaţă, se va considera un meniu cu următoarele opţiuni : Adaugare stivă (PUSH), Extragere stivă
(POP), Golire stivă, Afişare stiva.

#include <conio.h>
#include <stdio.h>

50
int o,i,stiva[100],sp=-1;
//sp=pozitia ultimului element din stiva

void afisare()
{ printf("\n");
if (sp==-1)
printf("Stiva este goala");
else
{printf("Stiva : ");
for (i=0;i<=sp;i++)
printf("%d ",stiva[i]);
}
getch();
}

void adaugare()
{ if (sp==99)
{printf("Stiva este deja plina !!!");
getch();
}
else
{int valoare;
printf("\nDati numarul de introdus in stiva : ");
scanf("%d",&valoare);
stiva[++sp]=valoare;
}
}

void scoate()
{if (sp==-1)
printf("Stiva este goala !!!");
else
printf("Elementul scos este %d",stiva[sp--]);
getch();
}

void golire()
{
sp=-1;
}

void main()
{ do
{ clrscr();
printf("Meniu");
printf("\n1.Adaugare in stiva (PUSH)");
printf("\n2.Scoatere din stiva (POP)");
printf("\n3.Golire stiva");
printf("\n4.Afisare stiva");
printf("\n5.Iesire");
printf("\n\nAlegeti optiunea : ");
scanf("%d",&o);
switch (o)
{
case 1:adaugare();break;
case 2:scoate();break;
case 3:golire();break;
case 4:afisare();break;
}
}
while (o!=5);
}

51
3. Probleme propuse

1. O listă dublu înlănţuită conţine o expresie aritmetică în notatie postfix, în care operanzii sunt reprezentaţi prin
litere mici, iar operatorii pot fi +, -, * sau / , nodurile avînd urmatoarea structură:

struct nod {
char oper; //operandul sau operatorul
int val;
//valoare operand sau 0 dacă e operator
nod *urm, *ant;
};
Să se scrie o funcţie de evaluare a expresiei aritmetice, folosind o stivă în care se depun valorile operanzilor şi
rezultatele parţiale (se va implementa stiva printr-o lista sau printr-un tablou). Pe parcursul evaluării, se vor face
verificarile privind corectitudinea expresiei, o eroare fiind semnalată printr-un mesaj, oprind evaluarea expresiei.

2. Este posibil să fie memorate două structuri de date de tip stivă într-un acelaşi tablou, una crescînd de la pozitia 1
spre sfirşit, cealaltă în sens invers. Se cere să se redacteze o funcţie generală push(x,s) unde s este una sau alta
dintre stive. Funcţia va include toate testele de eroare necesare, care vor fi semnalate prin mesaje corespunzătoare.

3. Să se realizeze un program care va conţine 2 funcţii A şi B. Funcţia A va gerera un număr aleator, pe care-l va
introduce într-o coadă, iar functia B va extrage un număr din aceasta coadă. (Obs: Aceast principiu este folosit în
comunicarea dintre doua procese, coada fiind de fapt un buffer de date de la procesul A la procesul B).
Lucrarea nr. 12
Tehnica dispersiei

1. Scopul lucrării – îl reprezintă prezentarea principiului tehnicii dispersiei precum şi evidenţierii eficacităţii
utilizării acestei tehnici în procesul de căutare.

2. Aspecte teoretice
2.1. Principiul tehnicii dispersiei
Tehnica dispersiei reprezintă o manieră specifică de organizare a unei mulţimi de chei (elemente cu o cheie specifică)
astfel încât regăsirea unei chei să necesite cât mai puţin efort. Rezolvarea acestei probleme se reduce la găsirea unei
asocieri specifice H a mulţimii cheilor K cu mulţimea adreselor A, adică :
H:K->A
Tehnica dispersiei se aplică structurii de date tip tablou. Ideea pe care se bazează această metodă este următoarea: se
rezervă un volum constant de memorie pentru aşa numitul “tablou dispersat”. Tabloul dispersat se implementează cu
ajutorul unei structuri de date de tip tablou. Notând cu p numărul elementelor din tabloul dispersat, indicele l care
precizează poziţia unui anumit nod poate lua valori între 0 şi p-1. De asemenea, se notează cu K mulţimea tuturor
cheilor şi cu k o cheie oarecare. Numărul cheilor va fi mai mare decât dimensiunea tabloului. În acest caz o funcţie H
care defineşte o aplicaţie a lui k pe mulţimea tuturor indicilor L , astfel :
l=H(k)
unde k este o cheie din mulţimea tuturor cheilor şi l un indice din mulţimea tuturor indicilor.
Funcţia H de asociere se numeşte funcţie de dispersie şi ea permite ca, pornind de la o cheie k dată, să se determine
indicele asociat acesteia, l. Din acest motiv, tehnica dispersiei se mai numeşte şi tehnica transformării cheilor,
întrucât cheile se transformă, prin intermediul funcţiei de dispersie, în indici de tablou.
Evident, funcţia H nu este o funcţie bijectivă deoarece numărul cheilor este mult mai mare decât numărul indicilor.
Practic, există mai multe chei cărora le corespunde acelaşi indice.
Principiul metodei este următorul: pentru introducerea unui nod în structură cu cheia k se determină mai întâi indicele
asociat l=H(k), după care se depune la nodul în tabloul dispersat în poziţia T(l) , unde T este tabloul de dispersie.
Dacă în continuare apare o altă cheie k’ care are acelaşi indice asociat l, adică l=H(k’)=H(k), atunci s-a ajuns la aşa
numita situaţie de coliziune, care se poate rezolva în mai multe moduri, după cum se va ilustra ulterior.
Pentru a regăsi un nod cu o cheie k se procedează similar, adică se calculează indicele asociat cheii k de căutat şi în
tabloul de dispersie la indicele găsit ca asociat cheii k se va căuta cheia. Dacă cheia se va regăsi căutarea se va
termina, dar dacă nodul căutat nu se va regăsi atunci se ajunge într-o situaţie de coliziune, situaţie ce se va rezolva la
fel ca şi la introducerea de noduri.
Aplicarea în practică a tehnicii dispersiei presupune deci rezolvarea a două probleme şi anume: definirea funcţiei de
dispersie H şi tratarea situaţiei de coliziune.

52
2.2. Determinarea funcţiei de dispersie. Tratarea situaţiei de coliziune
Funcţia de dispersie trebuie să repartizeze cât mai uniform mulţimea cheilor pe mulţimea indicilor, deoarece astfel se
minimalizează probabilitatea coliziunilor iar pe de altă parte calculele sunt mult mai simple. De asemenea, funcţia de
dispersie trebuie să fie uşor calculabilă. Una dintre funcţiile de dispersie cel mai des utilizate este următoarea:
H(k) = ord(k) mod p
unde ord(p) reprezintă numărul de ordine (întreg) ataşat cheii k şi care precizează a câta cheie este cheia k în
mulţimea K a cheilor iar p este un număr reprezentând numărul maxim de indici în tabloul dispersat
(0,p-1). În vederea repartizării cât mai uniforme a cheilor pe indici este recomandabil ca p să fie un număr prim (dar
nu obligatoriu).
In cazul cheilor şiruri de caractere se pot utiliza şi următoarele funcţii specifice:


length( k )
H (k )  ( ord ( k [i ]) mod p
i 1


length( k )
H (k )  ( 2 i ord ( k [i ]) mod p
i 1

Tratarea situaţiei de coliziune presupune generarea unei noi poziţii pentru noua cheie de inserat. O metodă în acest
sens este aceea de a înlănţui într-o listă toate nodurile ale căror indici primi sunt identici, metoda numindu-se
înlănţuire directă. Elementele acestei liste pot sau nu să aparţină tabelului iniţial. În caz că nu, memoria necesară se
alocă din aşa numita “zonă de depăşire”. Această metodă, care este deosebit de eficientă, are două dezavantaje:
necesitatea menţinerii unor liste secundare şi prelungirea fiecărui nod cu un spaţiu pentru pointerul necesar
înlănţuirii. In această variantă, algoritmul utilizează un tablou de pointeri ce indică fiecare spre o listă cu nodurile
care sunt asociate aceleiaşi întrări din tabloul dispersat.
Algoritmul este următorul:

#include<stdio.h>
#include<conio.h>
#include<alloc.h>
#include <stdlib.h>

#define N 10
typedef struct NOD
{
int cheie;
struct NOD *urm;
};

int DISPERSIE(int x) //functia de dispersie


{
div_t y;
y=div(x,N);
y.rem;
return y.rem;
}
void TIPLIST (NOD *f) //tiparirei tablou dispersat
{ NOD *b;
b=f;
while (b!=NULL)
{
printf("%d\t",b->cheie);
b=b->urm;
}
printf(" \n ");
}
void main(void)
{
NOD *el; /* el = un element */
NOD a;
int x,i;
char c;
NOD *t[N]; /* tabloul de dispersie*/

53
for(i=0;i<N;i++) t[i]=NULL;
clrscr();
printf(" Introduceti un element
intreg \n");
scanf("%d",&x);
while (x!=0)
{
el=(NOD *)malloc(sizeof(NOD));
el->cheie=x;
i=DISPERSIE(x);
/*pozitia in tablou */
el->urm=t[i];
t[i]=el;
printf("Mai doriti sa
introduceti alte elmente?y/n");
c=getch();
if (c!='y')
{
x=0;
}
else
{
printf(" \n
Introduceti un element intreg \n");
scanf("%d",&x);
};
}
if (c=='n')
{
for (i=0;i<N;i++)
{
printf("\nPe
pozitia %d avem elementele : ",i);
TIPLIST(t[i]);
};
};
}

O altă metodă, numită adresare deschisă, realizează în cazul unei coliziuni parcurgerea după o anumită regulă a
tabloului dispersat, pînă la găsirea primului loc liber, următorul indice fiind:
- adresare deschisă: indicele consecutiv din tablou faţa de cel care a provocat coliziunea, l+1, tabloul
considerîndu-se circular. Algoritmul are tendinţa de a îngrămădi însă nodurile în continuarea celor existente,
datorită metodei de parcurgere folosite
- adresare patratică: indicele l+r (l = indicele care a provocat coliziune; r = o valoare care se iniţializează pe 1 şi
se incrementează cu 2 la fiecare nereuşită). Un dezavantaj minor al acestei metode este acela că se poate întîmpla
să nu se găsească nici un element liber, ajungîndu-se la depăşirea tabloului, cu toate că în tabel mai există
elemente libere. Totuşi, probabilitatea ca acest lucru să se întîmple este foarte mică, şi în general metoda este
foarte performantă

2.3. Problemă rezolvată


Sa se realizeze un program cu ajutorul căruia se va face dispersia unei mulţimi de numere date folosindu-se funcţia de
dispersie :
k = (x mod n) + 1.

#include <conio.h>
#include <stdio.h>

struct coloana_struct
{ int linie[100];
int nr_linii;
};

54
coloana_struct coloana[100];
int f,n,o,x;

void afisare_coloana(int c)
{ printf("\nColoana %d : ",c);
int f;
if (coloana[c].nr_linii==0) printf("Goala");
else
for (f=1;f<=coloana[c].nr_linii;f++)
printf("%d ",coloana[c].linie[f]);
}
void afis_coloana()
{ int c;
do
{printf("Dati coloana pe care doriti s-o afisati [1..%d] = ",n);
scanf("%d",&c);
}
while ((c<1)||(c>n));
afisare_coloana(c);
getch();
}

void adauga_element()
{ printf("Dati elementul ce doriti sa-l introduceti : ");
scanf("%d",&x);
int poz=(x%n)+1;
printf("Elementul %d a fost adaugat la
coloana %d.",x,poz);
coloana[poz].linie[++coloana[poz].nr_linii]=x;
getch();
}

void main()
{ for (f=0;f<100;f++) coloana[f].nr_linii=0;
clrscr();
printf("Dati numarul de coloane de
dispersie : ");
scanf("%d",&n);

do
{ clrscr();
printf("Meniu pt dispersie cu %d
coloane",n);
printf("\n1.Adaugare numar");
printf("\n2.Afisare o anumita coloana");
printf("\n3.Afisare toate coloanele");
printf("\n4.Iesire");
printf("\n\nAlegeti optiunea : ");
scanf("%d",&o);
switch (o)
{case 1:adauga_element();break;
case 2:afis_coloana();break;
case 3:{for (f=1;f<=n;f++)
afisare_coloana(f);
getch();
} break;
}
}
while (o!=4);
}

3. Probleme propuse

55
1. Se cere să se construiască o tabelă de dispersie conţinînd identificatorii dintr-un text (aflat într-un fisier), fiecare
identificator avînd asociat şi contorul de apariţii. Textul este format din propoziţii, separate prin caracterul punct.
Cuvintele sunt la rindul lor separate cu caracterul spaţiu, virgulă sau punct-virgulă.

2. Sa se realizeze un program prin care se va face dispersia unor studenţi pe baza primei litere din nume (adica
studenţii al căror nume incep cu „A” vor fi introduşi incepind de la prima pozitie a tabloului dispersat, cei cu „B”
incepind de la a doua pozitie, ş.a.m.d) . Datele specifice unui student vor fi : nume, grupa, medie. Ca şi interfaţă se va
folosi un meniu cu următoarele opţiuni: adăugare student, ştergere student, căutare student, afişarea tuturor
studenţilor al căror nume care începe cu o anumită literă”, afişarea integrală a tabelei de dispersie.

3. Principalul dezavantaj al tehnicii dispersiei este acela că lungimea tabloului este fixă. Presupunînd că există un
mecanism de alocare dinamică a memoriei prin care, daca tabloul de dispersie este plin, se poate genera un alt tablou
de dispersie, toate cheile din tabloul iniţial copiindu-se in cel de-al doilea, după care zona ocupată de tabloul iniţial se
elibereaza. Aceasta tehnica se numeste tehnica redispersiei
(re-hashing). Să se scrie un program care implementează această tehnică.

56

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