Differences

This shows you the differences between two versions of the page.

Link to this comparison view

sda-aa:laboratoare:07 [2021/02/28 23:31]
127.0.0.1 external edit
sda-aa:laboratoare:07 [2021/04/12 06:55] (current)
cristian.rusu [4. Exerciții]
Line 1: Line 1:
-===== Laboratorul 6: BST si Tries =====+===== Laboratorul 6: Algoritmi de sortare 2 =====
  
 +==== 1. Obiectivele laboratorului ====
 +  * Înțelegerea structurii și proprietăților unui heap
 +  * Implementarea algoritmilor de sortare heap și radix sort (cu măsurarea timpului de execuție)
  
 +==== 2. Introducere ====
 +
 +=== 2.1 Heap ===
 +
 +Există mai mutle tipuri de heap, dar ne vom referi numai la binary heap pentru implementarea algoritmului Heap sort.
 +
 +Un heap binar este un arbore binar cu următoarele proprietăţi:​
 +   * este „complet“ (toate nivelele sunt pline, cu posibila excepţie a ultimului nivel), adică de înălţime minimă
 +   * există aceeaşi relaţie de ordine între orice nod şi părintele acestuia (excepţie - nodul rădăcină).
 +
 +Dacă nodurile conţin numere întregi după care stabilim relaţia de ordine, heap-ul poate fi de două feluri:
 +   * max-heap (rădăcina are cel mai mare număr, de la orice copil la părinte avem relaţia mai mic sau egal)
 +   * min-heap (rădăcina are cel mai mic număr, de la orice copil la părinte avem relaţia mai mare sau egal)
 +
 +=== 2.2 Bucket-uri ===
 +
 +
 +Un pas din algoritmul Radix sort foloseşte o funcţie de indexare. Aceasta prelucrează cheia fiecărui element pentru a decide câte elemente să pună în fiecare sector (bucket).
 +   * Sectoarele pot exista ca vectori independenţi sau ca un singur vector în care marcăm poziţia la care începe fiecare sector.
 +   * Este recomandat ca funcţia de indexare să existe explicit (să fie definită ca subprogram) atunci când are o formă complicată. Dacă are o formă simplă (cum ar fi o singură operaţie), această parte poate fi omisă.
 +
 +=== 2.3 Operații pe biți ===
 +
 +Menţionăm următoarele operaţii pe biţi ce se pot folosi în C/C++ :
 +
 +Operaţii logice
 +   * & şi pe biţi (bitwise AND)
 +   * | sau pe biţi (bitwise OR)
 +   * ^ sau exclusiv pe biţi (bitwise XOR)
 +   * ~ complement pe biţi (bitwise NOT)
 +
 +Deplasări
 +   * >> la dreapta (right shift)
 +   * << la stânga (left shift)
 +
 +Descriem numai operaţiile pe care le vom folosi în cadrul exemplului de mai jos: >> şi &.
 +   * Operaţia n >> k are ca rezultat valoarea obţinută prin mutarea la dreapta a tuturor biţilor lui n (pe primii k biţi se obţine 0, iar ultimii k biţi din n sunt ignoraţi).
 +   * Operaţia n & k are ca rezultat valoarea obţinută prin păstrarea biţilor nenuli din n pentru poziţiile pe care şi k are biţi nenuli (0 în rest)
 +
 +<note important>​Dacă n este număr natural şi k = 2p, atunci:
 +   * n >> p == n / k
 +   * n & (k - 1) == n % k
 +
 +Apar diferenţe în cazul numerelor negative.</​note>​
 +
 +
 +==== 3. Algoritmii de sortare ====
 +
 +=== 3.1 Heap Sort ===
 +   * Timp mediu: O(N log N)
 +   * Timp la limită: O(N log N)
 +   * Memorie: O(1)
 +   * Stabil: NU
 +
 +Descriere :
 +   * Metoda de sortare prin selecţie directă se bazează pe selecţia repetată a ultimei chei dintre n elemente, apoi dintre n-1 elemente rămase, etc.
 +   * Pentru a găsi cea mai mică cheie dintre n elemente sunt necesare n-1 comparaţii,​ apoi găsirea următoarei dintre n-1 elemente are nevoie de n-2 comparaţii,​ etc. ⇒ n(n-1)/2 comparaţii.
 +   * Această sortare se poate îmbunătăţi prin reţinerea, de la fiecare scanare, de mai multă informaţie decât identificarea unui singur element, cel mai mic.
 +   * De exemplu, cu n/2 comparaţii se poate determina cheia mai mică pentru fiecare pereche de elemente dintre cele n elemente, apoi cu alte n/4 comparaţii se poate determina cheia cea mai mică pentru fiecare pereche ale cheilor determinate anterior, şi aşa mai departe. Astfel, cu n-1 comparaţii se poate construi arborele de selecţie.
 +
 +Descriem următorii paşi pentru o variantă de implementare a algoritmului Heap sort (ordonare crescătoare):​
 +   * presupunem că vectorul formează un arbore binar, fiecare poziţie din vector reprezentând un nod, cu rădăcina pe poziţia 0 (zero) şi cu fiecare nod k având copiii 2k+1 şi 2k+2 (dacă nu există poziţia din vector cu indicele respectiv, atunci nu există nod copil ⇒ NULL)
 +   * formăm un max-heap cu aceeaşi reprezentare (pe vector, fără a construi altă structură pentru noduri)
 +   * extragem maximul din rădăcina heap-ului (poziţia 0 din vector) şi facem o intersschimbare între poziţia maximului şi ultima poziţie din vector. Acum maximul se află pe poziţia dorită şi putem să-l excludem din heap.
 +   * repetăm paşii (refacem forma de heap, extragem noul maxim, reducem cu 1 numărul de elemente nesortate), cât timp mai sunt elemente în heap.
 +
 +Definim următoarele două funcţii pentru a prezenta mai usor algoritmul Heap sort:
 +   * funcţia „cerne“ (asigură „cernerea“/​„scurgerea“/​„căderea“ nodului k până poziţia necesară pentru heap) - dacă nodul k nu are valoarea mai mare decât a copiilor lui (nu se păstrează relaţia de ordine a heap-ului), atunci nodul k va fi „cernut“ până va fi respectată relaţia.
 +   * funcţia „makeHeap“ (formează max heap-ul) - funcţia cerne toate nodurile pentru a obţine un heap
 +
 +Implementare:​
 +<code C>
 +void schimba(int a[],int i,int j) //functie auxiliara
 +{
 +    int aux;
 +    aux = a[i];
 +    a[i] = a[j];
 +    a[j] = aux;
 +}
 +void cerne(int a[],int n,int k)
 +{
 +    int fiuStanga = 2 * k + 1, //pozitia primului copil
 +        fiuDreapta = 2 * k + 2,
 +        pozMax = fiuStanga; //pozitia copilului mai mare
 +    if(fiuStanga >= n)
 +        return; //"​nodul"​ k este frunza
 +    if(fiuDreapta < n) {
 +        if(a[fiuDreapta] > a[fiuStanga] ) {
 +            pozMax = fiuDreapta;
 +        }
 +    }//am ales copilul mai mare
 +    if(a[k] < a[pozMax]) {
 +        schimba(a,​k,​pozMax);​ //nodul k este "​cernut"​ - coboara
 +        cerne(a,​n,​pozMax);​ //cernem la noua lui pozitie
 +    }
 +}
 +void makeHeap(int a[],int n) //functia mai e numita "​heapify"​
 +{
 +    //pentru i > n / 2, i este cu siguranta nod frunza
 +    for(int i = n / 2;i >= 0;i--) { //ne asiguram ca exista ordine
 +        cerne(a,​n,​i);​ // pentru orice nod de la i la n-1
 +    }
 +}
 +void heapSort(int a[],int n)
 +{
 +    makeHeap(a,​n);​ //construim un heap
 +    while(n > 1) {
 +        schimba(a,​0,​n-1);​ //mutam maximul pe ultima pozitie
 +        n--; //am asezat un element pe pozitia finala
 +        cerne(a,​n,​0);​ //elementul pus in locul maximului trebuie "​cernut"​
 +        //obtinem din nou forma de heap
 +    }
 + 
 +}
 +</​code>​
 +
 +
 +=== 3.2 Radix Sort ===
 +
 +   * Timp mediu: O(N * k)
 +   * Timp la limită: O(N * k)
 +   * Memorie: O(N + k)
 +   * Stabil: DA
 +k = lungimea cuvântului/​cheii (word size)
 +
 +Vom prezenta varianta LSD (Least Signifiant Digit) a algoritmului de sortare.
 +
 +Descriere:
 +LSD Radix Sort este una dintre cele mai rapide metode de sortare.Aceasta se bazează pe sortarea în funcţie de cea mai nesemnificativă „cifră“.
 +   * Radix Sort nu are la bază o tehnică de comparare. Fiecare element din vector (sau atribut al unui element, în cazul structurilor) după care facem sortarea va fi numit cheie.
 +   * Cheile sunt gândite ca şiruri de „caractere“ (unde un „caracter“ poate fi un bit, o cifra, o literă, …).
 +
 +Algoritmul trece prin următorii paşi:
 +   * pornind de la poziţia celui mai nesemnificativ „caracter“,​ numără de câte ori apare fiecare „caracter“ pe poziţia respectivă,​ apoi împarte un vector auxiliar în secţiuni (imaginare). Numărul de secţiuni este numărul de „caractere“ diferite ce pot exista în vector, adică fiecărei secţiuni îi este asociat un „caracter“,​ dimensiunea unei secţiuni depinde de numărul de apariţii ale „caracterului“ asociat;
 +   * pune fiecare element (în vectorul auxiliar) în secţiunea corespunzătoare,​ apoi copiază vectorul auxiliar înapoi în vectorul ce trebuie sortat. Se obţine un vector sortat până la poziţia curentă;
 +   * trece la poziţia următoare şi repetă paşii, ultima poziţie fiind cea a celui mai semnificativ „caracter“.
 +
 +Implementare:​
 +Exemplul prezentat foloseşte un octet pe post de „caracter“. Putem interpreta acest exemplu şi ca scriere a numerelor în baza 256, valoarea fiecărui octet fiind o „cifră“.
 +
 +<code C>
 +#define BYTE 8
 +#define COUNT_BYTE 256
 +int obtineOctetul(int n,int byteNr)
 +{   //​cautam octetul de la pozitia byteNr
 +    //octetul de pe pozitia 0 este LSD = octetul cel mai din dreapta(pentru int)
 +    int bitsNr =  BYTE * byteNr;
 +    int mask = COUNT_BYTE - 1;
 +    return (n >> bitsNr) & mask;
 +}
 +void rad(int *a,int *b, int byteNr,int n)
 +{   //​sortare dupa octetul de pe pozitia byteNr,
 +    // pe pozitia 0 este LSD = octetul cel mai din dreapta
 +    int i,
 +        count[COUNT_BYTE] = {0}, //numaram cate elemente au "​car."​ i pe pozitia byteNr
 +        index[COUNT_BYTE] = {0}; //pozitia la care vom pune urmatorul element cu "​car."​ i
 +    for(i = 0; i < n;i++) {
 +        int car = obtineOctetul(a[i],​byteNr);​
 +        count[car]++;​
 +    }
 +    for(i = 1;i < COUNT_BYTE;​i++) //sectionam vectorul b
 +        index[i] = index[i-1] + count[i-1];
 +    for(i = 0; i < n; i++) { //umplem sectiunile
 +        int car = obtineOctetul(a[i],​byteNr);​
 +        b[index[car]++] = a[i];
 +    }
 +}
 +void radixSort(int *a,int n)
 +{
 +    int *b = new int[n], //vector folosit la copiere
 +        byteNr, //pozitia curenta
 +        k = sizeof(a[0]);​ //numarul de "​caractere"​
 +    for(byteNr = 0; byteNr < k; byteNr += 2) {
 +        rad(a, b, byteNr, n); //in loc sa copiem b inapoi in a la fiecare pas
 +        rad(b, a, byteNr + 1, n); //copiem doar o data la 2 pasi
 +    }
 +    delete []b;
 +}
 +</​code>​
 +
 +
 +<note important>​Exemplul prezentat funcţionează bine pentru întregi cu acelaşi semn.</​note>​
 +
 +<note important>​Performanţa de timp poate fi influenţată prin schimbarea lungimii cuvântului (k), adică prin schimbarea „bazei“ folosite.</​note>​
 +
 +==== 4. Exerciții ====
 +
 +  - Generaţi un vector de n întregi cu n = 1e6. Sortaţi vectorul cu cei doi algoritmi şi comparaţi rezultatele (aici ne intereseaza viteza sortării, vezi https://​stackoverflow.com/​questions/​5248915/​execution-time-of-c-program). [80% notă]
 +  - Introduceţi o variabilă globală cu care să contorizaţi numărul de apelări ale funcţiei „cerne“. Afişaţi numărul de apelări necesare pentru construirea heap-ului (makeHeap) şi numărul de apelări necesare pentru tot algoritmul (heapSort). [10% notă]
 +  - Cu Radix Sort, încercaţi să sortaţi un vector cu orice numere întregi (pozitive şi negative). Verificaţi rezultatul şi adăugaţi un pas în algoritm pentru a aşeza corect elementele. [10% notă]
 +
 +==== 5. Probleme opționale, de interviu ====
 +
 +  - Se dă un vector cu n întregi, unde toate valorile din vector sunt cuprinse între 0 şi n^2 - 1. Sortaţi vectorul în timp O(n). Hint: încercaţi să folosiţi altă „bază“ decât 256 pentru algoritm.
sda-aa/laboratoare/07.1614547919.txt.gz · Last modified: 2021/03/01 15:21 (external edit)
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0