Clever Quest Lab ← Informatika · 3. ročník SŠ
Informatika3. ročník SŠTriediace algoritmy

Triediace algoritmy

Zoradiť pár čísel od najmenšieho po najväčšie zvládne každý. Počítač na to potrebuje presný postup zložený z jednoduchých krokov: porovnaj dva prvky a prípadne ich vymeň. Spoznáš päť triediacich algoritmov, pri každom uvidíš ukážku v Pythone, dozvieš sa, prečo sú niektoré mnohonásobne rýchlejšie ako iné, a každý si môžeš pustiť krok za krokom.

Preskočiť na vizualizáciu triedenia ↓

Čo znamená utriediť údaje

Utriediť (zoradiť) údaje znamená usporiadať ich podľa zvoleného kľúča — čísla podľa veľkosti, mená podľa abecedy, súbory podľa dátumu. Pri vzostupnom triedení nasleduje po každom prvku rovnaký alebo väčší prvok, pri zostupnom rovnaký alebo menší. Na tejto stránke triedime celé čísla vzostupne.

Triedenie patrí k úlohám, ktoré počítače riešia najčastejšie:

Údaje máme uložené v poli (v Pythone v zozname) s názvom a, ktoré má n prvkov. Prvky číslujeme od nuly: a[0], a[1], …, a[n - 1]. Číslu v hranatej zátvorke hovoríme index alebo pozícia prvku.

Porovnanie a výmena

Algoritmy na tejto stránke sa skladajú z dvoch základných operácií:

Rýchlosť algoritmu preto nemeriame v sekundách, ktoré závisia od počítača, ale počtom porovnaní a výmen, ktoré urobí pri poli s n prvkami.

Stabilné triedenie a triedenie na mieste

Triedenie je stabilné, ak prvky s rovnakým kľúčom zostanú v pôvodnom vzájomnom poradí. Príklad: zoznam žiakov je zoradený podľa priezviska a my ho stabilne utriedime podľa triedy. Žiaci tej istej triedy potom zostanú zoradení podľa priezviska. Pri nestabilnom triedení by sa ich poradie mohlo pomiešať.

Algoritmus triedi na mieste (anglicky in-place), ak prvky presúva priamo v pôvodnom poli a okrem neho potrebuje len niekoľko pomocných premenných. Opakom je algoritmus, ktorý si vytvára pomocné pole — pri veľkých údajoch spotrebuje citeľne viac pamäte.

Bublinkové triedenie (bubble sort)

Myšlienka. Pole opakovane prechádzame zľava doprava a porovnávame susedné prvky. Ak sú v nesprávnom poradí, vymeníme ich. Veľké prvky tak postupne „vybublajú“ na koniec poľa.

Postup:

  1. Porovnaj prvý a druhý prvok. Ak je ľavý väčší, vymeň ich.
  2. Posuň sa o jedno miesto doprava a urob to isté. Pokračuj až po koniec neutriedenej časti. Po prvom prechode je najväčší prvok na konci poľa.
  3. Prechod opakuj. Každý ďalší je o jedno porovnanie kratší, lebo prvky na konci sú už na svojom mieste.
  4. Ak v celom prechode nenastala žiadna výmena, pole je utriedené a algoritmus skončí skôr.
def bubble_sort(a):
    n = len(a)
    for i in range(n - 1):
        vymena = False
        for j in range(n - 1 - i):
            if a[j] > a[j + 1]:
                a[j], a[j + 1] = a[j + 1], a[j]
                vymena = True
        if not vymena:
            break
    return a

Počet porovnaní a výmen. Základná verzia bez kroku 4 urobí vždy (n − 1) + (n − 2) + … + 1 = n · (n − 1) / 2 porovnaní. So skorým ukončením stačí na už utriedené pole jediný prechod: n − 1 porovnaní a žiadna výmena. Najhorším prípadom je pole v obrátenom poradí, ktoré vyžaduje n · (n − 1) / 2 porovnaní a rovnako veľa výmen. Každá výmena opraví práve jednu dvojicu prvkov v nesprávnom poradí (inverziu), počet výmen sa preto rovná počtu inverzií v poli.

Kedy je vhodné. Je to najjednoduchší triediaci algoritmus, dobrý na pochopenie princípu a na veľmi malé alebo takmer utriedené polia. Na väčšie údaje je pomalý a v praxi sa nepoužíva. Je stabilný a triedi na mieste.

Triedenie výberom (selection sort)

Myšlienka. V neutriedenej časti poľa nájdeme najmenší prvok a vymeníme ho s prvým prvkom tejto časti. Utriedená časť na začiatku poľa tým narastie o jeden prvok.

