SDMP Lectia dan 4 5

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

3. Red

1980

450.00

4. Cyan

1975

800.00

5. Black

1981

500.00


7. Green

1987

350.00

8. Gray

1968

900.00

9. White

1980

600.00

10. Yellow


1988

300.00

1983

600.00

1984

550.00

6.

11.
12.
13. Magenta
14.
15. Orange


End of Table. Press any key ...
m=10, n=15, NCOMP=16, ALS=1.60, m/n=0.67, D(m/n)=2.00

Analiza rezultatelor rămâne ca exerciţiu.
Exerciţii.
1. Supraîncărcaţi operatorul de inserţie în clasa hashing_table.
2. Supraîncărcaţi operatorul de extragere în clasa hashing_table.
3. Găsiţi o funcţie de repartizare mai potrivită.

Tabele de repartizare cu înlănţuirea externă (repartizarea deschisă, înlănţuirea separată)
În metoda examinării liniare înregistrările ce produc coliziuni se includ în poziţiile libere aceluiaşi
vector de reflectare. Însă pentru aceste înregistrări se poate crea un tabel aparte. În tabelul adăugător
înregistrările se poate lega în lanţ, ca în liste, pentru uşurarea căutării.

41

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

00│


”Green”
”Red”
”Blue”
”Gray”
”Orange”
”White”
”Cyan”

”Yellow”
”Magenta”
”Black”

Primary
Table



Secondary
Table

00|Gray,

1968,900│
1975,800│

01│Blue,

1981,500│02

01│Cyan,

02│Red,

1980,450│01

02│Black, 1981,500|

03│




03│



04│



04│



05│



05│




06│Green, 1987,350│00

06│



07│White, 1980,600│

07│



08│



08│




09│Yellow,1988,300│

09│



10│



10│



11│



11│




12│Magenta,1983,600│

12│



13│



13│



14│Orange,1984,550│

14│




În tabele de repartizare cu înlănţuirea externă lungimea medie de căutare pentru distribuirea
uniformă şi aleatorie a înregistrărilor se defineşte după formula:

Dm, n  1 

m 1
, n – lungimea vectorului de reflectare, m – lungimea tabelului.
2n

Pentru a demonstra lucru cu tabele de repartizare cu înlănţuirea externă, în primul rând să declarăm
clasa hashing_linked_elem ca clasa derivată de la clasa usual_elem şi dotată cu câmpul next,
pentru a crea lanţuri de legături.
//
//

C l a s s


"h a s h i n g _ l i n k e d _ e l e m"

//
class hashing_linked_elem: public usual_elem
{
protected:
int next;
public:
hashing_linked_elem()
{
next = -1;
}

42

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

hashing_linked_elem(char* init_name, int init_year, double init_salary):
usual_elem(init_name, init_year, init_salary)
{
next = -1;
}
virtual void hashing_linked_show(const char* opening=NULL,
const char* ending=NULL)
{
if(!opening)
opening="";
if(!ending)
ending="\n";
printf("%s", opening);
usual_elem::show("", "");
if(!free())
printf(" [%4d]", next);
printf("%s", ending);
}
int hf(int n) // hashing function
{
return (name[0]-'A')%n;
}
int get_next()
{
return next;
}
int set_next(int new_next)
{
return next=new_next;
}
};

Exerciţii.
1. Supraîncărcaţi operatorul de inserţie în clasa hashing_linked_elem.
2. Supraîncărcaţi operatorul de extragere în clasa hashing_linked_elem.

Apoi, pe baza clasei abstracte SD, să declarăm clasa generică extern_hashing_table, care na va
da posibilitatea de a crea tabele de repartizare cu înlănţuirea externă.
43

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

//
//

C l a s s

"e x t e r n _ h a s h i n g _ t a b l e"

//

m/n

template class extern_hashing_table: public SD
{
protected:
int n;
int m;
el *t;
el *v;
public:
extern_hashing_table(char* file_name, int init_n=0): SD(file_name)
{
n=init_n;
if(n0)
{
int i=tmp.hf(n);
if(t[i].free())
{
t[i]=tmp;
m++;
}
else
{
repeated=-1;
if( tmp==t[i] )
{
repeated=i;
t[i].show("", " !!!\n");
}
else
{
if(t[i].get_next()==-1)

44

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

{
int j=0;
while(!v[j].free())
j++;
t[i].set_next(j);
v[j]=tmp;
m++;
}
else
{
i=t[i].get_next();
position=-1;
while((repeated==-1) && position==-1)
{
if( tmp==v[i] )
{
repeated=i;
v[i].show("", " !!!\n");
}
else
if(v[i].get_next()==-1)
{
position=i+1;
while(!v[position].free())
position++;
v[i].set_next(position);
v[position]=tmp;
m++;
}
else
i=v[i].get_next();
}
}
}
if ( repeated!=-1 )
{
char message[60];
char repeated_str[10];
message[0]='\0';
//strcat(message, "Key coincides with the key in the position: ");
//strcat(message, itoa(repeated+1, repeated_str, 10));
//strcat(message, "!\n");

45

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

strcat(message, "Key coincides !!!\n");
error(message);
}
}
}
fclose(SD::pf), SD::pf=NULL;
}
virtual void show(const char* opening=NULL, const char* ending=NULL,
int nlinepp=20)
{
clrscr();
if(!opening)
opening="";
if(!ending)
ending="\n";
printf("%s", opening);
for(int i=0; i0 && i%nlinepp==0)
{
printf("Press any key to continue ...\n");
getch();
clrscr();
printf("%s", opening);
}
printf("%4d. ", (i+1)); t[i].show();
}
printf("%s", ending);
printf("End of Table. Press any key ...\n");
getch();
}
virtual void primary_show(const char* opening=NULL, const char* ending=NULL,
int nlinepp=20)
{
clrscr();
if(!opening)
opening="";
if(!ending)
ending="\n";
printf("%s", opening);

46

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

for(int i=0; i0 && i% nlinepp==0)
{
printf("%s", "Press any key to continue ...\n");
getch();
clrscr();
printf("%s", opening);
}
printf("%4d. ", (i+1)); t[i].hashing_linked_show();
}
printf("%s", ending);
printf("End of Table. Press any key ...\n");
getch();
}
virtual void secondary_show(const char* opening=NULL,
const char* ending=NULL, int nlinepp=20)
{
//clrscr();
if(!opening)
opening="";
if(!ending)
ending="\n";
printf("%s", opening);
for(int i=0; i0 && i%nlinepp==0)
{
printf("%s", "Press any key to continue ...\n");
getch();
clrscr();
printf("%s", opening);
}
printf("%4d. ", (i+1)); v[i].hashing_linked_show();
}
printf("%s", ending);
printf("End of Table. Press any key ...\n");
getch();
}
int search(el e)

