Sunteți pe pagina 1din 21

Ministerul Educației și Cercetării al Republicii Moldova

Universitate Tehnică a Moldovei


Facultatea Calculatoare, Informatică ți Microelectronică

RAPORT
Lucrarea de laborator nr.2
la cursul Analiza și Prioectarea Algoritmilor

A efectuat: st. gr. TI-224 Deleu


A verificat: asist. univ. Valentina Astafi

Chișinău 2023
Tema: Metoda divide et impera.

Scopul lucrării:
1. Studierea metodei divide et impera.
2. Analiza și implemetarea algoritmilor bazați pe metoda divide et impera.
Consideratii teoretice:
Divide et impera este o tehnica de elaborare a algoritmilor care constă în:
1. Descompunerea cazului ce trebuie rezolvat într-un număr de subcazuri mai mici ale
aceleiaşi probleme.
2. Rezolvarea succesivă şi independentă a fiecăruia din aceste subcazuri.
3. Recompunerea subsoluţiilor astfel obţinute pentru a găsi soluţia cazului iniţial.
Să presupunem că avem un algoritm A cu timp pătratic. Fie c o constantă, astfel încât timpul
pentru a rezolva un caz de mărime n este tA(n) ≤cn2. Să presupunem că este posibil să rezolvăm
un astfel de caz prin descompunerea în trei subcazuri, fiecare de mărime [n/2]. Fie d o constantă,
astfel încât timpul necesar pentru descompunere şi recompunere este t(n)≤ dn. Folosind vechiul
algoritm şi ideea de descompunere-recompunere a subcazurilor, obţinem un nou algoritm B,
pentru care:
tB(n) = 3tA([n/2])+t(n) ≤ 3c((n+1)/2)2+dn = 3/4cn2+(3/2+d)n+3/4c
Termenul 3/4cn2 domină pe ceilalţi când n este suficient de mare, ceea ce înseamnă că
algoritmul B este în esenţă cu 25% mai rapid decât algoritmul A. Nu am reuşit însă să schimbăm
ordinul timpului, care rămâne pătratic. Putem să continuăm în mod recursiv acest procedeu,
împărţind subcazurile în subsubcazuri etc.
Algoritmul formal al metodei divide et impera:
function divimp(x)
{returnează o soluţie pentru cazul x}
if x este suficient de mic then return adhoc(x)
{descompune x în subcazurile x1, x2, …, xk}
for i ← 1 to k do yi ← divimp(xi)
{recompune y1, y2, …, yk în scopul obţinerii soluţiei y pentru x}
return y
unde adhoc este subalgoritmul de bază folosit pentru rezolvarea micilor subcazuri ale
problemei în cauză (în exemplul nostru, acest subalgoritm este A).
Un algoritm divide et impera trebuie să evite descompunerea recursivă a subcazurilor
“suficient de mici”, deoarece, pentru acestea, este mai eficientă aplicarea directă a
subalgoritmului de bază.
În exemplul precedent, cu toate că valoarea lui n 0 nu influenţează ordinul timpului, este
influenţată însă constanta multiplicativa a lui n lg 3, ceea ce poate avea un rol considerabil în
eficienţa algoritmului.Pentru un algoritm divide et impera oarecare, chiar dacă ordinul timpului
nu poate fi îmbunătăţit, se doreşte optimizarea acestui prag în sensul obţinerii unui algoritm cât
mai eficient. Nu exista o metodă teoretică generală pentru aceasta, pragul optim depinzând nu
numai de algoritmul în cauză, dar şi de particularitatea implementării. Considerând o
implementare dată, pragul optim poate fi determinat empiric, prin măsurarea timpului de
execuţie pentru diferite valori ale lui n0 şi cazuri de mărimi diferite.
In general, se recomandă o metodă hibridă care constă în a a) determinarea teoretică a
formei ecuaţiilor recurente; b) găsirea empirică a valorilor, constantelor folosite de aceste ecuaţii,
în funcţie de implementare.
Sortarea prin Interclasare (Mergesort):
Un exemplu ilustrativ al metodei Divide et Impera este Mergesort, un algoritm de sortare
eficient. Problema inițială, sortarea unui tablou, este descompusă în două subprobleme de
dimensiuni aproximativ egale. Aceste subprobleme sunt apoi rezolvate recursiv și rezultatele
interclasate pentru a obține tabloul sortat final. Prin această abordare, Mergesort atinge o
complexitate temporală de O(n log n), demonstrând eficacitatea metodei.