Postup:

  1. Polož i = 0. Prejdi pole od pozície i po koniec a zapamätaj si index m najmenšieho prvku.
  2. Ak najmenší prvok nestojí na pozícii i, vymeň ho s prvkom na tejto pozícii.
  3. Zväčši i o 1 a hľadaj minimum už len vo zvyšku poľa.
  4. Po n − 1 prechodoch je pole utriedené — prvok, ktorý zostal posledný, je najväčší.
def selection_sort(a):
    n = len(a)
    for i in range(n - 1):
        m = i
        for j in range(i + 1, n):
            if a[j] < a[m]:
                m = j
        if m != i:
            a[i], a[m] = a[m], a[i]
    return a

Počet porovnaní a výmen. Pri hľadaní minima treba vždy prejsť celú neutriedenú časť, preto algoritmus urobí n · (n − 1) / 2 porovnaní pri každom vstupe — aj keď je pole už utriedené. Výmen je však najviac n − 1, v každom prechode najviac jedna. Bublinkové triedenie aj triedenie vkladaním ich v najhoršom prípade potrebujú až n · (n − 1) / 2.

Kedy je vhodné. Keď je výmena dvoch prvkov oveľa drahšia ako porovnanie, napríklad pri presúvaní veľkých záznamov. Inak je pomalé. Triedi na mieste, ale nie je stabilné: v poli 3a, 3b, 1 sa v prvom prechode vymení 1 s 3a a vznikne 1, 3b, 3a — dve rovnaké trojky si vymenili poradie.

Triedenie vkladaním (insertion sort)

Myšlienka. Postupujeme ako pri ukladaní kariet do ruky. Ľavá časť poľa je utriedená a ďalší prvok do nej vložíme na správne miesto.

Postup:

  1. Prvý prvok tvorí utriedenú časť.
  2. Vezmi prvý prvok neutriedenej časti.
  3. Kým je jeho ľavý sused väčší, vymeň ich. Prvok sa tak posúva doľava.
  4. Keď narazí na menší alebo rovnaký prvok, prípadne na začiatok poľa, je vložený. Pokračuj ďalším prvkom.
def insertion_sort(a):
    for i in range(1, len(a)):
        j = i
        while j > 0 and a[j - 1] > a[j]:
            a[j - 1], a[j] = a[j], a[j - 1]
            j -= 1
    return a

V praxi sa vkladaný prvok najprv odloží do premennej a väčšie prvky sa iba posúvajú o jedno miesto doprava. Ušetrí sa tým časť zápisov do poľa, počet porovnaní zostáva rovnaký.

Počet porovnaní a výmen. Na už utriedenom poli stačí pri každom vkladanom prvku jedno porovnanie: spolu n − 1 porovnaní a žiadna výmena. V najhoršom prípade (obrátené poradie) musí každý prvok prejsť až na začiatok, čo dáva n · (n − 1) / 2 porovnaní aj výmen. Pri náhodne usporiadanom poli je to približne polovica. Počet výmen sa aj tu rovná počtu inverzií.

Kedy je vhodné. Na malé polia (niekoľko desiatok prvkov) a na takmer utriedené údaje, kde je veľmi rýchle. Preto ho na krátke úseky používajú aj knižničné triedenia. Je stabilné, triedi na mieste a dokáže spracúvať prvky, ktoré prichádzajú postupne.

Triedenie zlučovaním (merge sort)

Myšlienka. Používa princíp rozdeľuj a panuj. Pole rozdelíme na dve polovice, každú utriedime tým istým postupom (rekurzívne) a dve utriedené polovice zlúčime do jedného utriedeného poľa.

Postup:

  1. Pole s jedným prvkom (alebo prázdne) je utriedené. Tu sa rekurzia zastaví.
  2. Dlhšie pole rozdeľ na ľavú a pravú polovicu.
  3. Každú polovicu utrieď rekurzívne.
  4. Zlučovanie: porovnaj prvé (najmenšie) prvky oboch polovíc a menší z nich presuň do výsledku. Opakuj, kým sa jedna polovica neminie. Zvyšok druhej polovice pripoj na koniec.
def merge_sort(a):
    if len(a) <= 1:
        return a
    stred = len(a) // 2
    lava = merge_sort(a[:stred])
    prava = merge_sort(a[stred:])
    return zluc(lava, prava)