47

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

{
int position=-1;
int i=e.hf(n);
if(!t[i].free())
if(SD::ncomp++, e==t[i])
position=i;
else
if((i=t[i].get_next())!=-1)
do
{
if(SD::ncomp++, e==v[i])
position=i;
else
i=v[i].get_next();
}
while((position==-1) && (i!=-1));
return position;
}
int get_n()
{
return n;
}
int get_m()
{
return m;
}
protected:
int countn()
{
return 200;
}
};

În funcţia main() să creăm pe baza fişierului stud.txt un tabel de repartizare cu înlănţuirea
externă pentru n=15 şi să demonstrăm căutarea elementelor.
void main()
{
clrscr();
extern_hashing_table ex_hashing_gr("stud.txt", 15);

48

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

ex_hashing_gr.primary_show("Primary table:\n","");
ex_hashing_gr.secondary_show("Secondary table:\n","");
char ch='n';
char surname[21];
while(ch!='y')
{
printf("Enter a name to search: ");
scanf("%s", surname);
hashing_linked_elem e(surname, 2000, 0.0);
ex_hashing_gr.reset_ncomp();
int pos=ex_hashing_gr.search(e);
if(pos0)
{

51

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

ex_hashing_gr.reset_ncomp();
if(ex_hashing_gr.search(sample)>=0)
NCOMP+=ex_hashing_gr.get_ncomp();
}
fclose(pf);
printf("m=%d, n=%d, NCOMP=%d, ALS=%.2lf", ex_hashing_gr.get_m(),
ex_hashing_gr.get_n(), NCOMP, (double)NCOMP/ex_hashing_gr.get_m());
printf(", D(m/n)=%.2lf\n",
1.+(ex_hashing_gr.get_m()-1.)/(2.*ex_hashing_gr.get_n()));
getch();
}

Pentru n=15 rezultatul va fi:
Primary table:
1.
2. Blue

1981

500.00 [

2]

3. Red

1980

450.00 [

1]

7. Green

1987

350.00 [

0]

8. White

1980

600.00 [

-1]

1988

300.00 [

-1]

1983

600.00 [

-1]

1984

550.00 [

-1]

4.
5.
6.

9.
10. Yellow
11.
12.
13. Magenta
14.
15. Orange

End of Table. Press any key ...
Secondary table:
1. Gray

1968

900.00 [

-1]

2. Cyan

1975

800.00 [

-1]

3. Black

1981

500.00 [

-1]

4.
5.
6.
7.
8.
9.
10.
11.

52

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

12.
13.
14.
15.
End of Table. Press any key ...
m=10, n=15, NCOMP=13, ALS=1.30, D(m/n)=1.30

Iar pentru n=10 rezultatul va fi:
Primary table:
1.
2. Blue

1981

500.00 [

4]

3. White

1980

600.00 [

1]

1984

550.00 [

2]

7. Green

1987

350.00 [

0]

8. Red

1980

450.00 [

-1]

4.
5. Orange
6.

9.
10.
End of Table. Press any key ...
Secondary table:
1. Gray

1968

900.00 [

-1]

2. Cyan

1975

800.00 [

3]

3. Yellow

1988

300.00 [

-1]

4. Magenta

1983

600.00 [

-1]

5. Black

1981

500.00 [

-1]

6.
7.
8.
9.
10.
End of Table. Press any key ...
m=10, n=10, NCOMP=16, ALS=1.60, D(m/n)=1.45

Exerciţii.
1. Supraîncărcaţi operatorul de inserţie în clasa extern_hashing_table.
2. Supraîncărcaţi operatorul de extragere în clasa extern_hashing_table.

53

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

Tabele de repartizare cu înlănţuirea internă
Încărcarea poziţiilor vectorului de reflectare constă din două etape:


prima etapă se aseamănă cu repartizarea cu înlănţuirea externă;



a doua etapă se îndeplineşte după terminarea creării tabelului primar şi celui secundar. Ea constă
în mutarea lanţurilor din tabelul secundarr în poziţiile libere ale tabelului primar.

Astfel tabelele din exemplul precedent vor fi transformate în următorul tabel:
00│Black, 1981,500│

”Green”
”Red”
”Blue”
”Gray”
”Orange”
”White”
”Cyan”

”Yellow”
”Magenta”
”Black”

01│Blue,

1981,500│00

02│Red,

1980,450│03

03│Cyan,

1975,800│

04│Gray,

1968,900│

05│



06│Green, 1987,350│04
07│White, 1980,600│
08│



09│Yellow,1988,300│
10│



11│



12│Magenta,1983,600│
13│



14│Orange,1984,550│

Prioritatea acestei metode în comparaţie cu precedenta – economie de memorie, dar neajunsul –
flexibilitatea mică şi algoritmul de încărcare a tabelului este mai complicat.
Lungimea medie de căutare aici se defineşte după aceiaşi formula: D(m, n)  1 

m 1
. Această
2n

metodă se foloseşte pentru tabele permanente, şi pentru tabele temporare, care se încărcă la prima
etapă, dar se folosesc la alta.
Pentru tabele constante înregistrările cel mai des folosite se înscriu primele, atunci accesările
lanţurilor interioare sunt rare, ce micşorează lungimea medie de căutare. Este caracter, că la
folosirea lanţurilor interioare toate poziţiile ale vectorului de reflectare pot fi încărcate (adică m=n),
dar lungimea medie de căutare pentru repartizarea uniformă şi aleatorie a înregistrărilor nu întrece
1.5.

54

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

Pentru a demonstra lucru cu tabele de repartizare cu înlănţuirea internă să declarăm pe baza clasei
abstracte SD clasa generică extern_hashing_table, care na va da posibilitatea de a crea tabele de
repartizare cu înlănţuirea internă.
//
//

C l a s s

"i n t e r n _ h a s h i n g _ t a b l e"

//

m/n

template class intern_hashing_table: public SD
{
protected:
int n;
int m;
el *t;
el *v;
public:
intern_hashing_table(char* file_name, int init_n=0): SD(file_name)
{
n=init_n;
if(n0)
{
i=tmp.hf(n);
if(t[i].free())
{
t[i]=tmp;
m++;
}
else
{
repeated=-1;
if( tmp==t[i] )
{
repeated=i;
t[i].show("", " !!!\n");

55

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

}
else
{
if(t[i].get_next()==-1)
{
j=0;
while(!v[j].free())
j++;
t[i].set_next(j);
v[j]=tmp;
m++;
}
else
{
i=t[i].get_next();
position=-1;
while((repeated==-1) && position==-1)
{
if( tmp==v[i] )
{
repeated=i;
v[i].show("", " !!!\n");
}
else
if(v[i].get_next()==-1)
{
position=i+1;
while(!v[position].free())
position++;
v[i].set_next(position);
v[position]=tmp;
m++;
}
else
i=v[i].get_next();
}
}
}
if ( repeated!=-1 )
{
char message[60];
char repeated_str[10];

56

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

message[0]='\0';
//strcat(message, "Key coincides with the key in the position: ");
//strcat(message, itoa(repeated+1, repeated_str, 10));
//strcat(message, "!\n");
strcat(message, "Key coincides !!!\n");
error(message);
}
}
}
fclose(SD::pf), SD::pf=NULL;
el empty;
int k=0;
for(j=0; j=0)
NCOMP+=in_hashing_gr.get_ncomp();
}
fclose(pf);
printf("m=%d, n=%d, NCOMP=%d, ALS=%.2lf", in_hashing_gr.get_m(),
in_hashing_gr.get_n(), NCOMP, (double)NCOMP/in_hashing_gr.get_m());
printf(", D(m/n)=%.2lf\n",
1.+(in_hashing_gr.get_m()-1.)/(2.*in_hashing_gr.get_n()));
getch();

}

Pentru n=15 afişarea va fi:
Primary table:
1. Gray

1968

900.00 [

-1]

2. Blue

1981

500.00 [

4]

3. Red

1980

450.00 [

3]

4. Cyan

1975

800.00 [

-1]

5. Black

1981

500.00 [

-1]

7. Green

1987

350.00 [

0]

8. White

1980

600.00 [

-1]

1988

300.00 [

-1]

1983

600.00 [

-1]

1984

550.00 [

-1]

6.

9.
10. Yellow
11.
12.
13. Magenta
14.
15. Orange

62

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

End of Table. Press any key ...
m=10, n=15, NCOMP=13, ALS=1.30, D(m/n)=1.30

Pentru n=10 afişarea va fi:
Primary table:
1. Gray

1968

900.00 [

-1]

2. Blue

1981

500.00 [

9]

3. White

1980

600.00 [

3]

4. Cyan

1975

800.00 [

5]

5. Orange

1984

550.00 [

8]

6. Magenta

1983

600.00 [

-1]

7. Green

1987

350.00 [

0]

8. Red

1980

450.00 [

-1]

9. Yellow

1988

300.00 [

-1]

1981

500.00 [

-1]

10. Black

End of Table. Press any key ...
m=10, n=10, NCOMP=16, ALS=1.60, D(m/n)=1.45

Tabelele de repartizare aleatorie se încarcă destul de simplu, nu necesit ordonarea înregistrărilor şi
asigură o căutare rapidă. Deaceea aceste tabele deseori se folosesc în practică.
Exerciţii.
1. Supraîncărcaţi operatorul de inserţie în clasa intern_hashing_table.
2. Supraîncărcaţi operatorul de extragere în clasa intern_hashing_table.

Funcţii de repartizare
Timpul calculării funcţiei de repartizare f(k) intră în timpul mediu de căutare, deaceiea trebuie de
ţinut cont la alegerea algoritmului, realizând funcţia de repartizare.
O funcţie bună de repartizare trebuie să asigure repartizarea uniformă a înregistrărilor pe poziţiile
vectorului de reflectare, fiindcă distribuirea neuniformă măreşte timpul mediu de căutare. Însă dacă
calcularea valorii funcţiei de repartizare necesită îndeplinirea unui număr mare de operaţii, aceasta
poate distruge toată economia în timpul căutării. Deci, algoritmul calculării funcţiei de repartizare
nu trebuie să fie complicat. Să privim câteva metode de calculare a funcţiei de repartizare:
1. Una din metodele simple se bazează pe evidenţierea unei părţi din codul numeric al cheii. De
exemplu, fie dimensiunea maximă aşteptată a tabelului de nume simbolice nu întrece 256.
Atunci funcţia de repartizare poate avea în calitate de valoare 8 biţi, fiindcă 256=28. Se poate
pur şi simplu de a evidenţia primii 8 biţi din codul binar al identificatorului sau de a lua careva 8
biţi din mijlocul codului. Trebuie doar să asigurăm cu cât este posibil o distribuire uniformă a
înregistrărilor prin funcţia f(k) în intervalul [0, 255].

63

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

2. Pentru asigurarea distribuirii uniforme se foloseşte “sumarea” codului identificatorului: prima
jumătate a codului se sumează cu a doua şi din rezultat se evidenţiază 8 biţi. Se poate de
asemenea de împărţit codul cheii în bucăţi câte 8 biţi, de sumat bucăţile şi de pus suma pe
modulul 28. Ultima modificare are careva probabilitate teoretică: la presupunerea a statisticei
independente de sumare a bucăţilor se primeşte repartizarea aproape de uniformă.
3. O altă metodă de calculare a funcţiei de repartizare este împărţirea. Pentru vectorul de reflectare
de lungimea n, cheia se priveşte ca un număr întreg şi se împarte la mărimea n. Experimentele
arată, că restul de la împărţire este repartizat aproape uniform în intervalul [0, n-1] şi poate fi
folosit ca valoarea funcţiei de repartizare.
Verificarea experimentală a metodelor descrise pentru tabelele de repartizare cu examinarea lineară
a arătat că evidenţierea simplă a bucăţii din codul identificatorului măreşte lungimea medie de
căutare în comparaţie cu cea teoretică, definită după formula: D( ) 

2 
, în 4-5 sau şi mai
2  2

multe ori. Lungimea medie de căutare pentru metoda de sumare a bucăţilor după modulul 2k
aproape de două ori mai mare ca teoretică, dar pentru împărţirea, lungimea medie de căutare practic
coincide cu teoretică pentru σ≤8.85.

64

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

3. TEHNICI DE SORTARE
3.1.

Noţiuni generale

Sortarea este operaţia de aranjare a elementelor unui vector după valoarea
cheilor, pentru care este definită relaţia de ordine.
Tradiţional diferă sortarea internă de sortarea externă. Sortarea internă prelucrează datele păstrate în
memoria operativă a calculatorului, dar sortarea externă, operează cu datele care sunt păstrate în
fişiere.
În cazul sortării interne se tinde la minimizarea numărului de comparaţii şi permutări ale
elementelor.
În cazul sortării externe factorul hotărâtor este numărul de operaţii de intrare şi ieşire. În acest caz
numărul de comparaţii trece pe planul doi, totuşi şi el se i-a în consideraţie.
Cazul sortării interne
Presupunem, că datele supuse sortării se păstrează în memoria operativă într-un vector t. Fiecare
element t[i] al acetui vector este obiect al clasei parametrizate el în care sunt supraîncărcaţi
operatorii de comparaţie. Deci, sunt admise expresii:
t[i]0)
n++;
fclose(SD::pf), SD::pf=NULL;
}
virtual void show(const char* opening, const char* ending, int nlinepp=20)
{
clrscr();
if(!opening)
opening="";
printf("%s", opening);
if(!ending)
ending="\n";
for(int i=0; i0 && i%nlinepp==0)
{
printf("Press any key to continue...\n");
getch();
clrscr();
printf("%s", opening);
}
printf("%4d. ", (i+1));
t[i].show("", "\n");

67

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

}
printf("%s", ending);
printf("End of vector. Press any key ...\n");
getch();
}
int search(el e)
{
int position = -1;
for(int i=0; (position==-1) && it[i])
position=i;
return position;
}
int get_n()
{
return n;
}
long get_ncomp()
{
return SD::ncomp;
}
void reset_ncomp()
{
SD::ncomp=0;
}
protected:
void swap(int i, int j)
{
el tempor=t[i];
t[i]=t[j];
t[j]=tempor;
}
};

