Idea: Wyznaczamy najmniejszy element w ciągu (tablicy) i

Transkrypt

Idea: Wyznaczamy najmniejszy element w ciągu (tablicy) i
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)
pn , k 
1
n ( n 1)
2
2

, k  1,2,, n ( n21)
n(n  1)
n ( n 1 )
2
Stąd:
kp
k 1
n ,k

P-stwo, że w każdym z
porównań znajdziemy
element najmniejszy jest
jednakowe
n ( n1)
2
k
k 1
2
n ( n 1)

n ( n1 )
2
2
n ( n 1)
k 
k 1
n ( n 1)
2 1 2 n ( n 1)
n ( n 1)
2
2
 2 n (4n1)  12  14 n  14 n 2  14 n 2  O(n)

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ść :
Tm ax (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
i
j 1
j 1
Tsr (i )   j  pij  1i  j  1i (1  2  ...  i )  1i 12 i i  12 i .
Sumując teraz po wszystkich n-1 iteracjach, dostajemy:
n 1
n 1
n 1
Tsr (n)   Tsr (i )  i 21  12  (i  1) 
i 1

i 1
1 2 n
2 2
i 1
(n  1)  (n 2 ).
n
1
2
1
k

 2 (2  3  ... n) 
k 2
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:
 2 (i) 
2
(
j

T
(
i
))
 śr pij 
j 1...i
1i 2 1
1
(
j

)

 2 i 4i
j 1...i
2
(
2
j

i

1
)

j 1...i
Sumując po wszystkich n-1 iteracjach, dostajemy:
1
 (n)    (i )    4i
i 1...n 1
i 1...n 1 
2

3
1 2
(2 j  i  1)   ...  6 n  (n).

j 1...i

2
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