def zluc(lava, prava):
    vysledok = []
    i = j = 0
    while i < len(lava) and j < len(prava):
        if lava[i] <= prava[j]:
            vysledok.append(lava[i])
            i += 1
        else:
            vysledok.append(prava[j])
            j += 1
    vysledok.extend(lava[i:])
    vysledok.extend(prava[j:])
    return vysledok

Funkcia merge_sort pôvodné pole nemení, ale vráti nové, utriedené: a = merge_sort(a). Pri rovnosti berie funkcia zluc prvok z ľavej časti (podmienka <=), vďaka čomu je triedenie stabilné.

Počet porovnaní a zápisov. Prvky sa tu nevymieňajú, ale zapisujú do výsledku. Delením na polovice vznikne približne log2 n úrovní a na každej z nich sa spolu zlúči všetkých n prvkov. Výsledkom je približne n · log2 n zápisov a o niečo menej porovnaní — takmer nezávisle od toho, ako bol vstup usporiadaný.

Kedy je vhodné. Na veľké polia, keď potrebujeme zaručenú rýchlosť a stabilitu, a tiež na triedenie údajov, ktoré sa nezmestia do operačnej pamäte (zlučujú sa utriedené súbory). Nevýhodou je pomocná pamäť pre n prvkov — algoritmus netriedi na mieste.

Rýchle triedenie (quicksort)

Myšlienka. Aj rýchle triedenie pracuje podľa princípu rozdeľuj a panuj. Zvolíme jeden prvok, pivot, a úsek poľa preusporiadame tak, aby vľavo od pivota boli menšie prvky a vpravo väčšie alebo rovnaké. Pivot je tým na svojom konečnom mieste. Ľavú a pravú časť potom utriedime rovnakým postupom.

Preusporiadaniu okolo pivota hovoríme delenie (anglicky partition) a dá sa urobiť viacerými spôsobmi. Na tejto stránke používame Lomutovu schému delenia, v ktorej je pivotom posledný prvok úseku.

Postup:

  1. Za pivot zvoľ posledný prvok úseku. Index i nastav na začiatok úseku — označuje miesto, kam príde najbližší prvok menší ako pivot.
  2. Indexom j prejdi úsek od začiatku po predposledný prvok. Ak je a[j] menší ako pivot, vymeň a[i] s a[j] a zväčši i o 1.
  3. Nakoniec vymeň pivot s prvkom a[i]. Pivot je na svojom konečnom mieste.
  4. Rovnako utrieď úsek vľavo od pivota a úsek vpravo od neho. Úsek s najviac jedným prvkom je už utriedený.
def quick_sort(a, lavy, pravy):
    if lavy >= pravy:
        return
    pivot = a[pravy]
    i = lavy
    for j in range(lavy, pravy):
        if a[j] < pivot:
            a[i], a[j] = a[j], a[i]
            i += 1
    a[i], a[pravy] = a[pravy], a[i]
    quick_sort(a, lavy, i - 1)
    quick_sort(a, i + 1, pravy)

Celé pole utriedime volaním quick_sort(a, 0, len(a) - 1). Ak v kroku 2 alebo 3 ukazujú oba indexy na ten istý prvok, výmena nič nezmení — takú výmenu do počtu nezarátavame.

Počet porovnaní a výmen. Delenie úseku s k prvkami potrebuje k − 1 porovnaní. Ak pivot delí úseky približne na polovice, vznikne asi log2 n úrovní a porovnaní je rádovo n · log2 n. V najhoršom prípade je pivot vždy najmenším alebo najväčším prvkom úseku. Úsek sa potom skráti len o jeden prvok a porovnaní je n · (n − 1) / 2. Pri Lomutovej schéme to nastane napríklad vtedy, keď je pole už utriedené alebo zoradené v obrátenom poradí.

Kedy je vhodné. V praxi patrí k najrýchlejším algoritmom na veľké polia v operačnej pamäti. Triedi na mieste, navyše potrebuje len pamäť na rekurziu. Nie je stabilné. Pred najhorším prípadom sa chráni lepšou voľbou pivota — vyberie sa napríklad náhodný prvok alebo prostredný z troch.

Riešené príklady

Vo všetkých príkladoch triedime to isté pole 5, 2, 8, 1, 9, 3. Rovnaké pole je prednastavené aj vo vizualizácii nižšie, takže si každý krok môžeš overiť. Zápis 5 > 2 ↔ znamená, že sme porovnali 5 a 2 a vymenili ich; pri zápise 5 ≤ 8 výmena nenastala.