Exerciţii.

68

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

1. Supraîncărcaţi operatorul de inserţie în clasa vector.
2. Supraîncărcaţi operatorul de extragere în clasa vector.

3.3.

Sortarea prin interschimbare

Metoda de sortare prin interschimbare (engl. Exchange sort) constă în parcurgerea elementelor ale
vectorului, în aşa mod ca fiecare parcurgere micşorează numărul de inversii, până atunci când nu
rămâne nici o inversie. Problema constă în căutarea următoarei inversiei (i, j).
Schematic:
while (este_inversie (i, j))
swap(i, j);

Sortarea prin interschimbare constă în modificări succesive de tip t[i]t[j], până când
elementele vectorului nu vor deveni în ordine crescătoare.
Din această categorie fac parte metoda bulelor (bubblesort) – unul din cei mai slabi algoritmi de
sortare şi sortarea rapidă (quicksort) – unul din cei mai buni algoritmi de sortare.
Sortarea prin metoda bulelor
Metoda bulelor constă în compararea t[i] cu t[i+1], dacă ordinea este bună se compară t[i+1]
cu t[i+2], dacă ordinea nu este bună se interschimbă t[i] cu t[i+1] şi apoi se compară t[i+1]
cu t[i+2]. După prima parcurgere a vectorului, pe ultima poziţie ajunge elementul având valoarea
cea mai mare, după a doua parcurgere ajunge următorul element pe penultima poziţie, etc.
Algoritmul are complexitatea O(n2).
void bubble_sort()
{
BOOLEAN inversion;
do
{
inversion = FALSE;
for(int i=0; it[i+1])
{
swap(i,i+1);
inversion = TRUE;
}
}
while (inversion);
}

