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. Opis i charakterystyka algorytmu...2 2. Ciało algorytmu...3 3. Usprawnienia i modyfikacje algorytmu...4 4. Wnioski...5 5. Wyniki i pomiary...6 6. Implementacja w języku C++...9 7. 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: O( n log n) Złożoność oczekiwana: O( n log n) Złożoność pesymistyczna: 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: 0 1 2 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) Max-Heapify(A, i) 1 heap-size[a] lenght[a] 1 l Left(i) 2 for i lenght[a] div 2 downto 1 2 r Right(i) 3 do Max-Heapify(A, 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
4. Wnioski 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 n- elementowej. 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. 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. 10 0 58 116 malejace 5 50 78 602 954 malejace 5 100 156 1491 2246 malejace 5 500 1125 10835 15214 malejace 5 1000 2547 24719 33945 malejace 5 5000 15906 157355 209189 malejace 5 10000 34985 344925 453730 malejace 5 50000 207922 2080397 2681608 malejace 5 10 0 62 112 malejace 50 50 78 642 1014 malejace 50 100 172 1535 2328 malejace 50 500 1172 11331 15786 malejace 50 1000 2641 25895 35317 malejace 50 5000 16328 163735 216773 malejace 50 10000 32250 357407 468173 malejace 50 50000 214235 2137401 2749728 malejace 50 10 15 62 124 malejace 100 50 78 678 1052 malejace 100 100 172 1665 2479 malejace 100 500 1203 11733 16297 malejace 100 1000 2750 26357 36092 malejace 100 5000 16985 167049 221466 malejace 100 10000 36844 364099 477887 malejace 100 50000 214641 2171443 2798593 malejace 100 10 0 76 143 rosnace 5 50 78 718 1118 rosnace 5 100 187 1757 2615 rosnace 5 6
500 1281 12455 17312 rosnace 5 1000 2875 27885 38010 rosnace 5 5000 17312 170293 226566 rosnace 5 10000 38172 376905 496394 rosnace 5 50000 226094 2228683 2879485 rosnace 5 10 16 72 135 rosnace 50 50 78 676 1069 rosnace 50 100 171 1677 2517 rosnace 50 500 1265 11967 16724 rosnace 50 1000 3000 27019 36983 rosnace 50 5000 16875 166221 220968 rosnace 50 10000 36829 366091 481735 rosnace 50 50000 217234 2181809 2816948 rosnace 50 10 0 70 126 rosnace 100 50 78 674 1064 rosnace 100 100 172 1629 2431 rosnace 100 500 1203 11719 16282 rosnace 100 1000 2687 26443 36123 rosnace 100 5000 16953 167195 221671 rosnace 100 10000 36328 364321 478220 rosnace 100 50000 220156 2171563 2799039 rosnace 100 10 0 40 83 stale 5 50 47 242 484 stale 5 100 78 553 1061 stale 5 500 422 3411 6078 stale 5 1000 859 6943 12209 stale 5 5000 4547 38367 65029 stale 5 10000 9562 80755 134993 stale 5 50000 59360 528423 824811 stale 5 10 0 60 117 stale 50 50 78 618 980 stale 50 100 156 1395 2152 stale 50 500 1078 10221 14459 stale 50 1000 2360 22631 31417 stale 50 5000 14562 142359 191947 stale 50 10000 31203 309917 412352 stale 50 50000 200062 2001465 2599158 stale 50 10 16 70 128 stale 100 7
50 78 688 1055 stale 100 100 172 1627 2432 stale 100 500 1219 11789 16307 stale 100 1000 2703 26483 36055 stale 100 5000 16860 166979 221355 stale 100 10000 36875 364799 478481 stale 100 50000 214172 2171715 2798431 stale 100 Wykresy: Sortowanie HeapSort dla uporządkowania malejącaego z rozrzutem 5% 3000000 2500000 2000000 czas[ms] 1500000 porownan 1000000 przepisan 500000 0 10 50 100 500 1000 5000 10000 50000 Sortowanie HepaSort dla uporządkowania rosnącego z rozrzutem 50% 3000000 2500000 2000000 czas[ms] 1500000 porownan 1000000 przepisan 500000 0 10 50 100 500 1000 5000 10000 50000 8
Sortowanie HeapSort dla uporządkowania stałego z rozrzutem 100% 3000000 2500000 2000000 czas[ms] 1500000 porownan 1000000 przepisan 500000 0 10 50 100 500 1000 5000 10000 50000 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 <fstream> #include <iostream> #include <iomanip> #include <ctime> #include <cstdlib> #include "Console.h" namespace con = JadedHoboConsole; using namespace std; typedef int *Tab; void ustawparam(); //dlug, rodzaj ciagu, losowosc int ileelem(); //dlugosc ciagu void generuj(tab &tab); //wypelnia tablice void kopiuj(tab tab1,tab &tab2); //tworzy kopie dla drugiego alg void porown(long k); //dolicza i wypisuje porownania void przep(long k); //dolicza i wypisuje przepisania void start(); //start stopera void stop(); //stop stopera void otworzplik(); //przygotowuje plik do zapisu wynikow void zamknijplik(); void test(tab tab); //sprawdza poprawnosc sortowania + zapis void zwolnij(tab &tab); //zwalnia pamiec tablicy #endif 11
3. HeapSort.cpp #include "sorttab.h" using namespace std; // Zmienne globalne // Funkcje Tab tab, tab1; 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