Sunteți pe pagina 1din 40

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]<t[j], t[i]<=t[j], etc.
Presupunem, că funcţia swap(i,j) schimbă cu locurile elementele t[i] şi t[j] a vectorului.
Funcţia cmp(i,j) întoarce un număr întreg, care este egal cu
-1, dacă t[i]<t[j];
0, dacă t[i]==t[j];
1, dacă t[i]>t[j].

Definiţia 1. Perechea de indici (i,j), astfel ca i<j, dar t[i]>t[j] se numeşte


inversie.

Definiţia 2. Vectorul t se numeşte sortat în ordine crescătoare, dacă el nu


conţine nici o inversie.

Definiţia 3. Vectorul t se numeşte sortat în ordine descrescătoare, dacă pentru


orice i, j, i<j rezultă că sau (i,j) este inversie, sau t[i]==t[j].

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

Definiţia 4. Algoritmul sortării se numeşte stabil, dacă el niciodată nu schimbă


ordinea relativă în vector la oricare 2 elemente cu cheile egale. Adică pentru
orice pereche i, j: i<j; t[i]=t[j], i—>i', j—>j' => i'<j'.

Stabilitatea poate fi o cerinţă definitoare, de exemplu când are loc sortarea după o anumită cheie a
elementelor deja sortate după o altă cheie.
Exemplu: lista studenţilor care este deja sortată după numele se cere de aranjat după anul de studii,
dar pentru fiecare an de studii numele trebuie să rămână în ordinea alfabetică.

Despre complixitatea algoritmilor de sortare

La caracterizarea algoritmilor de sortare se foloseşte noţiunea de mărime (de exemplu numărul de


comparaţii de chei) care depinde de un număr natural n (de exemplu numărul de elemente) relativ la
o altă mărime cunoscută care tot depinde de numărul natural acesta. Se spune că mărimea k are
ordinul comparabil cu mărimea m, se notează k=O(m), se citeşte “k are ordinul o de m”, dacă:
k
lim m
n
const

Din formula aceasta rezultă că mărimile n-1, n, 2n, n+3 au unul şi acelaşi ordin O(n). Iar mărimea
n2 are un ordin mai mare.
Algoritmul de sortare a n elementelor bazat pe compararea cheilor are complexitatea minimă O(n)
sau mai mare.
Demonstrăm prin inducţia metematică:
Pentru k = 2 – o comparaţie. Presupunem că pentru k = n-1 trebuiesc n-2 comparaţii. Se adaugă încă
un element, deci mai trebuie de făcut cel puţin încă o comparaţie, obţinem n-2+1 = n-1 comparaţii.
Deci, complexitatea minimă este O(n).
Teoretic este demonstrat [vezi 1] că complexitatea medie a oricărui algoritm de sortare care
operează cu comparaţiile nu poate fi mai mică de O(nlog2n).
Algoritmii triviali de sortare bazaţi pe compararea cheilor au complexitatea atât medie cât şi cea
maximă O(n2).
Complexitatea medie a oricărui algoritm bun de sortare bazat pe compararea cheilor este O(nlog2n),
totodată complexitatea maximă a lui poate fi ori O(nlog2n) ori O(n2).

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

3.2. Clase pentru exemple de sortare