Complexitatea minimă este O(n). Dacă vectorul iniţial este deja sortat, variabila inversion
niciodată nu va primi valoarea TRUE.

69

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

Complexitatea maximă este O((n-1)2)=O(n2). Dacă elementul minimal are indicele iniţial n-1,
atunci va fi nevoie de n-1 executări a ciclului exterior, ca să-i dăm indicele 0.
Pentru fiecare executare a ciclului exterior cu n-1 comparaţii: (n-1)*(n-1)=(n-1)2 comparaţii.
Complexitatea medie de asemenea este egală cu O(n2), dacă elementul minimal căutat se află
aleator printre indicii 0, 1,…, n-1.
Exerciţiu: Este posibilă îmbunătăţirea acestui algoritm, ce nu schimbă totuşi esenţial complexitatea.
Îmbunătăţiţi algoritmul de mai sus, prescurtând cu un element parcurgerea de rând faţă de
precedentă.
Metoda bulelor este unul din cei mai răi algoritmi de sortare. Neajunsul constă în aceea că la fiecare
pas elementul următor se compară numai cu vecinul său următor.
Sortarea rapidă

3.4.

Sortarea rapidă (quicksort) a fost propusă de C.A.R. Hoare şi foloseşte principiile “Divide Et
Impera” şi “Echilibru”.
Ideea metodei este următoarea: se selectează un element arbitrar din vector numit principal (sau
pivot) şi se rearanjează vectorul în doi subvectori, astfel încât cel din stânga are toate elementele
mai mici sau egale decât pivotul, iar cel din dreapta mai mari sau egale ca pivotul. Procedeul se reia
în subvectorul din stânga şi apoi în cel din dreapta, etc. Procedeul se termină când se ajunge la
subvectori dintr-un singur element.
În baza clasei generice vector declarăm clasa derivată vector_quicksort, dotată cu algoritmul de
sortare rapidă.
//
//

