Wewnętrzne Zbiór do posortowania mieści się w pamięci Zewnętrzne Zbiór do posortowania mieści się w pamięci zewnętrznej, np.
Download
Report
Transcript Wewnętrzne Zbiór do posortowania mieści się w pamięci Zewnętrzne Zbiór do posortowania mieści się w pamięci zewnętrznej, np.
Wewnętrzne
Zbiór do posortowania
mieści się w pamięci
Zewnętrzne
Zbiór do posortowania
mieści się w pamięci
zewnętrznej, np. na
dyskach (wykorzystuje się
tylko stałą – małą – ilość
pamięci wewnętrznej.
Rodzaje
sortowania
(wg pamięci)
Adaptacyjne
Wykonuje się różne
sekwencje operacji dla
różnych układów
danych
Nieadaptacyjne
Sekwencja
wykonywanych operacji
nie zależy od
kolejności danych.
Rodzaje
sortowania
(wg operacji)
Parametry wydajnościowe sortowania:
→czas działania algorytmu
→ilość dodatkowej pamięci zużywanej przez algorytm
Wykorzystuje tyle
miejsca, ile
potrzeba na
zapisanie
sortowanych
danych + mały stos
lub tablica
Używa reprezentacji w
postaci listy połączonej
albo innego sposobu
pośredniego dostępu
do danych, czyli
wymaga dodatkowej
pamięci na n
wskaźników lub
indeksów
Wymaga
dodatkowej
pamięci na pełną
kopię sortowanych
danych
Definicja.
Sortowanie jest stabilne, jeśli zachowuje względną kolejność elementów
o jednakowych kluczach.
Przykład.
Lista studentów uporządkowana alfabetycznie według nazwisk. Jeśli
chcemy ją posortować wg ocen, to studenci mający taką samą ocenę
nadal będą w liście ułożeni alfabetycznie.
Za operację dominującą będziemy przyjmować porównanie elementów w
ciągu.
Za złożoność pamięciową S(n) będziemy przyjmować ilość dodatkowej
pamięci (oprócz n miejsc pamięci dla elementów w ciągu), potrzebnej
do wykonania algorytmu.
Zakładamy też, że elementy do posortowania są liczbami całkowitymi
umieszczonymi w tablicy – dla ułatwienia.
Idea:
Wyznaczamy najmniejszy element w ciągu (tablicy) i zamieniamy go
miejscami z elementem pierwszym, następnie z pozostałego ciągu
wybieramy element najmniejszy i ustawiamy go na drugie miejsce
tablicy (zmieniamy), itd.
Realizacja w C++
void selekcja (int a[],int l, int r)
{
for (int i=l; i<r; i++)
{
int min=i;
for (int j=i+1; j<=r; j++)
if (a[j]<a[min]) min=j;
zamiana(a[i],a[min]);
}
}
//realizacja funkcji zamiana
//przestawiajacej dwa elementy
//dowolnego typu
void zamiana(int &A, int &B)
{
int t=A;
A=B;
B=t;
}
Typ może być dowolny
Przykład:
S
E
L
E
K
C
J
A
7 porównań
A
E
L
E
K
C
J
S
6 porównań
A
C
L
E
K
E
J
S
5 porównań
A
C
E
L
K
E
J
S
4 porównania
A
C
E
E
K
L
J
S
3 porównania
A
C
E
E
J
K
L
S
2 porównania
A
C
E
E
J
K
L
S
1 porównanie
Analiza:
Załóżmy, że l=0 i r=n-1.
W linii pierwszej przykładu mamy n-1 porównań a[j]<a[min], potem w
kolejnych liniach: n-2, n-3, ….a na końcu tylko 1 porównanie. Zatem:
( n 1) ( n 2 ) ... 2 1
1 ( n 1 )
2
( n 1)
n ( n 1 )
2
1
2
n
2
O (n)
Tmax(n)
p n ,k
1
n ( n 1 )
2
2
n ( n 1)
, k 1, 2 , ,
n ( n 1 )
2
Stąd:
k p n ,k
k 1
2 n ( n 1 )
4
n ( n 1 )
2
P-stwo, że w każdym z
porównań znajdziemy
element najmniejszy jest
jednakowe
n ( n 1 )
2
k
n ( n 1 )
2
2
n ( n 1 )
k 1
2
n ( n 1 )
k
2
n ( n 1 )
k 1
12 14 n 14 n 14 n O ( n )
2
2
1
n ( n 1 )
2
2
n ( n 1 )
2
Policzmy teraz pesymistyczną wrażliwość tego algorytmu. Przypomnijmy, że
Ponieważ w procedurze zawsze jest wykonywany ten sam ciąg operacji,
niezależnie od danych wejściowych, to Δ(n)=0.
Obliczmy na koniec miarę wrażliwości oczekiwanej algorytmu.
(n) 0
w procedurze zawsze jest wykonywany ten sam ciąg
operacji, niezależnie od danych wejściowych
Ponadto S(n)=O(1)
Mówimy, że algorytm sortuje w miejscu
Zalety:
a) Liczba zamian w najgorszym przypadku: n-1.
b) Prostota implementacji.
c) Zadowalająca szybkość dla małych wartości n.
d) Nie wymaga dodatkowej pamięci.
Wady:
a) Nie jest stabilny.
b) Ma dużą złożoność (rzędu kwadratowego), więc nie nadaje się do
sortowania długich tablic.
c) Jest mało wrażliwy na wstępne uporządkowanie.
Algorytm można uczynić stabilnym, zwiększając współczynnik
proporcjonalności złożoności.
Idea:
W i-tym kroku trzeba wstawić element tab[i] na właściwe miejsce w
posortowanym fragmencie tab[0]…tab[i-1], wcześniej przesuwając
wszystkie elementy większe od niego w tym fragmencie w prawo o 1;
powstaje posortowany fragment tab[0]…tab[i+1].
Realizacja w C++
void InsertSort(int *tab)
{
for(int i=1; i<n;i++)
{
int j=i; // 0..i-1 jest już posortowane
int temp=tab[j];
while ((j>0) && (tab[j-1]>temp))
{
tab[j]=tab[j-1];
j--;
}
tab[j]=temp;
}
}
W
S
T
A
W
I
A
N
I
E
1 porównanie
S
W
T
A
W
I
A
N
I
E
<=2 porównania
S
T
W
A
W
I
A
N
I
E
<=3 porównania
A
S
T
W
W
I
A
N
I
E
<= 4 porównania
A
S
T
W
W
I
A
N
I
E
<=5 porównań
A
I
S
T
W
W
A
N
I
E
…….
A
A
I
S
T
W
W
N
I
E
…….
A
A
I
N
S
T
W
W
I
E
…….
A
A
I
N
N
S
T
W
W
E
<=9 porównań
A
A
E
I
N
N
S
T
W
W
GOTOWE
Analiza:
W linii pierwszej mamy 1 porównanie, potem maksymalnie 2, itd. , aż do
maksymalnie n-1 porównań na końcu. Zatem możemy policzyć
pesymistyczną złożoność :
T max ( n ) 1 2 ( n 2 ) ( n 1)
n ( n 1 )
2
(n )
2
Ponieważ element tab[i] z równym prawdopodobieństwem może
zająć każdą z i-tej pozycji w ciągu tab[0]<tab[1]<…<tab[i-1], to w
i-tym kroku mamy pij=1/i, czyli
i
T sr ( i )
i
j p ij
j 1
1
i
j 1i (1 2 ... i )
1 1 i
i 2
i
1 i
2
.
j 1
Sumując teraz po wszystkich n-1 iteracjach, dostajemy:
n 1
T sr ( n )
i 1
n 1
T sr ( i )
i 1
1 2n
2 2
n 1
i 1
2
1
2
n
( i 1)
i 1
( n 1) ( n ).
2
1
2
k 2
k
1
2
( 2 3 ... n )
Policzmy teraz pesymistyczną wrażliwość tego algorytmu. Przypomnijmy, że
Jest to zatem kres górny zbioru liczb, które powstają jako różnice
ilości operacji dominujących. Zatem od liczby największej z
możliwych należy odjąć najmniejszą z możliwych, żeby otrzymać
taki kres górny.
Ponieważ najmniejszą ilością porównań w każdym kroku n-1iteracji
jest jedno porównanie, a największa ilość wyrażą się obliczoną
właśnie Tmax(n)=n(n-1)/2 to
= =n(n-1)/2-(n-1)=Θ(n2).
Pesymistyczna wrażliwość złożoności czasowej jest zatem duża i
możemy się spodziewać dużej zmienności złożoności
obliczeniowej.
Średnia wrażliwość (czyli miara wrażliwości oczekiwanej): w i-tym kroku
mamy:
(i )
2
( j T śr ( i )) p ij
2
j 1 ... i
(j
1 i
2
)
2 1
i
j 1 ... i
1
4i
( 2 j i 1)
2
j 1 ... i
Sumując po wszystkich n-1 iteracjach, dostajemy:
(n)
i 1 ... n 1
2
(i )
1
4i
i 1 ... n 1
( 2 j i 1) ...
j 1 ... i
2
1
6
3
2
n ( n ).
Zalety:
a) Stabilność.
b) Średnio algorytm jest 2 razy szybszy niż algorytm sortowania przez
selekcję.
c) Optymalny dla ciągów prawie posortowanych.
d) Nie wymaga dodatkowej pamięci.
Udoskonalenia:
•Można przestać porównywać elementy, napotkawszy element, który jest
nie większy niż wstawiany, bo podtablica z lewej strony jest posortowana
– sortowanie adaptacyjne.
•W pierwszej pętli „for” wyznaczamy element najmniejszy i umieszczamy
go na początku tablicy, następnie sortujemy pozostałe elementy.
•Standardowo sortuje się zamiany elementów, ale można zrobić
przeniesienie większych elementów o jedną pozycję w prawo.
Ma prosty zapis.
Na czym polega to sortowanie? Przykład 7-elementowej tablicy.
Element zacieniowany w
pojedynczym przebiegu
głównej pętli „ulatuje” do
góry jako najlżejszy.
Tablica jest przemiatana
od dołu do góry (pętla i) i
analizowane są dwa
sąsiadujące ze sobą
elementy (pętla j); jeśli nie
są uporządkowane, to
następuje ich zamiana.
Implementacja w C++
void bubble(int *tab)
{
for (int i=1;i<n;i++)
for (int j=n-1;j>=i;j--)
if (tab[j]<tab[j-1])
{//swap
int tmp=tab[j-1];
tab[j-1]=tab[j];
tab[j]=tmp;
}
}
Analiza:
Algorytm jest
klasy O(n2)
•Dość często zdarzają się puste przebiegi(nie jest dokonywana żadna wymiana,
bo elementy są posortowane).
•Algorytm jest bardzo wrażliwy na konfigurację danych:
4,2,6,18,20,39,40 – wymaga jednej zamiany
4,6,18,20,39,40,2 – wymaga szesściu zamian
Ulepszenia: przyśpieszają, choć nie zmieniają klasy.
•Można zapamiętać indeks ostatniej zamiany (walka z pustymi przebiegami).
•Można przełączać kierunki przeglądania tablicy (walka z niekorzystnymi
konfiguracjami danych).
void ShakerSort(int *tab)
void bubble(int *tab)
{
for (int i=1;i<n;i++)
for (int j=n-1;j>=i;j--)
if (tab[j]<tab[j-1])
{//swap
int tmp=tab[j-1];
tab[j-1]=tab[j];
tab[j]=tmp;
}
}
Algorytm poprawiony – sortowania
przez wstrząsanie.
{
int left=1,right=n-1,k=n-1;
do
{
for(int j=right; j>=left; j--)
if(tab[j-1]>tab[j])
{
swap(tab[j-1],tab[j]);
k=j;
}
left=k+1;
for(j=left; j<=right; j++)
if(tab[j-1]>tab[j])
{
swap(tab[j-1],tab[j]);
k=j;
}
right=k-1;
}
while (left<=right);
}
Idea:
Jest to również metoda „dziel i rządź”,
ponieważ dzieli tablicę na dwie części, które
potem sortuje niezależnie.
Algorytm składa się z dwóch kroków:
Krok 1: procedura rozdzielania elementów
tablicy względem wartości pewnej komórki
tablicy służącej za oś podziału; proces
sortowania jest dokonywany przez tę
właśnie procedurę.
Krok 2: procedura służąca do właściwego
sortowania, która nie robi w zasadzie nic
oprócz wywoływania samej siebie; zapewnia
poskładanie wyników cząstkowych i w
konsekwencji posortowanie całej tablicy.
Sednem metody jest proces
podziału, który zmienia
kolejność elementów w tablicy
tak, że spełnione są trzy
warunki:
• element a[i] znajduje się dla
pewnego i na właściwej pozycji
w tablicy;
•Żaden z elementów a[l], …,
a[i-1] nie jest większy niż
a[i];
•Żaden z elementów a[i+1],
…, a[r] nie jest mniejszy niż
a[i].
W kółku mamy element
rozgraniczający, elementy mniejsze są
na lewo, a większe na prawo.
Oś podziału
Implementacja funkcji partition w C++. (W nagłówku funkcji tab jest adresem
pierwszego elementu tablicy A do posortowania, l i r określają odpowiednio początek i koniec
podtablicy A, która będzie dzielona przez funkcję partition.)
War.
int partition (int *tab, int l, int r)
//α: 1<=l<r.
początkowy
{
int i=l-1, j=r, v=*(tab+r), s;
for(;;)
{
i++;
while (*(tab+i)<v) i++;
j--;
while (v<=*(tab+j))
{
if(j==1) break;
j--;
}
if (j==i) break;
s=*(tab+i);
*(tab+i)=*(tab+j);
zamiana
*(tab+j)=s;
}
s=*(tab+i);
*(tab+i)=*(tab+r);
*(tab+r)=s;
return i;
War.
}
końcowy
//γ: A[k]<v<=A[i] dla k:=1,2,…,i-1.
//δ: A[k]<v<=A[i] dla k:=l,l+1,…i-1
A[j]<v<=A[m] dla m:=j+1,j+2,…, r-1
//μ: A[k]<v dla k:=l,l+1,…i v<=A[m]
dla m:=j,j+1,…, r-1
//η: A[k]<v dla k:=l,l+1,…i-1
v<=A[m] dla m:=i,i+1,…, r-1 bo j=i
//β: A[k]<A[i]<=A[m] dla
k:=l,l+1,…i-1 m:=i+1,i+2,…,r
Przeprowadzimy dowód semantycznej poprawności tego algorytmu,
stosując metodę niezmienników.
Warunek γ ma miejsce, bo poprzedzająca go instrukcja „while” zakończy
się dla i, przy którym v<=A[i], pozostałe nierówności są wynikiem
działania tej pętli. Jeśli i=1, wtedy pozostałe nierówności nie wystąpią.
Następny warunek δ jest uzupełnieniem γ o podobne jak w warunku γ
nierówności A[j]<v<=A[m] dla m:=j+1,j+2,…,r-1, których uzasadnienie
przeprowadzamy w oparciu o pętlę „while” ustalającą j. Jeżeli i<j, to
dokonujemy zamiany elementu j-tego z i-tym, co pociąga za sobą
zajście μ wobec δ i przejście do następnego przebiegu pętli „for”.
Jeżeli j=i, to wychodzimy z pętli „for” nie dokonując zamiany j-tego
elementu z i-tym, dlatego na wyjściu z pętli „for” ma miejsce warunek η
będący warunkiem δ zapisanym dla j=i. Po dokonaniu zamiany
elementu i-tego z r-tym analogicznie jak μ był konsekwencją δ,
warunek końcowy β jest konsekwencją η.
Uzasadnienie, że γ, δ, η, μ, β są niezmiennikami algorytmu partition
pozwala na stwierdzenie o indeksie i będącym wartością funkcji
partition: A[i] jest na właściwym miejscu w tablicy uporządkowanej,
elementy tablicy od A[l] do A[i-1] są mniejsze od A[i], elementy A[i+1]
do A[r] są większe lub równe A[i]. Zatem algorytm partition jest
częściowo poprawny.
Dobra określoność algorytmu jest oczywista.
Posiadanie własności stopu wynika z obserwacji, że obie pętle „while”
zawierają liczniki i oraz j, pierwszy rosnący, ograniczony z góry przez
r, drugi malejący ograniczony z dołu przez 1.
Pętla „for” będzie skończona ponieważ dla tak określonych liczników
zawsze zajdzie warunek i==j.
Realizacja w C++
void quicksort (int *tab, int l,
{
if(r<=l) return;
int i=partition(tab, l, r);
quicksort(tab, l, i-1);
quicksort(tab, i+1, r);
}
int r)
//1-instrukcja
//2-instrukcja
//3-instrukcja
//4-instrukcja
Właściwe
sortowanie
Dowód poprawności:
Własność: Dla dowolnej liczności n=r-l sortowanej tablicy algorytm jest
semantycznie poprawny.
Dowód:
Krok1.
Algorytm jest semantycznie poprawny dla n=0. Istotnie, wtedy r=l, a
zatem r<=l i z postaci 1-instrukcji wynika, że tablica jednoelementowa
nie ulegnie zmianie, pozostanie tablicą uporządkowaną. Oznacza to
semantyczną poprawność dla n=0.
Krok 2.
Ma miejsce następujące twierdzenie: jeżeli algorytm jest semantycznie
poprawny dla dowolnych n<=k, gdzie k jest dowolną ustaloną liczbą
naturalną nieujemną, to jest semantycznie poprawny dla n=k+1.
Istotnie. Zauważmy, że 1<=k+1, zatem dla n=r-l=k+1 spełniony jest
warunek początkowy α algorytmu partition i program wykona poprawnie
instrukcję 1 i 2.
Ponieważ l<=i<=r, zatem i-1-l<=r-l-1<=k oraz r-(i+1)=r-i-1<=r-l1<=k i na mocy założenia indukcyjnego wywołania algorytmu quicksort
w instrukcji 3 i 4 tego algorytmu wykonają się poprawnie, a zatem
wykona się poprawnie cały algorytm.
Tablica będzie uporządkowana, ponieważ instrukcja 3 poprawnie
uporządkuje elementy tablicy od od l-tego do i-1-szego, i-ty jest na
właściwej pozycji wobec poprawności algorytmu podział , a instrukcja 4
uporządkuje poprawnie elementy tablicy od i+1-szego do r-tego, co
kończy dowód twierdzenia w kroku 2.
Wobec spełnienia obu kroków na mocy zasady indukcji matematycznej
ma miejsce dowodzona własność.
Zależy od tego, czy podziały są zrównoważone, czy nie, a to z kolei
zależy od tego, które elementy zostaną wybrane do dzielenia.
Podziały
zrównoważone
• Algorytm asymptotycznie
ma taką złożoność jak
sortowanie przez scalanie.
Podziały
niezrównoważone
• Algorytm może działać tak
wolno jak sortowanie
przez wstawianie.
Najgorszy przypadek podziałów:
gdy procedura partition tworzy
jeden obszar złożony z n-1
elementów, a drugi tylko z 1
elementu.
Załóżmy, że takie
niezrównoważone
podziały będą
wykonywane w każdym
kroku algorytmu.
(n)
-koszt podziału
T (1) (1)
-wykonanie dla tablicy jednoelementowej
T ( n ) T ( n 1) ( n )
- równanie rekurencyjne
Rozwiązujemy rekurencję, iterując:
T ( n ) T ( n 1) ( n ) T ( n 2 ) ( n 1) ( n ) ...
n
(k ) k
k 1
k 1
n
n ( n 1)
2
(n
2
)
Czy to jest pesymistyczna złożoność obliczeniowa?
Niech Tmax (n) będzie najgorszym czasem działania algorytmu quicksort
dla danych wejściowych rozmiaru n. Mamy równanie rekurencyjne:
T max ( n ) max (T max ( q ) T max ( n q )) ( n )
1 q n 1
gdzie parametr q przyjmuje wartości od 1 do n-1, ponieważ mamy
dwa obszary, z których każdy ma co najmniej 1 element.
Zgadujemy, że T max ( n ) cn 2
dla pewnej stałej c. Zatem
T max ( n ) max ( cq c ( n q ) ) ( n ) c max ( q ( n q ) ) ( n )
2
2
2
1 q n 1
1 q n 1
f ( q ) q ( n q ) 2 q 2 nq n ,
2
2
2
f ' (q ) 4q 2n 0 q
1 q n 1
2
n
2
f (1) 1 ( n 1) f ( n 1),
2
max?
f( )
n
2
2
n
2
T max ( n ) c (1 ( n 1) ) ( n ) cn 2 c ( n 1) ( n ) cn
2
2
2
2
2
Przy odpowiednim
doborze dużej stałej c
Najlepszy przypadek podziałów:
T(n)=2T(n/2)+Θ(n)
Przypadek 2 tw. o
rekurencji
uniwersalnej daje
rozwiązanie:
T(n)= Θ(nlgn)
Podział na połowę
Jeśli zbadamy różnicę między przypadkiem pesymistycznym a
najlepszym, to dostaniemy pesymistyczną wrażliwość algorytmu:
Δ(n)=Θ(n2-nlg2n)
Podziały zrównoważone – przypadek średni
Przykład podziału w stosunku 9 do1
Przypadek średni jest bliski przypadkowi
najlepszemu – udowodnimy!
Podziały zrównoważone – przypadek średni (oczekiwana złożoność)
Załóżmy, że
-wystąpienie dowolnej permutacji n liczb całkowitych jako danych do
sortowania jest jednakowo prawdopodobne,
- podział permutacji na dwie podtablice i-1 elementową i n-i
elementową jest również jednakowo prawdopodobny dla dowolnego
i=1,2,…,n.
Wtedy średnia złożoność obliczeniowa Tsr(n) spełnia warunki:
T sr ( 0 ) T sr (1) 0
n
T sr ( n ) ( n 1) 1 (T sr ( i 1) T sr ( n i ) ) n ,
i 1
i n.
jednakowo prawdopodobne
n
T sr ( n ) n 1 2 T sr ( i 1) n ,
i 1
i n.
∙n
n
nT sr ( n ) n ( n 1) 2 T sr ( i 1) ,
odejmujemy
stronami
i 1
n 1
( n 1)T sr ( n 1) ( n 1)( n ) 2 T sr ( i 1) ,
i 1
nT sr ( n ) ( n 1)T sr ( n 1) 2 T sr ( n 1) 2 n ,
nT sr ( n ) ( n 1)T sr ( n 1) 2 n ,
T sr ( n )
n 1
T sr ( n )
n 1
T sr ( n 1)
n
T sr ( n 1)
n
2
n 1
2
n 1
,
: n ( n 1)
n 1.
T sr (1)
Stosujemy iteracje….
2 / 3 2 / 4 .... 2 /( n 1)
2
2 (1 1 / 2 1 / 3 ... 1 /( n 1) 3 / 2 ) 2 ( H n 1 3 / 2 ),
n 1.
n+1-sza suma szeregu harmonicznego
lub wykorzystanie szacowania sumy
za pomocą całki
Funkcja harmoniczna:
1
H n ln n O ( n ),
0 . 57
stała Eulera
T sr ( n )
2
log
( n 1) log
2
e
2
n O (1 / n ) 1 . 4 O (log
Z całki natomiast:
2
n)
n 1
2 (1 1 / 2 1 / 3 ... 1 /( n 1) 3 / 2 ) 2 ( 1 / k 3 / 2 )
k 1
n2
2
(1 / x ) d x 3 2 ln( n 2 ) 2 ln 1 3 2 ln( n 2 ) 3 2
1
T sr ( n ) 2 ( n 1)
log 2 ( n 2 )
log
2
3 ( n 1).
e
Stąd widać, że
T sr ( n ) O ( n log
2
n)
log 2 ( n 2 )
log
2
e
3,
Zalety:
•Praktycznie działa w miejscu (używa tylko niewielkiego stosu
pomocniczego).
•Do posortowania n elementów wymaga średnio czasu proporcjonalnego
do nlog2n .
•Ma wyjątkowo skromną pętlę wewnętrzną.
Wady:
•Jest niestabilny.
•Zabiera około n2 operacji w przypadku najgorszym.
•Jest wrażliwy (tzn. prosty niezauważony błąd w implementacji może
powodować niewłaściwe działanie w przypadku niektórych danych).
Średnia złożoność obliczeniowa jest niemal optymalna. Od kiedy w 1960
C.A.R.Hoare go opublikował, zaczęły się pojawiać jego ulepszone wersje – ale
algorytm jest tak zrównoważony, że poprawienie programu w jednym aspekcie,
pogarsza jego parametry w innym.
Jest często stosowany w bibliotekach standardowych. Można go usprawnić
ograniczając rekurencję do pewnego ustalonego n, a tablice o mniejszej długości
sortujemy nierekurencyjnym algorytmem sortowania.