Informatika gyűjtemény

NézetNyomtat

Rendezési algoritmusok

A feladat

Egy N elemű T[] tömb elemeit kell nagyság szerint növekvő sorrendbe rakni.

Az elmélet

Két elem összehasonlításakor három választ kaphatunk (<, =, >), tehát $k$ kérdéssel legfeljebb $3^k$ lehetőség között tudunk választani. Az $\,N$ elemnek $\,N!$-féle sorrendje van, ezek közül kell az egyetlen jót meghatároznunk, tehát szükségszerűen $N! \le 3^k$. Kettes alapú logaritmust véve innen $\log N! /\log 3 \le k$. Finomabb matematikai eszközökkel megmutatható, hogy $\log N! \approx c\cdot N\log N$, ennél gyorsabb rendező algoritmus nem készíthető. (Ez természetesen csak azokra a rendezésekre vonatkozik, amelyek a tömbelemek összehasonlításával és cserélgetésével működnek.)
A legegyszerűbb rendező algoritmusok általában $N^2$-tel arányos lépésszámmal dolgoznak, a kupacrendezés és a gyorsrendezés elméletileg optimális. Óvatosan kell azonban bánnunk az elméleti becslésekkel, a nagyságrend szempontjából elhanyagolt konstansokon néha sok múlik. "Kis" tömbök esetén az egyszerű cserés rendezések is tökéletesen megfelelnek. Gondolatébresztőnek egy kis táblázat. (Az egyszerűség kedvéért 10-es alapú logaritmussal számolva.)
$\,N$$N^2$$1000N\log N$
1010010000
10010000200000
100010000003000000
1000010000000040000000
A bemutatott példák közül a Shell rendezés látszik a leggyorsabbnak, de ez csak $N = 100$ miatt van így. Nagy adathalmazok esetén a kupacrendezés és a gyorsrendezés is hatékonyabb.

Algoritmusok

Az algoritmusok többségében használjuk a csere(i,j) eljárást, ami az alábbi műveleteket végzi:
    tmp := T[i]; T[i] := T[j]; T[j] := tmp

Egyszerű cserés rendezés

Az aktuális első elemet összehasonlítjuk a második, harmadik, ... elemmel. Ha az aktuális első elem nagyobb, cserélünk. A külső ciklus első lefutásakor helyére kerül a legkisebb elem. Ezután a külső ciklus továbblép, és a helyretett elem kikerül a rendezendő szakaszból. A külső ciklus $i.$ lefutásan után az első $i$ elem rendezett.
A belső ciklus lefutásakor egyre kisebb értékű elemekkel cseréljük az éppen vizsgált tagot, emiatt alakul ki az a jellegzetes kép, hogy a rendezett szakasz után nagyjából fordítottan rendezett szakasz jelenik meg.
Ciklus i := 1-től (N-1)-ig
    Ciklus j := (i+1)-től N-ig
        Ha T[i] > T[j] akkor
            Csere(i,j)
        Elágazás vége
    Ciklus vége
Ciklus vége

Minimumkiválasztásos rendezés

Megkeressük a legkisebb elemet és betesszük az első helyre. Ezután az első elemmel tovább nem foglalkozunk, a megmaradt $N-1$ elemmel megismételjük az eljárást. Most már az első két elem került helyre, stb...
Ciklus i := 1-től (N-1)-ig
    min := i
    Ciklus j := (i+1)-től N-ig
        Ha T[j] < T[min] akkor min := j Elágazás vége
    Ciklus vége
    Ha min <> i akkor Csere(i,min) Elágazás vége
Ciklus vége

Buborék rendezés

Menetenként végignézzük a szomszédos elemeket a tömb elejétől a vége felé haladva, és felcseréljük a rosszul rendezett párok tagjait. Egy menetben a legnagyobb elem a tömb végére kerül. Ezután eggyel rövidebb tömbbel folytatjuk az eljárást... Ha egy menetben nem történt csere, a teljes tömb rendezett és megállhatunk.
Ciklus i := (N-1)-től 1-ig
    voltCsere := HAMIS
    Ciklus j := 1-től i-ig
        Ha T[j] > T[j+1] akkor
            Csere(j,j+1)
            voltCsere := IGAZ
        Elágazás vége
    Ciklus vége
    Ha nem voltCsere akkor kilépés Elágazás vége
Ciklus vége

Kétirányú buborék rendezés

A buborék rendezés javítása. Egy menetben a legkisebb és legnagyobb elemet tesszük helyre, így egyszerre mozognak a kis elemek a tömb eleje, a nagyok pedig a tömb vége felé.