C l a s s

" v e c t o r

q u i c k s o r t"

//
template class vector_quicksort: public vector
{
public:
vector_quicksort(char* file_name, int NMAX=200):
vector(file_name, NMAX)
{
}
void quicksort(int i=0, int j=-1)
{
if(j>=n || j==-1)
j=n-1;
if(ij)
i=0;
quicksort_intern(i, j);
}

70

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

protected:
void quicksort_intern(int i, int j);
int divide(int i, int j);
};

Presupunem că există funcţia numită divide(), care într-un anumit fel alege elementul principal cu
cheia K şi rearanjează vectorul astfel, ca elementul principal primeşte un indice imain, iar toate
elementele cu cheile ≤ K se aranjează de la stânga (adică au indicii < imain), dar toate elementele cu
cheile ≥ K se aranjează de la dreapta (adică au indicii >imain):
imain+1

imain-1
imain

0

n-1

elemente ≥ t[imain]

elemente ≤ t[imain]

elementul principal t[imain]
Avem aici un caz tipic recursiv:


parametrizarea: se precaută pentru subvectorul t[i]÷t[j]; pentru vectorul iniţial i=0, j=n-1;



cazul trivial: i=j (nu avem ce sorta);



trecerea de la cazul general la un caz mai simplu, care are loc datorită funcţiei divide().

Dacă există o astfel de funcţie, atunci sortarea rapidă imediat se obţine în formă recursivă:
template
void vector_quicksort::quicksort_intern(int i, int j)
{
if (j>i)
{
int imain=divide(i,j);
quicksort_intern(i,imain-1);
quicksort_intern(imain+1,j);
}
}

Algoritmul are loc pentru ambele ordine de apeluri recursive.
Schema metodei de divizare în timpul O(n):


4

20 2

elementul

principal
10
15 3


12