Sortarea Rapida (Quicksort):


Un alt algoritm de sortare bazat pe Divide et Impera este Quicksort. Prin alegerea unui
element pivot, tabloul este partiționat în două subtablouri, fiecare rezolvată recursiv. Totuși,
eficiența Quicksort depinde de alegerea pivotului și de echilibrul partițiilor. Prin analiza atentă a
pragurilor în recursivitate, se poate obține o optimizare semnificativă a timpului de execuție.

Heapsort:
Heapsort este un algoritm eficient de sortare bazat pe principiul divide et impera și
folosește o structură de date specifică numită heap sau heap binar. A fost propus de J.W.J.
Williams în 1964 și este cunoscut pentru performanța sa constantă în timp, având o complexitate
a timpului de execuție O(n log n), unde n este dimensiunea datelor de intrare.

Metoda Divide et Impera oferă un cadru puternic pentru proiectarea algoritmilor


eficienți. Aplicațiile în sortarea algoritmilor, precum Mergesort și Quicksort, ilustrează
versatilitatea și potențialul acestei metode. Prin optimizarea pragurilor în recursivitate, se poate
obține o îmbunătățire semnificativă a performanței, adăugând o dimensiune pragmatică la
abordarea teoretică.

Mersul lucrării:
1. Implementarea algoritmilor propuși într-un limbaj de programare
2. Stabiliți proprietățile datelor de intrare în raport cu care se face analiza
3. Alegeți metrica pentru compararea algoritmilor propuși
4. Efectuați analiza emprică a algoritmilor propuși
5. Faceți o prezentare grafică a datelor obținute
6. Faceți o concluzie asupra lucrării efectuate

Sortarea Rapida (Quicksort):


Eficiență în Medii Medii și Mari:
1. Quicksort este eficient în practică și adesea mai rapid în cazurile medii și mari de date
decât Mergesort, având o complexitate medie O(n log n).
2. Sensibilitate la Datele de Intrare: Performanța Quicksort poate fi influențată de alegerea
pivotului, făcându-l mai puțin predictibil în anumite scenarii.
Implementarea algoritmul QuickSort
void quickSort(std::vector<int>& arr, int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}

void heapify(vector<int>& arr, int n, int i) {


int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;

if (left < n && arr[left] > arr[largest]) {


largest = left;
heap_comparisons++;
}

if (right < n && arr[right] > arr[largest]) {


largest = right;
heap_comparisons++;
}

if (largest != i) {
swap(arr[i], arr[largest]);
heap_permutations++;
heapify(arr, n, largest);
}
}
Figura 1 – Graful variației timpului in dependență de creșterea valorilor, met. Quick Sort

Sortarea prin Interclasare (Mergesort):


1. Eficiență în Sortarea Generală: Mergesort se evidențiază prin performanța sa constantă și
predictibilă, O(n log n), în cazul sortării generale a datelor.

2. Costul Spațial: Cu toate că aduce beneficii în termeni de timp, Mergesort necesită un


spațiu adițional pentru tablouri auxiliare, ceea ce poate fi o considerație în implementările
cu resurse limitate.

Implemetarea algoritmului MergeSort


void mergeSort(vector<int>& arr, int low, int high) {
if (low < high) {
int mid = low + (high - low) / 2;
mergeSort(arr, low, mid);
mergeSort(arr, mid + 1, high);
merge(arr, low, mid, high);
}
}