Vom folosi clasa abstractă elem şi clasa concretizată usual_elem.
Declarăm clasa generică vector parametrizată cu clasa el.
//
// C l a s s "v e c t o r"
//
template <class el> class vector: public SD
{
protected:
int n; // numărul de elemente ale vectorului
el* t; // pointer la vectorul de elemente

public:
vector<el>(int NMAX=200)
{
n=NMAX;
t=new el[n];
}

vector<el>(char* file_name, int NMAX=200): SD(file_name)


{
t=new el[NMAX];
n=0;
while(!feof(SD::pf))
if(t[n].fscanf_el(SD::pf)>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; i<n; i++)
{
if(i>0 && i%nlinepp==0)
{

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

printf("Press any key to continue...\n");


getch();
clrscr();
printf("%s", opening);
}
printf("%4d. ", (i+1));
t[i].show("", "\n");
}
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) && i<n ; i++)
if(SD::ncomp++, e==this->t[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];

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

t[i]=t[j];
t[j]=tempor;
}
};

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

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

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; i<n-1; i++)
if(ncomp++, t[i]>t[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.
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.

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

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.

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

3.4. Sortarea rapidă


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 el> class vector_quicksort: public vector<el>
{
public:
vector_quicksort<el>(char* file_name, int NMAX=200):
vector<el>(file_name, NMAX)
{
}

void quicksort(int i=0, int j=-1)


{
if(j>=n || j==-1)
j=n-1;
if(i<0 || i>j)
i=0;
quicksort_intern(i, j);
}

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

imain-1 imain+1
0 imain n-1

elemente ≤ t[imain] elemente ≥ t[imain]


75
elementul principal t[imain]
Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu

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):
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 <class el>
void vector_quicksort<el>::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.


elementul
Schema metodei de divizare în timpul O(n): princ ipal
4 20 2 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,
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.

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

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” al 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.

77
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 imain+1
i imain j

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 imain+1
i imain j

elemente ≤ t[imain] elementul elemente ≥ t[imain]


principal

template <class el> int vector_quicksort<el>::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);

78
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] <= t[i]))
imain++;
while((jmain > 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<usual_elem> 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:

79
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
10. Black 1981 500.00
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.


Exerciţiu.
Prefaceţi constructorul clasei sorted_table din 2.4 în aşa mod ca el mai întâi să creeze tabelul
simplu neordonat şi apoi să-l sorteze prin algoritmul de sortare rapidă.

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()
{
}

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

year_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->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<year_elem> 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

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

8. Yellow 1988 300.00


9. Magenta 1983 600.00
10. Black 1981 500.00
End of vector. Press any key ...
Group sorted by year:
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)

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

{
int result;
if(this->salary < ((salary_elem&)e2).salary)
result=-1;
else
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<salary_elem> 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
10. Black 1981 500.00
End of vector. Press any key ...
Group sorted by salary:
1. Yellow 1988 300.00

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

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
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.

Aprecierea complexităţii algoritmului de sortare rapidă

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). În
versiunea recursivă a funcţiei quicksort_intern() nu putem să îmbunătăţim complexitatea
spaţială. Pentru versiunea iterativă a acestei funcţii cu utilizarea stivei (vezi 4.4) există o metodă
simplă: de a începe întotdeauna cu subvectorul de lungime mai mică. Atunci aceasta lungime va fi
mai mică decât jumătatea din lungimea subvectorului precedent. Aşa că numărul maxim P(n) de
intervale înscrise simultan în stivă va satisface relaţiei
n
P ( n) 1 P , adică P(n) 1 1 1 ... 1 P(0)
2 log 2 n

Luând în consideraţie, că P(0)=0, obţinem că P(n)<log2n+1, aşa că complexitatea spaţială se reduce


la O(log2n), (pentru n=106, log2106≈20, trebuie să prevedem stiva de adâncime 20, ce este deja
accesibil). Deci cu scopurile viitoare modificăm funcţia quicksort_intern() ca ea să se arate
astfel:
template <class el>
void vector_quicksort<el>::quicksort_intern(int i, int j)
{
if (j>i)
{

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

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
{//î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 precedente de


sortare, nu le mai repetăm.
Complexitatea spaţială în viitor (când vom utiliza stiva) va fi redusă în aşa fel la mărimea
P(n) O(log 2 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
n
segmentarea vectorului aproximativ în două parţi egale, aşa ca imain . Atunci
2
n n n
T (n) O(n) 2T O ( n) 2 O 2T
2 2 4
n n n
O ( n) 2 O 2 O 2T ... O(n) O(n) ... O(n) O(n log 2 n),
2 4 8

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 să 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(n 2 ) .

Î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

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

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
incomod, fiindcă necesitatea de a sorta datele “aproape sortate” destul de des se întâlneşte în
practică.
Pentru a confirma cele spuse să creăm fişierul text cu numele, de exemplu ”stud20.txt” şi cu
uirmătorul conţinut:
Green 1987 350.00
Green 1987 350.00
Green 1987 350.00
Green 1987 350.00
Green 1987 350.00
Green 1987 350.00
Green 1987 350.00
Green 1987 350.00
Green 1987 350.00
Green 1987 350.00
Green 1987 350.00
Green 1987 350.00
Green 1987 350.00
Green 1987 350.00
Green 1987 350.00
Green 1987 350.00
Green 1987 350.00
Green 1987 350.00
Green 1987 350.00
Green 1987 350.00

În funcţia main() să creăm pe baza fişierului ”stud20.txt” un vector concretizat cu clasa


usual_elem şi să sortăm acest vector cu algoritmul de sortatre rapidă. Vom obţine următoarea
afişare:
Unsorted group:
1. Green 1987 350.00
2. Green 1987 350.00
3. Green 1987 350.00
4. Green 1987 350.00
5. Green 1987 350.00
6. Green 1987 350.00

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

7. Green 1987 350.00


8. Green 1987 350.00
9. Green 1987 350.00
10. Green 1987 350.00
11. Green 1987 350.00
12. Green 1987 350.00
13. Green 1987 350.00
14. Green 1987 350.00
15. Green 1987 350.00
16. Green 1987 350.00
17. Green 1987 350.00
18. Green 1987 350.00
19. Green 1987 350.00
20. Green 1987 350.00
End of vector. Press any key ...
Group sorted by name:
1. Green 1987 350.00
2. Green 1987 350.00
3. Green 1987 350.00
4. Green 1987 350.00
5. Green 1987 350.00
6. Green 1987 350.00
7. Green 1987 350.00
8. Green 1987 350.00
9. Green 1987 350.00
10. Green 1987 350.00
11. Green 1987 350.00
12. Green 1987 350.00
13. Green 1987 350.00
14. Green 1987 350.00
15. Green 1987 350.00
16. Green 1987 350.00
17. Green 1987 350.00
18. Green 1987 350.00
19. Green 1987 350.00
20. Green 1987 350.00
End of vector. Press any key ...
n=20, ncomp=247, n*log2(n)=86.44, n*n=400

După cum se vede în cazul acesta numărul de comparaţii este mai aproape de n2 decât de nlog2n.

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

Îmbunătăţirea sortării rapide

Ca sortarea rapidă să devină real un algoritm efectiv, ea cere 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 el> class vector_optim_quicksort:
public vector_quicksort<el>
{
public:
vector_optim_quicksort<el>(char* file_name , int threshold_init=15,
int NMAX=200):
vector_quicksort<el>(file_name, NMAX)
{
threshold=threshold_init;
if(threshold<1)
threshold=1;
}

void quicksort(int i=0, int j=-1)


{
if(j>=n || j==-1)
j=n-1;
if(i<0 || i>j)
i=0;
quicksort_intern(i, j);
}

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

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

template <class el>


void vector_optim_quicksort<el>::quicksort_intern(int i, int j)
{
if(j-i+1>threshold)
{
int imain=divide(i,j);
if(imain-i > j-imain)
{
quicksort_intern(i,imain-1);
quicksort_intern(imain+1,j);
}
else
{
quicksort_intern(imain+1,j);
quicksort_intern(i,imain-1);
}
}
else
insertsort(i, j);
}

template <class el>


void vector_optim_quicksort<el>::insertsort(int i, int j)
{
for(int k = i+1; k<=j; k++)
for(int l=k; (l>i)&&(ncomp++, t[l]<t[l-1]); l--)
swap(l-1, l);
}

Exemple de funcţia main() luăm din trei exemple precedente, înlocuind în ele denumirea clasei
vector_quicksort cu vector_optim_quicksort şi adăugând la parametrii constructorilor
valoarea pentru prag, de exemplu 4. Afişările, analiza cărora rămâne ca exerciţiu, vor arăta astfel:
Pentru vectorul concretizat cu clasa usual_elem
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

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

8. Yellow 1988 300.00


9. Magenta 1983 600.00
10. Black 1981 500.00
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=32, n*log2(n)=33.22, n*n=100

Pentru vectorul concretizat cu clasa year_elem


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
10. Black 1981 500.00
End of vector. Press any key ...
Group sorted by year:
1. Gray 1968 900.00
2. Cyan 1975 800.00
3. White 1980 600.00
4. Red 1980 450.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 ...

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

n=10, ncomp=37, n*log2(n)=33.22, n*n=100

Pentru vectorul concretizat cu clasa salary_elem


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
10. Black 1981 500.00
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
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=33, n*log2(n)=33.22, n*n=100

În următorul tabel este prezentat numărul de comparări efectuate la sortarea obiectelor


vector_optim_quicksort create pe baza fişierului ”stud.txt” pentru diferite valori ale pragului
şi clase de concretizare.

Câmpul de sortare
Prag
name year salary
0 39 42 43
1 39 42 43
2 37 37 41
3 35 37 38
4 32 37 33
5 29 34 28
6 29 34 28
7 29 34 28
8 29 34 28

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

9 29 34 28
10 30 28 25
11 30 28 25
Analiza rezultatelor prezentate în acest tabel rămâne ca exerciţiu.
Exerciţiu.
Prefaceţi constructorul clasei sorted_table din 2.4 în aşa mod ca el mai întâi să creeze tabelul
simplu neordonat şi apoi să-l sorteze prin algoritmul optimizat de sortare rapidă.

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

3.5. Sortarea prin inserţie

Inserţie simplă

Ideea principală a sortării prin inserţie (engl. Insert sort) este simplă: alegem careva element, sortăm
celelalte elemente, “inserăm” elementul ales (adică îl includem) la locul potrivit printre altele deja
sortate.
void recursivinsertsort(int i, int j)
{
if(j>i)
{
recursivinsertsort(i, j-1);
for(int l=j; (l>i)&&(ncomp++, t[l]<t[l-1]; l--))
swap(l-1, l);
}
}

În forma nerecursivă sortarea prin inserţie se scrie astfel:


void insertsort(int i, int j)
{
int k;
for(k=i+1; k<=j; k++) //aici este sortat t[i]÷t[k-1]
{
for(int l=k; (l>i)&&(ncomp++,t[l]<t[l-1]; l--))
swap(l-1, l);
//aici este sortat t[i]÷t[k]
}
}

Este evident că această metodă de sortare este stabilă.


Complexitatea medie şi maximă a operaţiei de inserţie este egală O(k-i), dar timpul îndeplinirii se
îmbunătăţeşte, dacă compararea cheilor se combină cu deplasarea elementelor care au chei mai mari
decât cheia elementului t[k].
Este uşor de observat, că în procesul inserţiei elementul inserat se încadrează în toate permutările
secvenţiale. Şi dacă interschimbările sunt mai voluminoase ca semischimburi, se poate puţin de
îmbunătăţit eficacitatea acestui proces:
void insertsort(int i, int j)
{
int k;
for(k=i+1; k<=j; k++) //aici este sortat t[i]÷t[k-1]
{
element tmp=t[k];
int l;

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

for(l=k-1; (l>=i)&&(ncomp++, t[l]<tmp); l--)


t[l+1]=t[l];
l++;
if(l<k)
t[l]=tmp;
//aici este sortat t[i]÷t[k]
}
}

Complexitatea maximă şi medie a algoritmului de inserare este egală O(n2) în presăpunere că toate
n(n 1)
permutările au probabilităţi egale. Numărul exact de îndeplinire a ciclului este egal pentru
4
n(n 1)
valoarea medie şi pentru valoarea maximală (în cazul când vectorul iniţial este sortat în
2
ordine inversă).
Evidenţiem totuşi una din proprietăţile importante a sortării prin inserţie simplă: spre deosebire de
alte metode ea are cea mai bună eficacitate dacă în vectorul iniţial este stabilită o oarecare ordine.
Dacă vectorul iniţial este total sortat, atunci complexitatea este egală O(n). În cazul general
algoritmul foloseşte orice ordine parţială, ce se conţine în vector. Dacă mai luăm în consideraţie şi
simplitatea algoritmului, ajungem la concluzie că acest algoritm este cel mai bun pentru finisarea
lucrului a metodelor mai pretenţioase, cum de exemplu este sortarea rapidă. Adică pentru finisarea
lucrului algoritmilor, care destul de repede împart vectorul în mai multe părţi “aproape” sortate, dar
cer destul de mult timp şi “se înăduşesc” la sortarea finală a subvectorilor mici.

Metoda lui Shell

Neajunsul esenţial al sortării prin inserţie simplă constă în aceia, că la fiecare pas de mutare
elementul care se mută, se deplasează doar cu o poziţie. Fiecare din aceste mutări elimină exact o
inversie. În rezultat numărul total de mutări a datelor este egal cu numărul iniţial de inversii, care în
c n2 n(n 1) n(n 1)
probabilitatea medie este: .
2 2*2 4
Donald L. Shell în anul 1959 a propus în loc de inserţia sistematică a elementului cu indicele i în
sub vectorul elementelor precedente (modul care contrazice principiului de echilibru), de a include
acest element în sublistă, conţinând elemente cu indicii i-h, i-2h, i-3h, …, unde h – o constantă
pozitivă (pas).
Astfel se formează vectorul, în care h–seria elementelor (elemente care se află la distanţa h unul de
la altul) se sortează deoparte.

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

t[0] t[1] t[2]…t[0+h] t[1+h] t[2+h]…t[0+2*h] t[1+2*h] t[2+2*h]…

După ce au fost sortate deoparte neintersectatele h–serii, procesul începe cu valori noi h'<h. Dar
proprietatea caracteristică sortării prin inserţie uşurează problema în comparaţie cu situaţia în care
vectorul iniţial este arbitrar. Preventiv sortarea prin serii cu distanţa h grăbeşte sortarea prin serii cu
distanţa h'. Algoritmul Shell esenţial foloseşte această proprietate magică, sortând mai întâi seriile
cu distanţa hi, apoi hi-1,…, apoi h1, unde hi, hi-1,…,h1 – sunt aranjate descrescător şi h1=1. Ultima
schimbare trebuie să furnizeze sortarea totală a vectorului, ce reprezintă ultimele valori hi, hi-1,…,h1.
Nu este un răspuns exact la întrebarea: “Care sunt valorile optimale pentru hi, hi-1,…,h1?”. Secvenţa
optimală depinde de mulţi factori şi nu în ultimul rând de n - dimensiunea vectorului sortat.
Pentru vectorii destul de mari rezultatele testelor au arătat, că poate fi recomandată secvenţa paşilor
{hi}, astfel ca hi+1=3hi+1, adică secvenţa care include în ordinea descrescătoare: …, 364, 121, 40,
13, 4, 1. Procesul începe cu hm-2, unde m – este cel mai mic număr întreg, aşa ca hm≥n, cu alte
n
cuvinte hm-2 este primul element al secvenţei egal cu .
9

Creăm în baza clasei generice vector clasa generică vector_Shellsort dotată cu algoritmul de
sortare prin metoda lui Shell:
//
// C l a s s "v e c t o r _ S h e l l s o r t "
//
template <class el> class vector_Shellsort: public vector<el>
{
public:
vector_Shellsort<el>(char* file_name, int NMAX=200):
vector<el>(file_name, NMAX)
{

void Shellsort();
};

template <class el>


void vector_Shellsort<el>::Shellsort()
{
int h;
int i, j, k;

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

el tmp;
//definirea pasului iniţial
h=1;
while(h<n/9)
h=3*h+1;
// aici pasul este egal max(1, hm-2)
do
{
// Sortarea prin serii cu distanţa h
for(k=0; k<h; k++)
{ // sortarea k serii inserate
for(i=h+k; i<n; i+=h)
{ //includerea lui t[i] la locul său printre precedente
tmp=t[i];
j=i-h;
while((j>=0)&&(SD::ncomp++, t[j]>tmp))
{
t[j+h]=t[j];
j-=h;
}
t[j+h]=tmp;
}
}
//micşorarea incrementului
h=h/3;
} while(h>0);
}

Observaţii:
1. În locul recalculării paşilor secvenţiali h se poate de obţinut pe ei din calcularea preventivă şi
stocarea într-un vector adăugător:
2. Se poate de unit buclele după k şi după i, înlocuind schimburi cu semischimburi;
3. Sortarea prin metoda lui Shell este instabilă, şi problema aceasta nu poate fi rezolvată uşor.

În funcţia main() instanţiem clasa vector_Shellsort concretizată cu clasa usual_elem, afişăm


vectorul iniţial, îl sortăm prin metoda lui Shell, afişăm vectorul sortat şi informaţii legate de
aprecierea complexităţii:
void main()
{
clrscr();

vector_Shellsort<usual_elem> gr("Stud.txt");
gr.show("Unsorted group:\n","");

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

gr.Shellsort();
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:
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
10. Black 1981 500.00
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=30, n*log2(n)=33.22, n*n=100

După cum se vede numărul de comparări a coincis cu numărul de comparări la inserţie simplă.
Exerciţiu.
Prefaceţi constructorul clasei sorted_table din 2.4 în aşa mod ca el mai întâi să creeze tabelul
simplu neordonat şi apoi să-l sorteze prin metoda lui Shell.

Înlocuim în funcţia main() clasa usual_elem cu clasa year_elem, atunci vom obţine:

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

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
10. Black 1981 500.00
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=28, n*log2(n)=33.22, n*n=100

Analiza rezultatelor rămâne ca exerciţiu.


Dacă vom înlocui în funcţia main() clasa year_elem cu clasa salary_elem, vom obţine:
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
10. Black 1981 500.00
End of vector. Press any key ...
Group sorted by name:
1. Black 1981 500.00
2. Blue 1981 500.00

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

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=25, n*log2(n)=33.22, n*n=100

Analiza rezultatelor rămâne ca exerciţiu.


Metoda lui Shell esenţial întrece inserţia simplă pentru n mai mari de 100. Numărul necesar de
comparări în mediu are ordinul 1.66n1.25 pentru un n destul de mare. Analizând tabelul următor:
n 1.66n1.25 nlog2n
10 29,5 33.2193

100 525 664


1000 9335 9966
10000 166000 132877
105 2,95*106 1,66*106
106 5,25*107 1,99*107

vedem că metoda lui Shell rezistă competiţia cu metodele O(nlog2n) până la n=105.
Sortarea Shell rău se adaptează la sistemele cu memorie virtuală (adică care acoperă vectorul cu
intervale largi).

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

3.6. Sortarea prin selecţie

Selecţie simplă

Ideea acestei metode de sortare (engl. Selection sort) este foarte simplă: se selectează elementul
maxim din tablou şi i se schimbă locul cu ultimul element; în continuare se caută elementul maxim
până la ultimul şi i se schimbă cu penultimul elementul. La fiecare parcurgere se examinează toate
elementele ale vectorului care au rămas neordonate, elementul maxim din care va fi alăturat la cele
ordonate.
În formă nerecursivă selecţie simplă constă din n-1 etape. La etapa k se caută elementul cu cheia
maximă dintre elementele care nu sunt ordonate până la capăt şi-l leagă la poziţia n-k.
Exemplu:
void selectsort()
{
int imax, i, j;
for(i=n-1; i>0; i--)
{
imax=i;
for(j=0; j<i; j++)
if(ncomp++, t[j]>t[imax])
imax=j;
swap(i,imax);
// aici sub vectorul t[i ] t[n 1] este sortat(invarianta buclei)
}
}

Acest algoritm are complexitatea O(n2) în toate cazurile. Numărul de îndepliniri a ciclului interior
n(n 1)
este egal .
2

3.7. Sortarea arborescentă (piramidală, heapsort)


Sortarea arborescentă provine de la selecţie simplă. Neajunsul principal al sortării prin selecţie
simplă constă în aceea că comparaţiile făcute la fiecare etapă dau mult mai multe informaţii decât
cele care se folosesc efectiv pentru a pune elementul curent la locul potrivit. Pentru a obţine o
îmbunătăţire esenţială, trebuie de folosit o structură de date mai dezvoltată, permiţându-i pe cât se
poate, să păstreze informaţia, obţinută secvenţial la verificare. De exemplu, dacă t[i]>=t[k], iar
t[k]>=t[j], atunci t[i]>=t[j].

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

Sortarea arborescentă foloseşte un arbore binar concret, din care este uşor de extras elemente cu
chei maxime. Acest arbore se numeşte arbore de maximizare, el posedă proprietatea că elementul în
fiecare vârf are cheia mai mare sau egală decât cheile ale fiilor dacă aceşti fii există. La aplicarea
arborilor de maximizare apare problema de reorganizare când doi subarbori ai rădăcinii sunt de
maximizare, iar arborele întreg nu este de maximizare.

Maximizarea arborelui

6 9

9 8 7 8

7 3 5 6 6 3 5 6

6 5 1 6 5 1
4 4

În acest caz pentru reorganizarea arborelui, adică pentru îl preface în arbore de maximizare, trebuie
să facem câţiva paşi. Începând de la rădăcină ne deplasăm în direcţia maximală interschimbând
elementele.
Arborele binar plin, adică, la care până la orice nivel există toate nodurile cu posibila excepţie
pentru cele mai drepte noduri ale ultimului nivel. Astfel de arbore poate fi reprezentat sub formă de
vector.
Nodului cu indice i îi corespunde nodurile cu indicii 2*i+1 (fiul stâng, dacă 2*i+1<=n-1) şi 2*i+2
(fiul drept, dacă 2*i+2<=n-1).
Important de înţeles că nu creăm un arbore aparte, folosind formulele 2*i+1 şi 2*i+2, prelucrăm
acest vector ca un arbore binar.
Un subarbore binar al arborelui binar care corespunde vectorului t[0]÷t[n-1] poate fi determinat
printr-o pereche de indici i şi j care corespund condiţiei (0<=i)&&(i<=j)&&(j≤n-1), fii
elementului k, unde (i<=k)&&(k<=j), vor avea indicii 2*i+1 şi 2*i+2, dacă aceste valori nu întrec
j.

Creăm în baza clasei generice vector clasa generică vector_heapsort dotată cu algoritmul de
sortare arborescentă:
//
// C l a s s "v e c t o r _ h e a p s o r t "
//
template <class el> class vector_heapsort: public vector<el>
{
public:

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

vector_heapsort<el>(char* file_name, int NMAX=200):


vector<el>(file_name, NMAX)
{
}

void heapsort();

protected:
void reorganization(int i, int j);
void planting();
void maxtreesort();
};

Descriem funcţia de reorganizare pentru subarbori.


Fie: i, j o pereche de indici astfel că 0<=i<=j<=n-1 şi subarborii t[2*i+1], t[2*i+2], sunt arbori
de maximizare, dacă aşa subarbori există, atunci funcţia de reorganizare poate fi descrisă astfel :
template <class el>
void vector_heapsort<el>::reorganization(int i, int j)
{
int pred, next, left, right; // indicii pentru parcurgerea arborelor
next=i ;
do
{
pred=next; // temporar
left=2*pred+1;
if(left<=j) // daca apartine subarborelui
{
if(ncomp++, t[left]>t[next])
next=left;
right=left+1; // 2*pred+2
if((right<=j)&&(ncomp++, t[right]>t[next]))
next=right;
if(next!=pred)
swap(pred,next); // schimbam cu locurile elementele
// t[pred] si t[next]
}
} while(pred!=next);
}

Ca rezultat, în arborele binar ce corespunde vectorului t, subarborele binar t[i], t[2*i+1],


t[2*i+2],…, t[j] va fi arbore de maximizare .

Complexitatea reorganizării este O(hi,j), unde hi,j - adâncimea subarborelui, corespunzător


elementelor t[i], t[2*i+1], t[2*i+2],…, t[m*i+1], t[m*i+2]. De aceea hi,j=O(log2(j-i+1)). În

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

aşa fel complexitatea reorganizării este O(log2(j-i+1)), mai concret ea se îndeplineşte cu 2(log2(j-
i+1)) comparaţii şi log2(j-i+2) interschimbări.
Algoritmul de sortare arborescentă constă din două etape fiecare din care foloseşte reorganizarea:
Plantare (planting) – transformă vectorul iniţial în vectorul cu arbore de maximizare;
Sortarea arborelui de maximizare (maxtreesort) – face sortarea luând în consideraţie arborii de
maximizare.
template <class el> void vector_heapsort<el>::heapsort()
{
planting();
maxtreesort();
}

La plantare reorganizarea se aplică la aşa indicii i subarborii la care, dacă există, sunt arbori de
maximizare. Începem de la elementul t[(n-1)/2], fiindcă elementul cu indice mai mare nu are
subarbori.
template <class el> void vector_heapsort<el>::planting()
{
for(int i=(n-1)/2; i>=0; i--)
reorganization(i, n-1);
}

Complexitatea plantării poate fi dedusă din complexitatea reorganizării, adică din subvectorii
cercetaţi, corespunzători vârfurilor arborelui:
1. adâncimea sub vectorului h=[log2n]+1 – vectorul t[0]÷t[n-1];
2. adâncimea sub vectorului h-1 – vectorii t[1]÷t[n-1], t[2]÷t[n-1];
3. adâncimea sub vectorului h-2 – vectorii t[3]÷t[n-1], t[4]÷t[n-1], t[5]÷t[n-1],
t[6]÷t[n-1];

4. …

În aşa fel complexitatea poate fi scrisă în forma: O(1*h*2*(h-1)+...+2h-2*2)=O(2h-1)=O(n).


După plantare t[0] a devenit elementul maximal, el trebuie sa fie interschimbat cu elementul t[n-
1], după ce rămâne de sortat sub vectorul t[0]÷t[n-2], dar acest sub vector va fi arbore de
maximizare cu unică excepţie posibilă, privind elementul t[0] (precedentul t[n-1]), de aceea la
acest subvector aplicăm reorganizarea, pentru a obţine maximul subvectorului în t[0], pe care
interschimbăm cu t[n-2], etc.
template <class el> void vector_heapsort<el>::maxtreesort()
{
for (int i=(n-1);i>=1;i--)
{
swap (0, i);
reorganization(0, i-1);
}

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

}
n 1
Complexitatea acestei proceduri este T (n) O (h0,i ) , unde h0,i - adâncimea corespunzătoarea
i 1

subarborelui t[0]÷t[i], adică h0,i log 2 i 1 . Deci, obţinem că T(n)=O(nlog2n). În aşa fel
complexitatea algoritmului de sortare arborescenta este O(n+nlog2n)=O(nlog2n).
Aceasta este complexitatea atât maximală cât şi cea medie fiindcă nimic n-am presupus despre
repartizarea iniţială a elementelor. Sortarea arborescentă este cel mai sigur algoritm de sortare.
void main()
{
clrscr();

vector_heapsort<usual_elem> gr("Stud.txt");
gr.show("Unsorted group:\n","");
gr.heapsort();
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 pentru clasa usual_elem va fi:


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
10. Black 1981 500.00
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

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

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=41, n*log2(n)=33.22, n*n=100

Exerciţiu.
Prefaceţi constructorul clasei sorted_table din 2.4 în aşa mod ca el mai întâi să creeze tabelul
simplu neordonat şi apoi să-l sorteze prin algoritmul de sortare arborescentă.

Înlocuim în funcţia main() clasa usual_elem cu clasa year_elem, atunci vom obţine:
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
10. Black 1981 500.00
End of vector. Press any key ...
Group sorted by year:
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=38, n*log2(n)=33.22, n*n=100

Înlocuim în funcţia main() clasa year_elem cu clasa salary_elem, atunci vom obţine:
Unsorted group:
1. Green 1987 350.00

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

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
10. Black 1981 500.00
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
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=40, n*log2(n)=33.22, n*n=100

Din rezultatele obţinute se vede că algoritmul de sortare arborescentă este puţin mai lent decât
algoritmul de sortare rapidă.
Exerciţii.
1. Creaţi clasa vector_ternarysort dotată cu algoritmul de sortare care va utiliza arborele ternar
plin de maximizare (t[3*i+1], t[3*i+2], t[3*i+3]). Demonstraţi utilizarea acestei clase.
Apreciaţi teoretic complexitatea medie şi cea maximă pentru acest algoritm. Calculaţi numărul de
comparaţii ale cheilor pentru un vector concret.
2. Creaţi clasa vector_quaternarysort dotată cu algoritmul de sortare care va utiliza arborele
cuaternar plin de maximizare (t[4*i+1], t[4*i+2], t[4*i+3] t[4*i+4]). Demonstraţi utilizarea
acestei clase. Apreciaţi teoretic complexitatea medie şi cea maximă pentru acest algoritm. Calculaţi
numărul de comparaţii ale cheilor pentru un vector concret.

3.8. Comparaţii practice ale diferiţilor algoritmi de sortare


S-au folosit diferite principii de sortare şi poate părea greu de ales algoritmul potrivit. El depinde de
mulţi factori. Dar fiecare din aceşti algoritmi se poate uşor şi repede de reprogramat. De aceea
adresarea la Bubblesort din cauza că “este uşor de scris”, nu poate fi justificată nicicum.
La alegerea algoritmului de sortare pot fi date următoarele recomandări:

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

- pentru n mici (≈100) şi poate mai mari, dacă nu încercăm să câştigăm câteva microsecunde,
sortarea prin inserţie simplă ne dă un rezultat destul de bun, în special dacă datele sunt deja
parţial sortate;
- pentru n de la câteva sute până la câteva mii, metoda Shell ne dă un rezultat excelent. În
sistemele cu memorie virtuală ea nu trebuie folosită, dacă vectorul se aranjează pe un număr
mare de pagini;
- pentru n >100 (de exemplu) quicksort este probabil, cel mai bun algoritm în caz general; dar
el poate să crească până la O(n2) cu probabilitatea ne nulă (probabilitatea totuşi este destul
de mică, dacă este bine scrisă divizarea);
- pentru n>100 sortarea Heapsort cere aproape de două ori mai mult timp în mediu, faţă de
sortare rapidă, dar este garantată comportarea ei cu O(nlogn).

În comparaţiile experimentale ale diferitor metode de sortare, a fost folosit un vector real, în care
fiecare element era cheia lui proprie. Vectorul a fost alcătuit din elemente aleatorie, ceia ce puţin a
favorizat sortarea rapidă.
Observaţie: Concluziile pot fi diferite în dependenţă de preţul de comparare a cheilor ce se
compară, de modul de interschimbare a elementelor, şi de alte operaţii:

N 10 100 1000 10000 25000 50000

metoda
Bublesort 0,16 20 2400
Extractsort 0,12 7,3 680
Insertsort 0,12 6,7 610
Shellsort 0,07 2 37 600 1800 4200
Heapsort 0,2 3,5 50 660 1830 3960
Quicksort 0,07 2 28 365 1000 2140

107

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