Prelucrăm vectorul din stânga şi din dreapta până atunci, până când din stânga nu va fi găsit
elementul cu cheia, ce întrece cheia elementului principal, dar din dreapta – elementul cu cheia mai
mică ca cheia elementului principal. După aceasta se poate de schimbat cu locurile aceste două
elemente, lichidând prin asta inversia. Apoi astfel de prelucrare dublă, din stânga şi din dreapta,
71

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

continuă cu poziţiile deja găsite. Vectorul se socoate împărţit, când poziţiile din stânga şi din
dreapta se întâlnesc. Valoarea comună a lor notăm prin imain.
Evident, că complexitatea divizării nu întrece O(n), sau mai bine spus O(j-i) când divizarea se
aplică la subvectorul t[i]÷t[j].

Sfaturi practice la alegerea elementului principal
Alegerea elementului principal trebuie să fie în aşa fel ca să se micşoreze probabilitatea cazului
când după divizarea subvectorii (segmentele) să difere mult după lungime.
Prima strategie: la fiecare divizare alegerea aleatorie (folosind funcţia-generator de numere
aleatoare) a valorii indicelui elementului principal dintre i, i+1, …, j. Neajunsul acestei metode cheltuieli suplimentare de timp necesare pentru această operaţie.
A două strategie: în calitate de elementul principal se alege elementul cu valoarea medie dintr-un
set nu mare de elemente. Cel mai simplu şi mai uşor de examinat setul ce conţine trei elemente cu
indicii respectiv i, j şi (i+j)/2.
Ambele metode micşorează probabilitatea cazului catastrofal O(n2), doar totuşi aşa situaţie nu este
exclusă. Sortarea rapidă întotdeauna poate să se degenereze. Paradoxal, că sortarea rapidă este unul
din cei mai buni algoritmi de sortare internă, dar suntem nevoiţi să ne refuzăm de ea în probleme
unde limitele superioare de timp (de tip knlog2n) necesare pentru sortarea, sunt critice.

Algoritmul divizării
Există mai multe variante ale algoritmului de divizare. Toate din ele urmăresc cel puţin două
scopuri:


a accelera ciclurile interioare;



a prevedea caracterul “aleator” a vectorului. Adică de a exclude introducerea întâmplătoare a
ordinei în segmentele de divizare din punct de vedere al productivităţii generale a algoritmului.
Adică trebuie să ne refuzăm de orice încercare de a sorta în procesul de divizare.

72

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

Sedgewick R. E. a propus următoarea metodă de divizare:
a) punem elementul principal în poziţia i (îl schimbăm dacă este necesar cu elementul t[i]).
i

i+1

j

b) divizăm subvectorul t[i+1], t[i+2],… t[j], cu ajutorul valoarei elementului principal t[i]
lăsând pe t[i] la locul său. Se primeşte divizarea cu poziţia intermediară imain, de exemplu:
imain-1
i

imain+1
j

imain

elemente ≥ t[i]

elemente ≤ t[i]

c) schimbăm cu locurile elementul t[i] cu elementul t[imain] şi dăm valoarea imain ca
rezultatul întors de către funcţia divide.
imain-1
i

imain+1
j

imain

elemente ≥ t[imain]

elemente ≤ t[imain] elementul
principal

template int vector_quicksort::divide(int i, int j)
{
int imain, jmain, imed;
imed =(i+j)/2;
imain = (SD::ncomp++, t[i] < t[imed]) ?
((SD::ncomp++, t[imed] < t[j]) ?
imed
:
(SD::ncomp++, t[i] < t[j]) ? j : i)
:
((SD::ncomp++, t[imed] > t[j]) ?
imed
:
(SD::ncomp++, t[i] > t[j]) ? j : i);

73

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

if(imain > i)
swap(i, imain);
imain = i+1, jmain = j;
while(imain < jmain)
{
while((imain < jmain)&&(SD::ncomp++, t[imain] imain)&&(SD::ncomp++, t[jmain] >= t[i]))
jmain--;
if(imain < jmain)
swap(imain, jmain);
}
if(SD::ncomp++, t[imain] > t[i])
imain--;
if(imain > i)
swap(i, imain);
return imain;
}

Este clar că funcţia divide() are complexitatea O(n). Ciclul exterior
while(imain < jmain)
{
}

verifică fiecare element al vectorului t[0], t[i],… t[n-1] cel mai mult de două ori, dar restul
operaţiilor cere un timp fix.
În funcţia main() creăm un vector si-il sortatăm prin metoda quicksort:
void main()
{
clrscr();
vector_quicksort gr("Stud.txt");
gr.show("Unsorted group:\n","");
gr.quicksort();
gr.show("Group sorted by name:\n","");
printf("n=%d, ncomp=%d, n*log2(n)=%.2lf, n*n=%.0lf\n",
gr.get_n(),gr.get_ncomp(),
gr.get_n()*log((double)gr.get_n())/log(2.0),
(double)gr.get_n()*gr.get_n());
getch();
}

Afişarea va arăta astfel:
Unsorted group:

74

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

1. Green

1987

350.00

2. Red

1980

450.00

3. Blue

1981

500.00

4. Gray

1968

900.00

5. Orange

1984

550.00

6. White

1980

600.00

7. Cyan

1975

800.00

8. Yellow

1988

300.00

9. Magenta

1983

600.00

1981

500.00

10. Black

End of vector. Press any key ...
Group sorted by name:
1. Black

1981

500.00

2. Blue

1981

500.00

3. Cyan

1975

800.00

4. Gray

1968

900.00

5. Green

1987

350.00