int partition(vector<int>& arr, int low, int high) {


int pivot = arr[high];
int i = low - 1;

for (int j = low; j < high; j++) {


if (arr[j] <= pivot) {
i++;
swap(arr[i], arr[j]);
quick_comparisons++;
quick_permutations++;
}
quick_comparisons++;
}

swap(arr[i + 1], arr[high]);


quick_permutations++;
return i + 1;

Figura 2 – Graful variației timpului in dependență de creșterea tabloului, met. Merge


Sort

Heapsort
1. Stabilitate și Eficiență Spațială: Heapsort se evidențiază prin stabilitatea sa și eficiența
spațială, având o complexitate O(n log n) și necesitând doar spațiul ocupat de tabloul de
intrare.

2. Performanță Constantă: Este eficient într-un număr variat de scenarii, având o


performanță constantă și o stabilitate remarcabilă.

Implementarea algoritmului HeapSort


public static void heapSort(int[] arr) {
int n = arr.length;
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}

for (int i = n - 1; i > 0; i--) {


int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;

heapify(arr, i, 0);
}
}

private static void heapify(int[] arr, int n, int i) {


int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;

if (left < n && arr[left] > arr[largest]) {


largest = left;
}

if (right < n && arr[right] > arr[largest]) {


largest = right;
}

if (largest != i) {
int temp = arr[i];
arr[i] = arr[largest];
arr[largest] = temp;

heapify(arr, n, largest);
}
}
Figura 3 – Graful variației timpului in dependență de creșterea valorilor, met. Heap Sort

Concluzii:
Algoritmul Quick Sort oferă o creștere liniară, ceea ce îl face eficient pentru seturi de
date mari. În cel mai bun caz, are o complexitate Ω(N log (N)), care se realizează atunci când
pivotul ales împarte matricea în jumătăți aproape egale, asigurând partiții echilibrate și, implicit,
o sortare eficientă. În medie, complexitatea sa este aceeași, ceea ce îl face unul dintre cei mai
rapizi algoritmi de sortare. Cu toate acestea, în cel mai rău caz, când pivotul este ales incorect,
complexitatea poate ajunge la O(N^2). Pentru a atenua această situație, se folosesc tehnici
precum alegerea pivotului ca mediana a trei elemente.
Merge Sort este un algoritm de sortare stabil, ceea ce înseamnă că păstrează ordinea
relativă a elementelor egale. O caracteristică notabilă a Merge Sort este performanța sa garantată
în cel mai rău caz, cu o complexitate a timpului de O(N log N). Acest lucru îl face un algoritm
eficient și pentru seturi de date mari. Cu toate acestea, pe seturi de date mici, Merge Sort își
pierde avantajele și poate fi înlocuit cu algoritmi mai eficienți. Un alt dezavantaj este necesitatea
de a utiliza memorie suplimentară pentru a stoca subsecțiunile combinate în timpul procesului de
sortare. De asemenea, Merge Sort nu este un algoritm "in-place", ceea ce înseamnă că necesită
memorie suplimentară pentru a stoca datele sortate, ceea ce poate fi o problemă în aplicații cu
restricții de memorie.
Heap Sort se diferențiază de Merge Sort prin faptul că este un algoritm "in-place". Acest
lucru înseamnă că are nevoie de o cantitate minimă de memorie suplimentară, deoarece nu
trebuie să stocheze datele sortate în afara listei inițiale. Heap Sort este mai ușor de înțeles decât
alți algoritmi de sortare eficienți, deoarece nu se bazează pe concepte avansate precum
recursivitatea. Totuși, nu este la fel de eficient ca Quick Sort sau Merge Sort în majoritatea
scenariilor, iar complexitatea sa medie este de O(N log N).
Spațiu auxiliar: O(log n), datorită stivei de apeluri recursive. Cu toate acestea, spațiul
auxiliar poate fi O(1) pentru implementarea iterativă.
Algoritmul Timpul, ms (Random order)
L. Tabloului 50 ch 100 ch 1000 ch 10000 ch 100000 ch 1000000 ch
Quicksort 0 0 0 9 70 6554
Mergesort 0 0 0 0 19 104
Heapsort 0 2 0 0 14 110

Figura 4 – Graful variației timpului a celor 3 algoritmi în dependență de mărimea uni


tablou random

Algoritmul Timpul, ms (Ascending order)


