HeapSort – sorting by heapifying
Transkrypt
HeapSort – sorting by heapifying
Wrocław 26.05.2006 Algorytmy i Struktury Danych – laboratorium (INZ1505L) Autor: Wojciech Podgórski WIZ INF Prowadzący: mgr Marcin Parczewski Sprawozdanie dotyczące testowania algorytmu sortowania. Algorytm: Sortowanie stogowe (HeapSort). Uwaga : Floyd zauważył, że przy rozbieraniu stogu element wstawiany na wierzchołek stogu opada zazwyczaj na samo dno stogu. Zaproponował następujące ulepszenie : po usunięciu największego elementu schodź do dna stogu idąc zawsze w stronę większego z potomków i przesuwając go jednocześnie na wolne miejsce (ojca) , po dojściu do dna wstaw tam ostatni element stogu i przesuwaj go DoGóry (bardzo rzadko zachodzi taka potrzeba). Jakie są efekty tego usprawnienia ? Spis Treści: 1. 2. 3. 4. 5. 6. 7. Opis i charakterystyka algorytmu...................................................................................2 Ciało algorytmu..............................................................................................................3 Usprawnienia i modyfikacje algorytmu..........................................................................4 Wnioski...........................................................................................................................5 Wyniki i pomiary............................................................................................................6 Implementacja w języku C++.........................................................................................9 Bibliografia...................................................................................................................13 1 1. Opis i charakterystyka algorytmu. Algorytm sortowania stogowego, zwany również algorytmem sortowanie przez kopcowanie (Heap Sort) jest jednym z najciekawszych narzędzi sortujących. Opiera się on na drzewiastej strukturze danych zwanej stertą, stogiem lub kopcem. Kopiec jest strukturą o kilku ważnych cechach, o których należy wspomnieć aby wytłumaczyć istotę sortowania stogowego. Własności kopca: • • • wartości potomków danego węzła są w stałej relacji z wartością rodzica (dla kopca typu max wartość rodzica jest zawsze większa od wartości potomka, dla kopca typu min odwrotnie). Jeżeli kopiec ma być kopcem zupełnym, wtedy dodatkowo spełnione muszą być warunki: 1. drzewo jest prawie pełne tzn. liście występują na ostatnim i ewentualnie przedostatnim poziomie w drzewie 2. liście na ostatnim poziomie są spójnie ułożone od strony lewej do prawej. Jeżeli implementujemy kopiec na tablicy i wybieramy węzeł o indeksie i, to jego lewy potomek będzie miał indeks 2i+1 prawy natomiast 2i+2 (w numeracji od 0 do n) Poprawnie zbudowany kopiec: Rys.1 Poprawnie zbudowany kopiec Heap Sort będący algorytmem prawie tak szybkim jak Quick Sort (a w przypadku pesymistycznym, nawet szybszym!), jest praktycznie nie wrażliwy na uporządkowanie danych wejściowych. Jego złożoność obliczeniowa (czasowa) wynosi: • Złożoność optymistyczna: • Złożoność oczekiwana: • Złożoność pesymistyczna: O ( n ⋅ log n) O (n ⋅ log n) O (n ⋅ log n) Taki rodzaj złożoności jest spowodowany wywoływaniem procedur Build-Max-Heap (utwórz kopiec o własności typu max), która jest złożoności jest klasy O(n) oraz Max-Heapify (przywróć danym zawartym w strukturze własność kopca typu max), która jest złożoności O(log n) . Złożoność pamięciowa jest klasy O(1) Heap Sort zawdzięcza to temu, iż jest tzw. algorytmem sortującym w miejscu. Oznacza to, iż w czasie procesu sortowania tylko stała 2 liczba elementów tablicy wejściowej jest przechowywana poza nią. Tak więc algorytmy, które nie działają w miejscu wymagają dodatkowej pamięci. Heap Sort cechuje się również tym, iż jest niestety algorytmem niestabilnym. Oznacza to, iż posiadając 2 lub więcej identycznych wartości, nie sortuje ich w kolejności wejściowej. 2. Ciało algorytmu Po utworzeniu struktury kopca następuje właściwe sortowanie. Polega ono na usunięciu wierzchołka kopca, zwierającego element maksymalny dla typu max (minimalny dla typu min), a następnie wstawieniu w jego miejsce elementu z końca kopca i odtworzenie porządku kopcowego (Max-Heapify). W zwolnione w ten sposób miejsce, zaraz za końcem zmniejszonego kopca wstawia się usunięty element maksymalny. Operacje te powtarza się aż do wyczerpania elementów w kopcu. Wygląd tablicy można tu schematycznie przedstawić następująco: 012… k | k+1 k+2 n ------------------------------------------------------------------------| Kopiec elementów do posortowania | Posortowana tablica | ------------------------------------------------------------------------Tym razem kopiec kurczy się, a tablica przyrasta (od elementu ostatniego do pierwszego). Także, tu złożoność usuwania elementu połączonego z odtwarzaniem porządku kopcowego, ma złożoność logarytmiczną, a zatem złożoność tej fazy to O ( n ⋅ log n ) . Algorytm można również opisać w „pseudo kodzie” w celu implementacji w różnych językach programowania, oto on: Heapsort(A) 1 Build-Max-Heap(A) 2 for i ← lenght[A] downto 2 3 do zamień A[1] ↔ A[i] 4 heap-size[A] ← heap-size[A] – 1 5 Max-Heapify(A, 1) Build-Max-Heap(A) 1 heap-size[A] ← lenght[A] 2 for i ← lenght[A] div 2 downto 1 3 do Max-Heapify(A, i) Max-Heapify(A, i) 1 l ← Left(i) 2 r ← Right(i) 3 if l ≤ heap-size[A] i A[l] > A[i] 4 then largest ← l 5 else largest ← i 6 if r ≤ heap-size[A] i A[r] > A[largest] 7 then largest ← r 8 if largest ≠ i 9 then zamień A[i] ↔ A[largest] 10 Max-Heapify(A, largest) 3 Rysunek ukazujący działanie algorytmu na prostym kopcu 5 elementowym: Rys. 2 Sortowanie przez kopcowanie na kopcu 5-elementowym typu max proszę zauważyć że w kroku A przywraca się porządek kopca 3. Usprawnienia i modyfikacje algorytmu Trudno jest usprawnić algorytm, który i tak jest już bardzo sprawny, jednak wyróżniamy 2 podstawowe modyfikacje: a) Koncepcja Floyda Koncepcja Floyda, polega na zauważeniu, iż element ponownie wstawiony do kopca podczas procesu wysortowywania, przebywa z reguły całą drogę w dół. Możemy więc zaoszczędzić czas unikając sprawdzenia, czy element trafił na pozycję, promując większego z synów do czasu osiągnięcia najniższego poziomu, a potem przesuwając z powrotem w górę kopca na właściwe miejsce. Ten pomysł pozwala zmniejszyć liczbę porównań asymptotycznie dwukrotnie – blisko wartości: lg n!≈ n ⋅ lg n − n ln 2 będącym absolutnym minimum dla liczby porównań algorytmu sortowania. Metoda ta wymaga stosowania dodatkowej ewidencji i jest przydatna w praktyce tylko wtedy gdy koszt porównań jest stosunkowo wysoki(np. gdy sortuje się rekordy z łańcuchami znaków albo z innymi typami długich kluczy). b) Kopiec oparty na tablicowej reprezentacji pełnego drzewa trynarnego (trójkowego) Inna koncepcja usprawnienia polega na zbudowaniu kopca opartego na tablicowej reprezentacji pewnego drzewa trynarnego (trójkowego), 4 uporządkowanego kopcowo, którego węzeł na pozycji k jest większy, lub równy węzłom na pozycjach 3k-1, 3k , 3k+1, jest mniejszy lub równy niż węzeł na pozycji E((k+1)/3) dla pozycji z przedziału od 1 do n w tablicy nelementowej. Mniejszy koszt wypływający z faktu zredukowanej wysokości drzewa, pociąga ze sobą wyższy koszt sprawdzania 3 węzłów potomnych, w każdym rozpatrywanym węźle. Dalsze zwiększanie liczby potomków przypadających na jeden węzeł nie powoduje raczej zwiększenia ogólnej wydajności algorytmu. 4. Wnioski Sortowanie stogowe jest bardzo specyficznym rodzajem sortowania. Z pewnością nie nadaje się do sortowania małych zbiorów danych, ponieważ lepiej wtedy użyć algorytmu QuickSort, który jest szybszy. Wymaga zastosowania określonej struktury danych, co ogranicza jego stosowanie i komplikuje implementację. Niestety, jest również algorytmem niestabilnym co może, w niektórych wypadkach powodować niemożność posortowania danych(w takim przypadku wybiera się inny). Problemem jest również to, iż działając na strukturze statycznej (np. tablicy) pojawia się ograniczenie ilości danych. HeapSort na strukturze dynamicznej jest bardzo trudny do zaimplementowania, choć jego złożoność obliczeniowa jest mniej więcej taka sama (niestety w takiej strukturze złożoność pamięciowa nie jest już stała). HeapSort najlepiej sprawdza się w sortowaniu dużych zbiorów danych, ułożenie danych w strukturę kopca, powoduje zmniejszenie złożoności obliczeniowej wykonywanych operacji do rzędu O (log n) co daje dużą szybkość. Jest również algorytmem bardzo nie wrażliwym na uporządkowanie danych, co czyni go czasem szybszym niż QuickSort. Sortując w miejscu zapewnia sobie złożoność pamięciową rzędu O (1) co jest najlepszym wyborem. Bardzo ważne również jest to, iż w każdym, a nawet najgorszym wypadku jego złożoność obliczeniowa jest zaledwie liniowo-logarytmiczna. Wady: - niestabilność algorytmu - wymagana określona struktura danych - ograniczony rozmiar zbioru danych (struktura statyczna). - stosunkowo trudna implementacja - dla małych zbiorów danych wolniejszy od QuickSort, ale szybszy niż MergeSort! Zalety: - duża szybkość - bardzo mała wrażliwość na uporządkowanie - dla dużych zbiorów danych, szybszy nawet od QuickSort - wszystkie złożoności obliczeniowe (optymistyczna, pesymistyczna, oczekiwana) takie same i klasy O(n ⋅ log n) - złożoność pamięciowa jest stała O(1) - sortowanie w miejscu 5 5. Wyniki i pomiary Testy dokonywane były na komputerze o parametrach: • • • Procesor – AMD Athlon™ 64 Processor 3000+ Płyta Główna – ASUS K8V-X Pamięć RAM: 512 MB DDR2 Program został skompilowany pod Bloodshed Software Dev-C++ 4.9.9.2 korzystającego z kompilatora MinGW bez optymalizacji. Długość Czas[ms] Porównań Przypisań Uporządkowanie 10 50 100 500 1000 5000 10000 50000 0 78 156 1125 2547 15906 34985 207922 58 602 1491 10835 24719 157355 344925 2080397 116 954 2246 15214 33945 209189 453730 2681608 malejace malejace malejace malejace malejace malejace malejace malejace Długość Czas[ms] Porównań Przypisań Uporządkowanie 10 50 100 500 1000 5000 10000 50000 0 78 172 1172 2641 16328 32250 214235 62 642 1535 11331 25895 163735 357407 2137401 112 1014 2328 15786 35317 216773 468173 2749728 malejace malejace malejace malejace malejace malejace malejace malejace Długość Czas[ms] Porównań Przypisań Uporządkowanie 10 50 100 500 1000 5000 10000 50000 15 78 172 1203 2750 16985 36844 214641 62 678 1665 11733 26357 167049 364099 2171443 124 1052 2479 16297 36092 221466 477887 2798593 malejace malejace malejace malejace malejace malejace malejace malejace Długość Czas[ms] Porównań Przypisań Uporządkowanie 10 50 100 0 78 187 76 718 1757 143 1118 2615 rosnace rosnace rosnace Rozrzut [%] 5 5 5 5 5 5 5 5 Rozrzut [%] 50 50 50 50 50 50 50 50 Rozrzut [%] 100 100 100 100 100 100 100 100 Rozrzut [%] 5 5 5 6 500 1000 5000 10000 50000 1281 2875 17312 38172 226094 12455 27885 170293 376905 2228683 17312 38010 226566 496394 2879485 rosnace rosnace rosnace rosnace rosnace Długość Czas[ms] Porównań Przypisań Uporządkowanie 10 50 100 500 1000 5000 10000 50000 16 78 171 1265 3000 16875 36829 217234 72 676 1677 11967 27019 166221 366091 2181809 135 1069 2517 16724 36983 220968 481735 2816948 rosnace rosnace rosnace rosnace rosnace rosnace rosnace rosnace Długość Czas[ms] Porównań Przypisań Uporządkowanie 10 50 100 500 1000 5000 10000 50000 0 78 172 1203 2687 16953 36328 220156 70 674 1629 11719 26443 167195 364321 2171563 126 1064 2431 16282 36123 221671 478220 2799039 rosnace rosnace rosnace rosnace rosnace rosnace rosnace rosnace Długość Czas[ms] Porównań Przypisań Uporządkowanie 10 50 100 500 1000 5000 10000 50000 0 47 78 422 859 4547 9562 59360 40 242 553 3411 6943 38367 80755 528423 83 484 1061 6078 12209 65029 134993 824811 stale stale stale stale stale stale stale stale Długość Czas[ms] Porównań Przypisań Uporządkowanie 10 50 100 500 1000 5000 10000 50000 0 78 156 1078 2360 14562 31203 200062 60 618 1395 10221 22631 142359 309917 2001465 117 980 2152 14459 31417 191947 412352 2599158 stale stale stale stale stale stale stale stale Długość Czas[ms] Porównań Przypisań Uporządkowanie 10 16 70 128 stale 5 5 5 5 5 Rozrzut [%] 50 50 50 50 50 50 50 50 Rozrzut [%] 100 100 100 100 100 100 100 100 Rozrzut [%] 5 5 5 5 5 5 5 5 Rozrzut [%] 50 50 50 50 50 50 50 50 Rozrzut [%] 100 7 50 100 500 1000 5000 10000 50000 78 172 1219 2703 16860 36875 214172 688 1627 11789 26483 166979 364799 2171715 1055 2432 16307 36055 221355 478481 2798431 stale stale stale stale stale stale stale 100 100 100 100 100 100 100 Wykresy: Sortowanie HeapSort dla uporządkowania malejącaego z rozrzutem 5% 3000000 2500000 2000000 czas[ms] 1500000 porownan 1000000 przepisan 500000 50 00 0 50 00 10 00 0 10 00 50 0 10 0 50 10 0 Sortowanie HepaSort dla uporządkowania rosnącego z rozrzutem 50% 3000000 2500000 2000000 czas[ms] 1500000 porownan 1000000 przepisan 500000 50 00 0 50 00 10 00 0 10 00 50 0 10 0 50 10 0 8 Sortowanie HeapSort dla uporządkowania stałego z rozrzutem 100% 3000000 2500000 2000000 czas[ms] 1500000 porownan 1000000 przepisan 500000 50 00 0 50 00 10 00 0 10 00 50 0 10 0 50 10 0 6. Implementacja w języku C++ 1. sorttab.cpp #include "sorttab.h" typedef int FunLos(int &w,int &nie,int i); int n; int wszystkie,niepopr; long lpor,lprzep; FunLos *losuj; int ciag,proc; clock_t pocz,kon; long czas; ofstream f; //plik z wynikami bool otwarty=false; int losujros(int &w,int &nie,int i) { if (rand()/(float)RAND_MAX <(float)nie/w--) {nie--; return rand()*n/RAND_MAX;} return i; } int losujst(int &w,int &nie,int i) { if (rand()/(float)RAND_MAX <(float)nie/w--) {nie--; return rand()*2*n/RAND_MAX;} return n; } int losujmalej(int &w,int &nie,int i) { if (rand()/(float)RAND_MAX<(float)nie/w--) {nie--; return rand()*n/RAND_MAX;} return w; } void ustawparam() { cout<<"SORTOWANIE TABLICY "<<endl; 9 cout<<"Ile elementow : "; cin >>n; cout<<"Jaki ciag: 1-rosnacy, 2-malejacy, 3-staly "; cin>>ciag; cout<< "Podaj procent losowych elementow "; cin >>proc; wszystkie=n;niepopr=(long)n*proc/100; cout << "Uporzadkowanie "; switch (ciag) { case 1:{ cout<<"rosnace ";losuj=losujros;break; } case 2:{ cout<<"malejace ";losuj=losujmalej;break; } case 3:{ cout<<"stale ";losuj=losujst; } } cout <<"rozrzut "<<proc<<'%'<<endl; } int ileelem() { return n;} void generuj(Tab &tab) { tab = new int[n]; for(int i=0;i<n;i++) tab[i]=losuj(wszystkie,niepopr,i+1); } void kopiuj(Tab tab1,Tab &tab2) { tab2= new int[n]; for (int i=0; i<n;i++) tab2[i]=tab1[i]; } void druk() { cout << "\rPorownan : "<<setw(8)<<lpor <<" przepisan "<<setw(8)<<lprzep;} void porown(long k) { lpor+=k; druk();} void przep(long k) { lprzep+=k;druk();} void start() { lprzep=0; lpor=0; pocz=clock();} void stop() { kon=clock(); czas=(kon-pocz)*1000/CLOCKS_PER_SEC;} void otworzplik() //przygotowuje plik do zapisu wynikow { char nazwa[200]; cout<< "Podaj nazwe pliku "; cin >>nazwa; f.open(nazwa,ios::app); f.seekp(0,ios::end); if (!f.tellp()) {f<<"dlugosc czas[ms] porownan przepisan uporzadkowanie rozrzut [%]"<<endl;} otwarty=true; } void dopliku() { f<<setw(6)<<n<<setw(10)<<czas<<setw(9)<<lpor<<setw(10)<<lprzep<<" switch (ciag) { case 1: f<<" rosnace ";break; case 2: f<<" malejace ";break; "; 10 case 3: f<<" stale } f<<setw(8)<<proc<<endl; "; } void zamknijplik() { f.close(); otwarty = false;} void test(Tab tab) { int i; for(i=0;i<n-1 && tab[i] <= tab[i+1]; i++); cout <<" CZAS :"<<setw(9)<<czas<<" ms "; if (i==n-1) { cout<<" SORT "<<con::fg_green<<" OK "<<con::fg_white<<endl; if (otwarty) dopliku(); } else cout<<con::fg_red<<" BLAD "<<con::fg_white<<" SORT "<<endl; } void zwolnij(Tab &tab) { delete [] tab ;tab=NULL; } 2. sorttab.h #ifndef _sorttab_h_ #define _sorttab_h_ #include #include #include #include #include #include <fstream> <iostream> <iomanip> <ctime> <cstdlib> "Console.h" namespace con = JadedHoboConsole; using namespace std; typedef int *Tab; void int void void void void void void void void void void ustawparam(); //dlug, rodzaj ciagu, losowosc ileelem(); //dlugosc ciagu generuj(Tab &tab); //wypelnia tablice kopiuj(Tab tab1,Tab &tab2); //tworzy kopie dla drugiego alg porown(long k); //dolicza i wypisuje porownania przep(long k); //dolicza i wypisuje przepisania start(); //start stopera stop(); //stop stopera otworzplik(); //przygotowuje plik do zapisu wynikow zamknijplik(); test(Tab tab); //sprawdza poprawnosc sortowania + zapis zwolnij(Tab &tab); //zwalnia pamiec tablicy #endif 11 3. HeapSort.cpp #include "sorttab.h" using namespace std; // Zmienne globalne Tab tab, tab1; // Funkcje void przywroc(int T[], int k, int n) // "Max-Heapify" funkcja przywracajaca tablicy wlasnosc kopca { int i,j; i = T[k-1]; przep(1); while (k <= n/2) { porown(1); j=2*k; przep(1); if((j<n) && ( T[j-1]<T[j]) ) { porown(2); j++; przep(1); } if (i >= T[j-1]) { porown(1); break; } else { porown(1); T[k-1] = T[j-1]; k=j; przep(2); } } T[k-1]=i; przep(1); } void heapSort(int T[], int n) // algorytm sortowania przez kopcowanie, Heap Sort { int k, swap; przep(1); for(k=n/2; k>0; k--) // zbuduj kopiec { porown(1); przep(1); przywroc(T, k, n); } do // sortuj - rozbieraj kopiec i przywracaj mu własność kopca po każdym zdjętym elemencie. { 12 porown(1); swap=T[0]; T[0]=T[n-1]; T[n-1]=swap; n--; przep(4); przywroc(T, 1, n); } while (n > 1); } int main() { cout << con::fg_white <<"\t\t\t Heap Sort v. 1.0\n\n\n"; long int a=0; cout << "Ile testow chcesz przeprowadzic?:"; cin >> a; otworzplik(); for (int j=0; j<a; j++) { unsigned int seed=time(NULL); cout <<seed<<endl; srand(seed); // losowy; srand(0) - powtarzalny ustawparam(); //wybor rodzaju ciagu i jego dlugosci generuj(tab); //utworzenie tablicy //for(int i =0; i<ileelem(); i++) cout<<tab[i]<<' '; cout << endl; start(); //start pomiaru czasu heapSort(tab, ileelem()); stop(); //zatrzymanie stopera test(tab); //sprawdzenie poprawnosci sortowania i zapis do pliku zwolnij(tab); // zwolnienie pamieci cout << "\n\n"; } zamknijplik(); cout << con::bg_red <<"\n\nAutor: Wojciech Podgorski\n\n" << con::bg_black; system("pause"); } 7. Bibliografia 1. Cormen T., Leiserson C. E., Rivest R.L., Wprowadzenie do algorytmów, WNT Warszawa 1997. 2. Wróblewski P., Algorytmy, struktury danych i techniki programowania, Helion, Gliwice 1996. 3. Sedgewick R., Algorytmy w C++, Addison, Wesley, RM, Warszawa 1999. 13