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

Podobne dokumenty