L. Tabloului 50 ch 100 ch 1000 ch 10000 ch 100000 ch 1000000 ch
Quicksort 0 0 3 38 3439 533100
Mergesort 0 0 0 0 2 57
Heapsort 0 0 0 1 2 71
Figura 5. Variația timpului de execuție al algoritmilor dacă tabloul este crescător

Algoritmul Timpul, ms (Descending order)


L. Tabloului 50 ch 100 ch 1000 ch 10000 ch 100000 ch 1000000 ch
Quicksort 3 0 0 12 648 243333
Mergesort 0 0 0 0 12 82
Heapsort 0 0 0 1 9 92
Figura 6. Variația timpului de execuție a algoritmilor dacă tabloul este descrescător

Observăm că în toate cazurile prezentate mai sus, cel mai costisitor devine a și Quick
Sort, acesta devenind extrem de ineficient atunci cand pivotul sau ales cea mai mică sau mare
valoare din întreg tabloul, astfel cazurile sale nefavorabile fiind întâlnite în tablourile pre-sortate
crescător sau descrescător.
Pe de altă parte algoritmurile precum Merge Sort sau heap Sort, în mare parte își
păstrează performanța, sau chiar în unele cazuri se dovedesc a fi mult mai eficiente. Spre
exemplu la 100000 de valori a unui tablou crescător timpul de execuție s-a dovedit a fi de doar
2ms, pe când în cazul Quick Sort au fost nevoie de 3439ms, o performanță destul de
semnificativă.
Figura 7. Reprezentarea variației algoritmului Quick Sort intr-un tabel sortat crescător,
descrescător și unul random

Figura 8. . Variațiea algoritmului Merge Sort intr-un tabel sortat crescător, descrescător
și unul random
Figura 9. Reprezentarea variației algoritmului Heap Sort intr-un tabel sortat crescător,
descrescător și unul random
Concluzii:
În concluzie, abordarea Divide et Impera, care presupune descompunerea problemelor
complexe în subprobleme mai mici, rezolvate independent și apoi recompuse, reprezintă un
cadru solid pentru dezvoltarea algoritmilor eficienți. Această metodă oferă numeroase avantaje,
precum complexitatea optimă a timpului și abilitatea de a aborda o gamă largă de probleme.
Cu toate acestea, implementarea și alegerea algoritmilor bazati pe Divide et Impera
necesită o analiză atentă a avantajelor și limitărilor fiecărui algoritm. De exemplu, Mergesort
oferă complexitatea timpului optimă, dar poate necesita spațiu suplimentar pentru stocarea
subsecțiunilor combinate. Quicksort aduce eficiență în cazul mediu, dar necesită gestionarea cu
grijă a celor mai nefavorabile situații. Heapsort oferă o abordare eficientă și stabilă, cu
complexitate constantă în timp, dar poate nu este la fel de rapid ca Quicksort în cazul mediu.
Prin urmare, alegerea între acești algoritmi ar trebui să se facă în funcție de contextul
specific și cerințele de performanță ale unei anumite aplicații. În plus, o abordare hibridă care
combină analiza teoretică cu optimizarea empirică poate ajuta la determinarea algoritmilor optmi
pentru diverse scenarii.
În înțelegerea avantajelor și limitărilor fiecărui algoritm bazat pe Divide et Impera, putem
lua decizii informate pentru alegerea și implementarea adecvată în funcție de necesitățile
specifice ale fiecărei probleme. Metoda Divide et Impera rămâne un instrument puternic în
proiectarea algoritmilor, cu aplicații semnificative în sortare și în multe alte domenii ale
informaticii.

Anexa 1 : Cod sursă

#include <iostream>
#include <vector>
#include <ctime>
#include <cstdlib>
#include <algorithm>

using namespace std;

// Variabile globale pentru a urmări timpul, permutările și


comparațiile
clock_t start_time;
long long merge_permutations = 0;
long long merge_comparisons = 0;
long long quick_permutations = 0;
long long quick_comparisons = 0;
long long heap_permutations = 0;
long long heap_comparisons = 0;