vége := N+1; eleje := 0
Ciklus amíg eleje < vége
    eleje := eleje + 1; vége := vége - 1; voltCsere := hamis
    Ciklus j := eleje-től (vége-1)-ig
        Ha T[j] > T[+ 1] akkor
            Csere(j,+ 1)
            voltCsere := igaz
        Elágazás vége
    Ciklus vége
    Ha nem voltCsere akkor 
        Kilépés 
    különben
        voltCsere := hamis
    Elágazás vége
    Ciklus j := (vége-1)-től eleje-ig (-1)-esével
        Ha T[j] > T[+ 1] akkor
            Csere(j,+ 1)
            voltCsere := igaz
        Elágazás vége
    Ciklus vége
    Ha nem voltCsere akkor Kilépés Elágazás vége
Ciklus vége

Shell rendezés

Az algoritmus alapötlete az, hogy ne csak a szomszédos elemeket hasonlítsuk össze, hanem először távoli indexű értékeket nézzünk, majd folyamatosan csökkentsük a távolságot. Sokan vizsgálták azt a kérdést, hogy milyen távolságsorozat adja a legjobb futási időt.
A most bemutatott változatban a D.E. Knuth által javasolt h[] = {1,4,13,40,121} távolságsorozattal dolgozunk.
Tetszőleges távolságsorozat helyes rendezést biztosít, ha a legkisebb lépés értéke 1.
Ciklus s := 5-től 1-ig (-1)-esével
    lep := h[s]
    Ciklus j := (lep+1)-től N-ig
        i := j - lep; x := T[j]
        Ciklus amíg i > 0 és T[i] > x 
            T[i+lep] := T[i]
            i = i - lep
        Ciklus vége 
        T[i+lep] := x
    Ciklus vége
Ciklus vége

Kupac rendezés

A tömböt kupaccá alakítjuk. A kupac tetejére kerül a legnagyobb elem, ezt a tömb végén lévő elemmel felcseréljük, csökkentjük a kupac méretét és helyreállítjuk a kupac-tulajdonságot. A buborékrendezéshez hasonlóan itt is minden menetben az aktuális szakasz legnagyobb eleme kerül helyére. Egy menet azonban sokkal gyorsabb, mert a kupac-tulajdonság helyreállítása $\log N$-nel arányos lépésben megy, míg a buborék rendezésnél egy-egy menet $N$-nel arányos lépést végez. (Részletesebb magyarázat a kupac adatszerkezet leírásánál.)
bal(k):
    bal := 2 * k
Eljárás vége

jobb(k):
    jobb := 2 * k + 1
Eljárás vége

epit(T):
    Ciklus i := (N/2)-től 1-ig (-1)-esével
        sullyeszt(N,i,T)
    Ciklus vége
Eljárás vége

sullyeszt(p, r, T):
    b := bal(r); j := jobb(r)

    Ha b<=és T[b]>T[r] akkor max := b különben max := r Elágazás vége
    Ha j<=és T[j]>T[max] akkor max := j Elágazás vége
    Ha max != r akkor
        Csere(max,r)
        sullyeszt(p,max,a);
    Elágazás vége
Eljárás vége

rendez(T):
    db := N
    epit(T)
    Ciklus i := db-től 1-ig (-1)-esével
        Csere(1,i)
        db --;
        sullyeszt(db,1,T); 
    Ciklus vége
Eljárás vége

Gyorsrendezés

A középső indexű elem szerint kettéválogatjuk a tömböt. Alulra kerülnek a középsőnél kisebbek, felülre pedig a nagyobbak. Ezután az alsó és a felső részre rekurzívan meghívjuk a rendező eljárást.
A rendezést a QuickSort(T, 1, N) hívással indíthatjuk el.
A rekurzív módszer akkor hatékony, ha elég sokszor nagyjából két egyenlő részre bontjuk az éppen rendezendő szakaszt. Mivel az eredeti adatsorról nem feltételezhetünk semmit, nem biztos, hogy a középső indexű elem adja a legjobb kettéosztást. A gyorsrendezés egyik gyakran használt változatában véletlenszerűen választjuk ki a kettéosztást definiáló "pivot elemet", ezzel kivédjük a "rossz" adatsorból adódó lassulást.
QuickSort(T, lo0, hi0): 

lo = lo0;hi = hi0;
Ha hi0 > lo0 akkor
    mid = T[ ( lo0 + hi0 ) / 2 ]
    Ciklus amíg lo <= hi 
        Ciklus amíg ( lo < hi0 ) és ( T[lo] < mid ) lo := lo + 1 Ciklus vége
        Ciklus amíg ( hi > lo0 ) és ( T[hi] > mid ) hi := hi - 1 Ciklus vége
        Ha lo <= hi akkor
                        Csere(lo, hi)
                        lo := lo + 1
                        hi := hi - 1
        Elágazás vége
    Ciklus vége
    Ha lo0 < hi akkor QuickSort( T, lo0, hi ) Elágazás vége
    Ha lo < hi0 akkor QuickSort( T, lo, hi0 ) Elágazás vége
Elágazás vége