Príklad 1: Utrieď pole bublinkovým triedením a spočítaj porovnania a výmeny.
  1. Prvý prechod, 5 porovnaní: 5 > 2 ↔ 5 ≤ 8 8 > 1 ↔ 8 ≤ 9 9 > 3 ↔ Po 3 výmenách: 2, 5, 1, 8, 3, 9 Najväčší prvok 9 je na konci.
  2. Druhý prechod, 4 porovnania: 2 ≤ 5 5 > 1 ↔ 5 ≤ 8 8 > 3 ↔ Po 2 výmenách: 2, 1, 5, 3, 8, 9
  3. Tretí prechod, 3 porovnania: 2 > 1 ↔ 2 ≤ 5 5 > 3 ↔ Po 2 výmenách: 1, 2, 3, 5, 8, 9
  4. Štvrtý prechod, 2 porovnania: 1 ≤ 2 2 ≤ 3 Žiadna výmena, algoritmus končí skôr.
  5. Spolu: 5 + 4 + 3 + 2 = 14 porovnaní a 3 + 2 + 2 = 7 výmen ✓

Základná verzia bez skorého ukončenia by urobila ešte piaty prechod s jedným porovnaním, spolu 6 · 5 / 2 = 15 porovnaní.

Príklad 2: Utrieď to isté pole triedením vkladaním.
  1. Vkladáme 2: 5 > 2 ↔ Prvok je na začiatku poľa. Utriedená časť: 2, 5
  2. Vkladáme 8: 5 ≤ 8 Zostáva na mieste. Utriedená časť: 2, 5, 8
  3. Vkladáme 1: 8 > 1 ↔ 5 > 1 ↔ 2 > 1 ↔ Utriedená časť: 1, 2, 5, 8
  4. Vkladáme 9: 8 ≤ 9 Zostáva na mieste. Utriedená časť: 1, 2, 5, 8, 9
  5. Vkladáme 3: 9 > 3 ↔ 8 > 3 ↔ 5 > 3 ↔ 2 ≤ 3 Výsledok: 1, 2, 3, 5, 8, 9
  6. Spolu: 1 + 1 + 3 + 1 + 4 = 10 porovnaní a 1 + 0 + 3 + 0 + 3 = 7 výmen ✓

Výmen je rovnako ako pri bublinkovom triedení. Nie je to náhoda: oba algoritmy každou výmenou odstránia jednu inverziu a toto pole ich má práve 7.

Príklad 3: Ukáž rozklad a zlučovanie pri triedení zlučovaním.