void merge(vector<int>& arr, int low, int mid, int high) {


vector<int> left(mid - low + 1);
vector<int> right(high - mid);

for (int i = 0; i < left.size(); i++) {


left[i] = arr[low + i];
}

for (int i = 0; i < right.size(); i++) {


right[i] = arr[mid + 1 + i];
}

int i = 0, j = 0, k = low;

while (i < left.size() && j < right.size()) {


if (left[i] <= right[j]) {
arr[k++] = left[i++];
} else {
arr[k++] = right[j++];
}
merge_comparisons++;
merge_permutations++;
}
while (i < left.size()) {
arr[k++] = left[i++];
merge_permutations++;
}

while (j < right.size()) {


arr[k++] = right[j++];
merge_permutations++;
}
}

void mergeSort(vector<int>& arr, int low, int high) {


if (low < high) {
int mid = low + (high - low) / 2;
mergeSort(arr, low, mid);
mergeSort(arr, mid + 1, high);
merge(arr, low, mid, high);
}
}

int partition(vector<int>& arr, int low, int high) {


int pivot = arr[high];
int i = low - 1;

for (int j = low; j < high; j++) {


if (arr[j] <= pivot) {
i++;
swap(arr[i], arr[j]);
quick_comparisons++;
quick_permutations++;
}
quick_comparisons++;
}

swap(arr[i + 1], arr[high]);


quick_permutations++;
return i + 1;
}

void quickSort(std::vector<int>& arr, int low, int high) {


if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}

void heapify(vector<int>& arr, int n, int i) {


int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;

if (left < n && arr[left] > arr[largest]) {


largest = left;
heap_comparisons++;
}
if (right < n && arr[right] > arr[largest]) {
largest = right;
heap_comparisons++;
}

if (largest != i) {
swap(arr[i], arr[largest]);
heap_permutations++;
heapify(arr, n, largest);
}
}

void heapSort(vector<int>& arr) {


int n = arr.size();

for (int i = n / 2 - 1; i >= 0; i--) {


heapify(arr, n, i);
}

for (int i = n - 1; i > 0; i--) {


swap(arr[0], arr[i]);
heapify(arr, i, 0);
heap_permutations++;
}
}

void printArray(const vector<int>& arr) {


for (int value : arr) {
cout << value << " ";
}
cout << endl;
}

int main() {
srand(static_cast<unsigned>(time(nullptr)));

int arraySize;
cout << "Introduceti dimensiunea vectorului: ";
cin >> arraySize;
vector<int> inputArray(arraySize);

for (int i = 0; i < arraySize; i++) {


inputArray[i] = rand() % 1000000; // Generați numere
între 0 și 999999
}

cout << "Vectorul initial:" << endl;


printArray(inputArray);

vector<int> mergeSortArray = inputArray;


vector<int> quickSortArray = inputArray;
vector<int> heapSortArray = inputArray;

// Măsurăm timpul pentru Merge Sort


start_time = clock();
mergeSort(mergeSortArray, 0, arraySize - 1);
double merge_sort_time = double(clock() - start_time) /
CLOCKS_PER_SEC;
// Măsurăm timpul pentru Quick Sort
start_time = clock();
quickSort(quickSortArray, 0, arraySize - 1);
double quick_sort_time = double(clock() - start_time) /
CLOCKS_PER_SEC;

// Măsurăm timpul pentru Heap Sort


start_time = clock();
heapSort(heapSortArray);
double heap_sort_time = double(clock() - start_time) /
CLOCKS_PER_SEC;

// Afișăm timpul, permutările și comparațiile pentru fiecare


metodă
cout << "Sortarea folosind Merge Sort: " << merge_sort_time
<< " secunde, " << merge_permutations << " permutari, " <<
merge_comparisons << " comparatii" << endl;
cout << "Sortarea folosind Quick Sort: " << quick_sort_time
<< " secunde, " << quick_permutations << " permutari, " <<
quick_comparisons << " comparatii" << endl;
cout << "Sortarea folosind Heap Sort: " << heap_sort_time <<
" secunde, " << heap_permutations << " permutari, " <<
heap_comparisons << " comparatii" << endl;

return 0;
}

Rezultatul la consolă:

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