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 ( n21) 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 ( n1) 2 k k 1 2 n ( n 1) n ( n1 ) 2 2 n ( n 1) k k 1 n ( n 1) 2 1 2 n ( n 1) n ( n 1) 2 2 2 n (4n1) 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 12 i i 12 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 1i 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