Documente Academic
Documente Profesional
Documente Cultură
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> .
5
Dacă dorim să folosim metoda experimentală, şi să utilizăm funcţia gettime, programul se va modifica astfel:
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.
#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));
//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).
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
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
Î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:
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
#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);
11
//lansam cautarea binara
cautare_binara();
getch();
}
3. Probleme propuse
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).
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;
}
}
}
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;
}
}
}
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).
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);
}
//sortam betisoarele
sort_insertie();
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");
}
}
#define TRUE 1
#define FALSE 0
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.
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.
#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];
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]);
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.
#define FALSE 0
#define TRUE 0
#define n 10
int x;
FILE *a, *b, *c;
int p,k,i;
int temp;
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);
}
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
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.
#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);
}
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ă.
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.
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.
#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.
#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.
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:
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.
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
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).
#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 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 :
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;
};
33
inceput
cheie 1 2 3 Ν
urm … Νull
info … … … …
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
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
34
q 25 q->urm=p->urm
p->urm=q
10 20 30 …
…
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 …
…
q =(nod*)malloc(sizeof(nod));
*q=*p;
p->urm=q;
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.
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.
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;
}
}
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;
}
}
}
#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ă.
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).
… …
… …
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.
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
#include <stdio.h>
#define TRUE 1
#define FALSE 0
#define lungime_maxima 10
typedef struct{
int virf;
int elemente[lungime_maxima];
}stiva;
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
Î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;
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);
}
#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;
};
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ă
#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