6. Magenta

1983

600.00

7. Orange

1984

550.00

8. Red

1980

450.00

9. White

1980

600.00

10. Yellow

1988

300.00

End of vector. Press any key ...
n=10, ncomp=39, n*log2(n)=33.22, n*n=100

Analiza rezultatelor rămâne ca exerciţiu.
Dacă vrem să sortăm după anul de naştere, atunci declarăm în baza clasei usual_elem clasa
year_elem la care suprascriem funcţia cmp().
//
//

C l a s s

"y e a r _ e l e m"

//
class year_elem : public usual_elem
{
public:
year_elem()
{
}
year_elem(char* init_name, int init_year, double init_salary):
usual_elem(init_name, init_year, init_salary)
{
}

75

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

virtual int cmp(elem& e2)
{
int result;
if(this->year < ((year_elem&)e2).year)
result=-1;
else
if(this->year > ((year_elem&)e2).year)
result=1;
else
result=0;
return result;
}
};

În funcţia main() instanţiem clasa generică vector_quicksort cu clasa-argument year_elem.
void main()
{
clrscr();
vector_quicksort gr("Stud.txt");
gr.show("Unsorted group:\n","");
gr.quicksort();
gr.show("Group sorted by year:\n","");
printf("n=%d, ncomp=%d, n*log2(n)=%.2lf, n*n=%.0lf\n",
gr.get_n(),gr.get_ncomp(),
gr.get_n()*log((double)gr.get_n())/log(2.0),
(double)gr.get_n()*gr.get_n());
getch();
}

Afişarea va arăta astfel:
Unsorted group:
1. Green

1987

350.00

2. Red

1980

450.00

3. Blue

1981

500.00

4. Gray

1968

900.00

5. Orange

1984

550.00

6. White

1980

600.00

7. Cyan

1975

800.00

8. Yellow

1988

300.00

9. Magenta

1983

600.00

1981

500.00

10. Black

End of vector. Press any key ...
Group sorted by year:

76

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

1. Gray

1968

900.00

2. Cyan

1975

800.00

3. Red

1980

450.00

4. White

1980

600.00

5. Black

1981

500.00

6. Blue

1981

500.00

7. Magenta

1983

600.00

8. Orange

1984

550.00

9. Green

1987

350.00

10. Yellow

1988

300.00

End of vector. Press any key ...
n=10, ncomp=42, n*log2(n)=33.22, n*n=100

Analiza rezultatelor rămâne ca exerciţiu.
Exerciţii.
1. Supraîncărcaţi operatorul de inserţie în clasa year_elem.
2. Supraîncărcaţi operatorul de extragere în clasa year_elem.

În sfârşit declarăm în baza clasei usual_elem clasa derivată salary_elem care compară obiecte
după salariu.
//
//

C l a s s

"s a l a r y _ e l e m"

//
class salary_elem : public usual_elem
{
public:
salary_elem()
{
}
salary_elem(char* init_name, int init_year, double init_salary):
usual_elem(init_name, init_year, init_salary)
{
}
virtual int cmp(elem& e2)
{
int result;
if(this->salary < ((salary_elem&)e2).salary)
result=-1;
else

77

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

if(this->salary > ((salary_elem&)e2).salary)
result=1;
else
result=0;
return result;
}
};

În funcţia main() instanţiem clasa generică vector_quicksort cu clasa-argument salary_elem.
void main()
{
clrscr();
vector_quicksort gr("Stud.txt");
gr.show("Unsorted group:\n","");
gr.quicksort();
gr.show("Group sorted by salary:\n","");
printf("n=%d, ncomp=%d, n*log2(n)=%.2lf, n*n=%.0lf\n",
gr.get_n(),gr.get_ncomp(),
gr.get_n()*log((double)gr.get_n())/log(2.0),
(double)gr.get_n()*gr.get_n());
getch();
}

Afişarea de data aceasta va arăta astfel:
Unsorted group:
1. Green

1987

350.00

2. Red

1980

450.00

3. Blue

1981

500.00

4. Gray

1968

900.00

5. Orange

1984

550.00

6. White

1980

600.00

7. Cyan

1975

800.00

8. Yellow

1988

300.00

9. Magenta

1983

600.00

1981

500.00

10. Black

End of vector. Press any key ...
Group sorted by salary:
1. Yellow

1988

300.00

2. Green

1987

350.00

3. Red

1980

450.00

4. Blue

1981

500.00

5. Black

1981

500.00

6. Orange

1984

550.00

78

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

7. White

1980

600.00

8. Magenta

1983

600.00

9. Cyan

1975

800.00

10. Gray

1968

900.00

End of vector. Press any key ...
n=10, ncomp=43, n*log2(n)=33.22, n*n=100

Analiza rezultatelor rămâne ca exerciţiu.
Exerciţii.
1. Supraîncărcaţi operatorul de inserţie în clasa salary_elem.
2. Supraîncărcaţi operatorul de extragere în clasa salary_elem.

