Zbiór do posortowania mieści się w pamięci Ogólnie o sortowaniu Rodzaje sortowania (wg pamięci) 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.
Ogólnie o sortowaniu Rodzaje sortowania (wg operacji) 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.
Wymaga dodatkowej pamięci na pełną kopię sortowanych danych Ogólnie o sortowaniu 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 Wymaga dodatkowej pamięci na pełną kopię sortowanych danych 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
Ogólnie o sortowaniu 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.
Ogólnie o sortowaniu 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.
Sortowanie przez selekcję 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++ //realizacja funkcji zamiana //przestawiajacej dwa elementy //dowolnego typu void zamiana(int &A, int &B) { int t=A; A=B; B=t; } 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]); } Typ może być dowolny
Sortowanie przez selekcję Przykład: S E L K C J A 7 porównań 6 porównań 5 porównań 4 porównania 3 porównania 2 porównania 1 porównanie
Sortowanie przez selekcję 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: Tmax(n) P-stwo, że w każdym z porównań znajdziemy element najmniejszy jest jednakowe Stąd:
Sortowanie przez selekcję 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. 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
Sortowanie przez selekcję Zalety: Liczba zamian w najgorszym przypadku: n-1. Prostota implementacji. Zadowalająca szybkość dla małych wartości n. Nie wymaga dodatkowej pamięci. Wady: Nie jest stabilny. Ma dużą złożoność (rzędu kwadratowego), więc nie nadaje się do sortowania długich tablic. 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.
Sortowanie przez wstawianie 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;
Sortowanie przez wstawianie 1 porównanie <=2 porównania <=3 porównania <= 4 porównania <=5 porównań ……. <=9 porównań GOTOWE
Sortowanie przez wstawianie 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ść : 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 Sumując teraz po wszystkich n-1 iteracjach, dostajemy:
Sortowanie przez wstawianie 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.
Sortowanie przez wstawianie Średnia wrażliwość (czyli miara wrażliwości oczekiwanej): w i-tym kroku mamy: Sumując po wszystkich n-1 iteracjach, dostajemy:
Sortowanie przez wstawianie Zalety: Stabilność. Średnio algorytm jest 2 razy szybszy niż algorytm sortowania przez selekcję. Optymalny dla ciągów prawie posortowanych. Nie wymaga dodatkowej pamięci.
Sortowanie przez wstawianie 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.
Sortowanie bąbelkowe 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.
Sortowanie bąbelkowe Implementacja w C++ Algorytm jest klasy O(n2) 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 jest klasy O(n2) Analiza: 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
Sortowanie bąbelkowe 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) { 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++) right=k-1; while (left<=right); 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.
Sortowanie szybkie (quicksort) 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.
Sortowanie szybkie 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.
Sortowanie szybkie Oś podziału
Sortowanie szybkie 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. początkowy 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; } if (j==i) break; s=*(tab+i); *(tab+i)=*(tab+j); *(tab+j)=s; *(tab+i)=*(tab+r); *(tab+r)=s; return i; //α: 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 zamiana War. końcowy
Sortowanie szybkie 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ą η.
Sortowanie szybkie 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.
Sortowanie szybkie Realizacja w C++ Właściwe sortowanie 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.
Sortowanie szybkie 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ść.
Sortowanie szybkie – złożoność obliczeniowa 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.
Sortowanie szybkie – złożoność obliczeniowa 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.
Sortowanie szybkie – złożoność obliczeniowa koszt podziału wykonanie dla tablicy jednoelementowej równanie rekurencyjne Rozwiązujemy rekurencję, iterując: Czy to jest pesymistyczna złożoność obliczeniowa?
Sortowanie szybkie – 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: 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 Przy odpowiednim doborze dużej stałej c
Sortowanie szybkie – złożoność obliczeniowa 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ę
Sortowanie szybkie – złożoność obliczeniowa Jeśli zbadamy różnicę między przypadkiem pesymistycznym a najlepszym, to dostaniemy pesymistyczną wrażliwość algorytmu: Δ(n)=Θ(n2-nlg2n)
Sortowanie szybkie – złożoność obliczeniowa Podziały zrównoważone – przypadek średni Przykład podziału w stosunku 9 do1 Przypadek średni jest bliski przypadkowi najlepszemu – udowodnimy!
Sortowanie szybkie – złożoność obliczeniowa 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: jednakowo prawdopodobne ∙n
Sortowanie szybkie –złożoność obliczeniowa odejmujemy stronami Stosujemy iteracje…. n+1-sza suma szeregu harmonicznego lub wykorzystanie szacowania sumy za pomocą całki
Sortowanie szybkie –złożoność obliczeniowa Funkcja harmoniczna: stała Eulera Z całki natomiast: Stąd widać, że
Sortowanie szybkie 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.