Polovica s nepárnym počtom prvkov sa delí tak, že ľavá časť je o jeden prvok kratšia (stred = len(a) // 2). Zlučuje sa v tomto poradí:

  1. [2] + [8] → [2, 8] 1 porovnanie, 2 zápisy.
  2. [5] + [2, 8] → [2, 5, 8] 2 porovnania, 3 zápisy.
  3. [9] + [3] → [3, 9] 1 porovnanie, 2 zápisy.
  4. [1] + [3, 9] → [1, 3, 9] 1 porovnanie, 3 zápisy. Po zápise jednotky je ľavá časť vyčerpaná, zvyšok pravej sa len pripojí.
  5. [2, 5, 8] + [1, 3, 9] → [1, 2, 3, 5, 8, 9] 5 porovnaní, 6 zápisov.
  6. Spolu: 1 + 2 + 1 + 1 + 5 = 10 porovnaní a 2 + 3 + 2 + 3 + 6 = 16 zápisov ✓
Príklad 4: Urob prvé delenie rýchleho triedenia (Lomutova schéma).
  1. Pivotom je posledný prvok: pivot = 3, i = 0
  2. j = 0: 5 < 3 neplatí, nič sa nemení.
  3. j = 1: 2 < 3 platí. Vymeníme a[0] a a[1]: 2, 5, 8, 1, 9, 3, i = 1
  4. j = 2: 8 < 3 neplatí.
  5. j = 3: 1 < 3 platí. Vymeníme a[1] a a[3]: 2, 1, 8, 5, 9, 3, i = 2
  6. j = 4: 9 < 3 neplatí.
  7. Pivot vymeníme s a[2]: 2, 1, 3, 5, 9, 8
  8. Delenie stálo 5 porovnaní a 3 výmeny. Pivot 3 je na konečnej pozícii 2, vľavo sú menšie prvky 2, 1, vpravo väčšie 5, 9, 8 ✓

Rovnako sa potom rozdelia úseky 2, 1 a 5, 9, 8. Celé triedenie tohto poľa potrebuje 8 porovnaní a 5 výmen.

Zložitosť: koľko práce triedenie stojí

Počet operácií závisí od počtu prvkov n. Pri bublinkovom triedení (v základnej verzii) aj pri triedení výberom sa v prvom prechode urobí n − 1 porovnaní, v druhom n − 2, v ďalšom zase o jedno menej, až napokon jediné. Sčítame ich ako členy aritmetickej postupnosti:

(n − 1) + (n − 2) + … + 2 + 1 = n · (n − 1) / 2

Pre n = 6 je to 5 + 4 + 3 + 2 + 1 = 15, čo sedí so vzorcom: 6 · 5 / 2 = 15.

Zápis O(n2) a O(n · log n)

Výraz n · (n − 1) / 2 sa dá upraviť na 0,5 · n2 − 0,5 · n. Pri veľkom n rozhoduje člen s n2, ostatné sú oproti nemu zanedbateľné. Preto hovoríme, že algoritmus má zložitosť O(n2) — čítame „rádovo n na druhú“. Znamená to: keď sa počet prvkov zdvojnásobí, práce je približne štyrikrát viac.

Triedenie zlučovaním a (v priemere) rýchle triedenie majú zložitosť O(n · log n). Logaritmus log2 n udáva, koľkokrát treba n prvkov rozpoliť, aby zostal jediný; pre milión prvkov je to približne 20. Pri takomto algoritme znamená dvojnásobný počet prvkov len o niečo viac ako dvojnásobok práce.

Pri malom poli je rozdiel nepatrný, pri veľkom obrovský:

Počet prvkov nn · (n − 1) / 2n · log2 n (približne)Pomer
1045331,4-krát
1 000499 5009 96650-krát
1 000 000499 999 500 00019 931 56925 000-krát

Počítač, ktorý stihne 100 miliónov porovnaní za sekundu, by milión prvkov triedil bublinkovým triedením približne 5 000 sekúnd, teda vyše 83 minút. Triedeniu zlučovaním by stačilo asi 0,2 sekundy.

Prehľad piatich algoritmov

AlgoritmusNajlepší prípadPriemerný prípadNajhorší prípadPamäť navyšeStabilný
BublinkovéO(n)O(n2)O(n2)O(1)áno
VýberomO(n2)O(n2)O(n2)O(1)nie
VkladanímO(n)O(n2)O(n2)O(1)áno
ZlučovanímO(n · log n)O(n · log n)O(n · log n)O(n)áno
RýchleO(n · log n)O(n · log n)O(n2)O(log n)nie

Najlepší prípad O(n) pri bublinkovom triedení platí pre verziu so skorým ukončením. O(1) znamená stále rovnaké množstvo pamäte bez ohľadu na veľkosť poľa — algoritmus triedi na mieste. Rýchle triedenie potrebuje pamäť na rekurziu, v priemere O(log n).

V bežných programoch netreba triedenie písať nanovo. V Pythone slúži na triedenie vstavaná funkcia sorted(a) a metóda a.sort(). Používajú algoritmus Timsort, ktorý spája triedenie zlučovaním s triedením vkladaním, je stabilný a má zložitosť O(n · log n).

🧠 Zapamätaj si

Vyskúšaj si to — triedenie krok za krokom

Vyber algoritmus, nechaj prednastavené pole alebo zadaj vlastné a stlač Prehrať. Tlačidlom Krok postupuješ po jednej operácii. Pri každom kroku vidíš, ktoré prvky sa porovnávajú alebo vymieňajú, ktorý riadok ukážky sa práve vykonáva a koľko porovnaní a výmen už algoritmus urobil.

Algoritmus

Zadaj 2 až 12 celých čísel od −99 do 99, oddelených čiarkou alebo medzerou. Čísla sa môžu opakovať.

Krok
0 z 0
Porovnania
0
Výmeny
0
  • nezaradený prvok
  • porovnávaná dvojica (plný rámček, znak ?)
  • práve vymenené prvky (prerušovaný rámček, znak ↔)
  • pivot (písmeno p pod stĺpcom)
  • utriedená časť (znak ✓ pod stĺpcom)
  • úsek, s ktorým algoritmus práve pracuje
  • prvok už presunutý do výsledku
  • práve zapísané do výsledku

Výška stĺpca zodpovedá hodnote. Pod stĺpcom je jeho pozícia (index) a názvy premenných z ukážky, ktoré naň práve ukazujú.

Ukážka v Pythone

Zvýraznený je riadok, ktorý sa v aktuálnom kroku vykonáva.

Pozri aj: anglický Sorting Algorithm Visualizer, ktorý má viac algoritmov a porovná dva vedľa seba, a tému Číselné sústavy.