Construirea sortării rapide efective
Apelurile recursive neterminate vor fi înscrise în stivă. În cel mai nefavorabil caz divizarea
secvenţială poate da sistematic imain=0 (imain=i) sau imain=n-1 (imain=j). În acest caz
divizarea vectorului în doi sub vectori va da permanent unul de lungime 0 iar altul va avea lungimea
n-1 (j-i). Adâncimea în apelurile recursive poate atinge valoarea n – lungimea vectorului iniţial.
Trebuie să prevedem stiva de adâncime n (complexitatea spaţială O(n), ce nu este accesibil).
Există o metodă simplă: de a începe întotdeauna cu subvectorul de lungime mai mică. Atunci
această lungime va fi mai mică decât jumătatea din lungimea subvectorului precedent. Aşa că
numărul maxim P(n) de elemente înscrise simultan în stivă, care este şi adâncimea maximă a
apelurilor recursive, satisface relaţiei
 n
P(n)  1  P    , adică P(n)  111 ...1 P(0)
log2 n
 2
Luând în consideraţie, că P(0)=0, obţinem că P(n)i)
{
int imain=divide(i,j);
if(imain-i > j-imain)
{//începem cu intervalul stâng
quicksort_intern(i,imain-1);
quicksort_intern(imain+1,j);
}
else

79

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

{//începem cu intervalul drept
quicksort_intern(imain+1,j);
quicksort_intern(i,imain-1);
}
}
}

Deoarece modificarea introdusă nu va schimba afişările obţinute prin exemplele de sortare
precedente, nu le mai repetăm.
Complexitatea spaţială se reduce în aşa fel la mărimea P(n)  O(log2 n). Apreciem complexitatea
temporală T(n). Fiindcă divizarea are complexitatea O(n), avem:

T (n)  O(n)  T (imain 0)  T (n 1 imain) . Deci, totul depinde de

imain,

adică cum vectorul va

fi divizat de către funcţia divide().
O situaţie ideală care ar putea fi obţinută prin aplicarea principiului de echilibru, constă în
segmentarea vectorului

aproximativ în două parţi

egale, aşa

ca

n
imain . Atunci
2

 n
n
 n 
T (n)  O(n)  2T    O(n)  2 O   2T    
 2
 4 
  2
 n  n
 n 
O(n)  2 O   2 O   2T      ...  O(n)  O(n)  ... O(n)  O(n log 2 n),
 8 
  2   4
fiindcă T(0)=0. Deci, T(n)=O(nlog2n), precum coeficientul la nlog2n este acelaşi ca şi coeficientul
pe lângă n la complexitatea împărţirii.
Aşadar, metoda obţine O(nlog2n) ce este limita de jos pentru complexitatea algoritmilor de sortare
bazaţi pe compararea cheilor.
Dacă divizarea sistematic se obţine lângă primul sau lângă ultimul elemente ale subvectorilor
cercetaţi (adică permanent imain=i, sau imain=j), atunci fiecare dată rămâne de sortat o parte a
subvectorului, în care numărul de elemente este cu o unitate mai mic decât subvectorul precedent, şi
rezultă că complexitatea va fi T (n)  O(n)  O(1)  T (n  1)  O(n)  O(n  1)  ... O(1)  O(n2 ) .
În acest caz sortarea rapidă are complexitatea teoretică asemănătoare cu complexitatea celor mai răi
algoritmi de sortare, de exemplu sortarea prin metoda bulelor. Dar complexitatea practică probabil
va fi şi mai mare, din cauza timpului necesar pentru realizarea recursiei dirijate de stivă. Pentru
sortarea rapidă este arătat teoretic că complexitatea medie apreciată pentru probabilităţile egale a
tuturor permutărilor este egală cu O(nlog2n) cu aproximativ 2nlog2n comparaţii a cheilor, de
asemenea este arătat că probabilitatea celui mai nefavorabil caz cu complexitatea O(n2) este destul
de mică.
Posibilitatea celui mai nefavorabil caz nu este exclusă când datele sunt deja sortate sau parţial
sortate (poate fi şi în ordine inversă).
Paradoxul sortării rapide în contrastul cu sortarea prin inserţie sau chiar prin metoda bulelor, constă
în aceea că sortarea rapidă îşi pierde calitatea la vectorii parţial ordonaţi. Faptul acesta este

80

Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

incomod, fiindcă necesitatea de a sorta datele “aproape sortate” destul de des se întâlneşte în
practică.

Îmbunătăţirea sortării rapide
Ca sortarea rapidă să devină real un algoritm efectiv, ea mai cere încă o îmbunătăţire. Este evident,
că în versiunile precedente recursia şi alegerea elementului principal devin destul de grele pentru
subvectori mici. Sortarea rapidă nu poate fi aplicată la vectori mici. De aceea recursia trebuie oprită
când dimensiunea subvectorului devine mai mică decât careva constantă, numită prag. După aceasta
se foloseşte metoda, eficacitatea căreia poate să se îmbunătăţească la datele parţial sortate, de
exemplu sortarea prin inserţie simplă.
D. Knuth a obţinut că valoarea optimală teoretică a pragului este egală cu 9.
În practică rezultatele bune ne dau valorile pragului de la 8 până la 20, iar valoarea optimă se
conţine între 14 şi 16.
//
//

C l a s s

" v e c t o r

o p t i m

q u i c k s o r t "

//
template class vector_optim_quicksort:
public vector_quicksort
{
public:
vector_optim_quicksort(char* file_name , int threshold_init=15,
int NMAX=200):
vector_quicksort(file_name, NMAX)
{
threshold=threshold_init;
if(threshold=n || j==-1)
j=n-1;
if(ij)
i=0;
quicksort_intern(i, j);
}
protected:
int threshold;

81

Structuri de date (în baza C++):