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.)
int partition (int *tab, int l, int r) {
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);
*(tab+j)=s;
}
s=*(tab+i);
*(tab+i)=*(tab+r);
*(tab+r)=s;
return i;
}
zamiana
//α: 1<=l<r.
//γ: 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 War.
początkowy
War.
końcowy
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, int r) {
if(r<=l) return; //1-instrukcja int i=partition(tab, l, r); //2-instrukcja quicksort(tab, l, i-1); //3-instrukcja quicksort(tab, i+1, r); //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-l- 1<=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.
• Algorytm asymptotycznie ma taką złożoność jak sortowanie przez scalanie.
Podziały zrównoważone
• Algorytm może działać tak wolno jak sortowanie
przez wstawianie.
Podziały
niezrównoważone
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.
) ( )
1 (
) (
) 1 ( )
1 (
) (
n n
T n
T T
n
( )
) (
...
) ( )
1 (
) 2 (
) ( )
1 (
) (
2 2
) 1 ( 1
1
n k
k
n n
n T n
n T n
T
n n n
k n
k
-koszt podziału
-wykonanie dla tablicy jednoelementowej - równanie rekurencyjne
Rozwiązujemy rekurencję, iterując:
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:
) ( ))
( )
( (
max )
(
max max1
max
n
1T q T n q n
T
q n
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 dla pewnej stałej c. Zatem 2
m ax
( n ) cn
T
) ( )
) (
( max )
( )
) (
( max )
(
2 21 1
2 2
1
max
n
1cq c n q n c q n q n
T
n q n
q
2 2
2
2
2 2
2 2
)
2( ),
1 (
) 1 (
1 ) 1 (
max?
0 2
4 )
( '
1 1
, 2
2 )
( )
(
n n
n
f n
f n
f
q n
q q
f
n q
n nq q
q n q
q f
2 2
2 2
m ax
( n ) c ( 1 ( n 1 ) ) ( n ) cn 2 c ( n 1 ) ( n ) cn
T
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:
. ,
) ) (
) 1 ( ( 1
) 1 (
) (
0 ) 1 ( )
0 (
1
n i n i
n T i
T n
n T
T T
n
i
sr sr
sr
sr sr
jednakowo prawdopodobne
. ,
) 1 ( 2
1 )
(
1
n i n i
T n
n T
n
i
sr
sr
∙n
, ) 1 ( 2
) )(
1 (
) 1 (
) 1 (
, ) 1 ( 2
) 1 (
) (
1
1 1
n
i
sr sr
n
i
sr sr
i T n
n n
T n
i T n
n n
nT
odejmujemystronami
. 1 1 ,
2 )
1 (
1 ) (
) 1 (
: , 2 ) 1 (
) 1 (
) (
, 2 ) 1 (
2 ) 1 (
) 1 (
) (
n n n
n T n
n T
n n n
n T n
n nT
n n
T n
T n
n nT
sr sr
sr sr
sr sr
sr
Stosujemy iteracje….
. 1 ),
2 / 3 (
2 ) 2 / 3 ) 1 /(
1 ...
3 / 1 2 / 1 1 ( 2
) 1 /(
2 ....
4 / 2 3 / 2 2
) 1 ( 1
2 )
1 (
1 ) (
1
n
H n
T n n
n n T n
n T
n sr
sr sr
n+1-sza suma szeregu harmonicznego lub wykorzystanie szacowania sumy
za pomocą całki
Funkcja harmoniczna:
H
n ln n O ( n
1), 0 . 57
stała Eulera
) (log 4
. 1 ) / 1 ( log
) 1 log (
) 2
(
2 22
n O
n O
n e n
n
T
sr
Z całki natomiast:
).
1 (
log 3
) 2 (
) log 1 (
2 ) (
, log 3
) 2 (
2 log 3
) 2 ln(
2 3 1 ln 2 ) 2 ln(
2 3 )
/ 1 ( 2
) 2 / 3 /
1 ( 2 ) 2 / 3 ) 1 /(
1 ...
3 / 1 2 / 1 1 ( 2
2 2
2 2 2
1
1
1
e n n n
n T
e n n
n x
d x
k n
sr n
n
k
Stąd widać, że
) log
( )
( n O n
2n
T
sr
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.