Documente Academic
Documente Profesional
Documente Cultură
S.Pereteatcu, A.Pereteatcu
3. TEHNICI DE SORTARE
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.
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].
68
Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu
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ă.
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
public:
vector<el>(int NMAX=200)
{
n=NMAX;
t=new el[n];
}
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
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:
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
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
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
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);
}
}
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].
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
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
imain-1 imain+1
i imain j
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;
}
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();
}
79
Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu
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
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();
}
81
Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu
Î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()
{
}
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;
}
};
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();
}
83
Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu
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
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);
}
}
}
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
86
Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu
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
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;
}
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
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
90
Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu
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
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);
}
}
93
Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu
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.
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
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();
};
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.
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();
}
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
98
Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu
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
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
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
void heapsort();
protected:
void reorganization(int i, int j);
void planting();
void maxtreesort();
};
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. …
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();
}
104
Structuri de date (în baza C++): Suport de curs
S.Pereteatcu, A.Pereteatcu
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
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.
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:
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