IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Heapsort

Heapsort patrí medzi chytré algoritmy, ktoré sú rádovo rýchlejšie ako tie doteraz spomínané. Stavia na myšlienkach algoritme Selection sort a je teda založený na odtrhávanie extrému (v tomto prípade maxima), ktoré vždy presúva na koniec poľa. Po začlenení všetkých maxím na koniec máme určite pole zotriedené. Problém Selection sortu však bol práve vo hľadanie maxima alebo minima. V každom vonkajšom cykle sa celá Hlavička časť poľa musela prejsť a každý prvok skontrolovať, či nie je náhodou hľadaným maximom. V poli maximum rýchlejšie asi nenájdeme.

Ale čo keby existovala dátová štruktúra, v ktorej by sme mohli prvky reprezentovať rovnako, ako v poli, a zároveň sme maximum našli v konštantnom čase, bez jediného priechodu? Keď sa takto pýtam, asi vám je už jasné, že štruktúra existovať bude. Nazýva sa Halda (anglicky Heap, preto Heap sort čiže triedenie haldou).

Halda je binárnym stromom s nasledujúcimi vlastnosťami:

  • Všetky "poschodia" haldy až na poslednú sú plne obsadené prvky (teda každý vnútorný vrchol má práve 2 synov, strom je veľmi vyvážený)
  • Posledné poschodie haldy je zaplnené zľava (môže byť aj zaplnené celé)
  • Pre prvky v halde platí tzv. Špeciálna vlastnosť haldy: obaja synovia sú vždy menšie alebo rovné otcovi.

Zo špeciálne vlastnosti haldy nutne vyplýva, že v koreni bude vždy uložené maximum, čo sa nám veľmi hodí.

Halda môže vyzerať napríklad takto:

Halda – Heapsort - Triediace / radiacej algoritmy

Samozrejme si môžeme definovať špeciálnu vlastnosť haldy obrátene a mať v koreni minimum, aj to sa používa.

Pretože zvyčajne chceme triediť poľa a nie haldu, musíme si z tohto poľa haldu najprv postaviť. Teraz iste očakávate, že si vytvoríme stromovú štruktúru pomocou ukazovateľov alebo referencií a do nej budeme prvky pridávať. Haldu možno však malým trikom (vďaka jej vyváženie) reprezentovať v obyčajnom poli, nebudeme teda potrebovať žiadnu pomocnú štruktúru ani pamäť. Budeme pracovať priamo v našom poli, na ktoré sa budeme pozerať ako na haldu a môžeme teda triediť rovno na mieste.

Halda v obrázku vyššie by v poli vyzerala takto (hore indexy prvkov, dole prvky):

0 1 2 3 4 5 6 7 8 9
9 7 8 5 4 0 3 2 1 2
Všimnite si, že zhaldované pole nemá veľa spoločného s poľom setříděným. Teraz si ukážeme, ako s týmto poľom pracovať ako s haldou. Iste ste si všimli, že prvky v poli sú prvky tak, ako sú v halde, zoradené zhora nadol a zľava doprava. Keď budeme chcieť pristúpiť ku koreňu, jednoducho siahneme na index 0, posledný prvok haldy je tiež na indexe posledného prvku poľa. Budeme však potrebovať prístup z ľubovoľného otca do jeho synov a tiež zo syna k jeho otcovi.

Pokiaľ bude mať syn index i, index jeho otca vypočítame ako (i - 1) / 2 (delenie je celočíselné). Pokiaľ bude mať otec index i, jeho ľavý syn bude mať index (2 * i + 1) a pravý syn (2 * i) + 2.

Môžeme si rýchlo overiť, že to funguje. Skúsime nájsť syna prvku 7, ten má index 1 (pole je indexované od 0 a je druhý). (1 - 1) / 2 je 0, jeho otec je teda prvok 9, čo sedí. Jeho synovia budú (2 * 1 + 1) = 3 a na treťom indexu je päťka - naozaj jeho ľavý syn. Pravý syn má index o jednu väčšiu a naozaj je na ňom prvok 4.

Máme teda potrebné nástroje a môžeme sa pustiť do prvého kroku algoritmu - zhaldování poľa.

Priebeh

Zhaldování pole (postavenie haldy)

Máme pole nasledujúcich prvkov:

3 2 5 1 9 4
Pole nám reprezentuje nasledujúce "haldu":
Zhaldování pole – postavenie haldy – Heapsort - Triediace / radiacej algoritmy

Úvodzovky som použil zámerne, pretože táto halda je rozbitá - neplatí nám špeciálna vlastnosť haldy. Musíme ju teda opraviť a polia zhaldovat. Použijeme na to funkciu up (hore), ktorú budeme postupne volať na prvky od koreňa dole. Funkcia zistí, či daný prvok neporušuje špeciálnu vlastnosť haldy (teda ak nie je väčší ako jeho otec) a ak je, prvok s otcom prehodí. Tým sa však môže stať, že sa rovnaký problém prenesie o úroveň vyššie, preto sa funkcia opakuje, kým špeciálnu vlastnosť haldy pre daný prvok neplatí alebo kým nedôjde do koreňa. Funkciu voláme pre každý prvok dokiaľ nedôjde nakoniec, potom si môžeme byť istí, že sme haldu opravili.

Z dôvodu úspory miesta v článku len popíšem priebeh zhaldování, pozerajte sa pritom na obrázok vyššie. Môžete sa tiež pozrieť na video nižšie, kde je to podrobne znázornené.

Na prvý prvok (koreň, 3) funkciu up púšťať nebudeme, pretože ten žiadneho otca nemá. Začneme teda s prvkom 2, ten je menšia ako otec, necháme ho na mieste. Ďalšia je prvok 5, ten je väčší ako otec (3), preto ho s otcom prehodíme a sme už v koreni, takže končíme. Prvok 1 je menšia ako 2, to je v poriadku. 9 je však väčší ako 2, preto je prehodíme. 9 je však stále väčší ako 3, prehodíme teda znovu a 9 (maximum) sa dostáva do koreňa. 4 je väčší ako 3, preto prehodíme. 4 je potom menšia ako 9, to je už v poriadku. Zhaldované polia a halda, ktorú reprezentuje, vyzerajú nasledovne:

9 5 4 1 2 3
.<> Zhaldované pole – postavenie haldy – Heapsort - Triediace / radiacej algoritmy

Môžeme teda pristúpiť k samotnému triedenia.

Triedenie

Ako som sa už zmienil, budeme vykonávať vlastne Selection sort v halde, budeme teda prehadzovať maximum s posledným prvkom, potom ďalšie druhé maximum s predposledným atď. Maximum leží vždy na indexe 0, takže ho nájdeme v konštantnom čase. Prehodenie tiež žiadny čas nezaberie.

Menší problém tu však bude - prehodením prvkov si totiž celkom iste haldu rozbijeme. Pôvodného maxima vzadu si už síce všímať nebudeme (je to už zotriedená časť poľa, ktoré si rovnako ako v Selection sortu nevšímame), ale prvok, ktorý je teraz novým koreňom, dosť pravdepodobne nebude maximum. Haldu opravíme podobnú funkciu ako up, tentoraz funkcií down (dole). Tú pustíme na koreň a ak je niektorý sa synov väčšie ako otec, tak ho s otcom prehodíme. Ak sú väčšie obaja, prehodíme otca s väčším synom. Podobne ako pri funkcii up, aj tu sa nám problém môže posunú nadol, preto sa znovu pozrieme na syna (teraz už otca) a zistíme, či je väčší ako jeho synovia. Proces opakujeme, kým platia špeciálne vlastnosť haldy alebo kým prídeme do poslednej úrovne. Túto funkciu musíme zavolať po každom prehodenie koreňa s posledným prvkom.

Keď sa zamyslíme, zistíme, že odtrhnutie maxima nás nestojí vôbec nič, prehodenie tiež nie. Jediný časový problém je s funkciou down, ktorá sa bude vykonávať n krát (pre každé maximum) a v každom svojom zavolaní sa bude opakovať maximálne log n krát (pretože halda je vyvážený binárny strom, má výšku log n, kde základ logaritmy je 2, pretože každý vrchol má 2 synov. Funkcia preveruje jeden prvok v každom poschodí, v najhoršom prípade teda prejde všetky poschodia, ktorých je log n). U funkcie up je časová zložitosť je, ako pri funkcii down, teda opäť O (n log n). Celkovo zavoláme raz funkciu up a funkciu down potom n krát, teda výsledná časová zložitosť heapsortu je O (n log n).

Oproti skôr zmieneným algoritmom sme si výrazne polepšili a zrýchlenie na väčších poliach je obrovské. Algoritmus však nie je stabilný, takže náročné uspokojí až Merge sort alebo Quicksort.

Vlastnosti algoritmu

časová zložitosť O (n log n)
stabilita nie
rýchlosť veľmi dobrá
Pozn. je myslená časová zložitosť priemerného prípadu a rýchlosť vzhľadom ku všetkým triediacim algoritmom

Vizualizácia

Video ukážka

Zostavenie haldy z poľa

Z nejakého dôvodu sa nám pokazila halda a v druhom videu je v nej chyba (malé zaváhanie a následná oprava). Na behu algoritmu to nič nemení :)

Aplikovanie algoritmu na zhaldované pole

Zdrojový kód

//oprava haldy nahoru
public static void up(int[] list, int i) {
  int child = i; //ulozim syna
  int parent, temp;
  while (child != 0) {
  parent = (child - 1) / 2; //otec
  if (list[parent] < list[child]) { //detekce chyby
    temp = list[parent]; //prohozeni syna s otcem
    list[parent] = list[child];
    list[child] = temp;
    child = parent; //novy syn
  }
  else
    return; //ok
  }
}

//oprava haldy dolu
public static void down(int[] list, int last) {
  int parent = 0;
  int child, temp;
  while (parent * 2 + 1 <= last) {
    child = parent * 2 + 1;
    // pokud je vybran mensi syn
    if ((child < last) && (list[child] < list[child + 1]))
    child++; //vybereme toho vetsiho
    if (list[parent] < list[child]) { //detekce chyby
      temp = list[parent]; //prohozeni syna s otcem
      list[parent] = list[child];
      list[child] = temp;
      parent = child; //novy otec
    }
    else
      return;
    }
}

// postaveni haldy z pole
public static void heapify(int[] list) {
  for (int i = 1; i < list.length; i++)
  up(list, i);
}

// samotne trideni
public static void heapsort(int[] list) {
  heapify(list);
  int index = list.length - 1; // posledni prvek
  int temp;
  while (index > 0) {
    temp = list[0]; // prohození posledního prvku s maximem
    list[0] = list[index];
    list[index] = temp;
    index -= 1; //nastaveni noveho posledniho prvku
    down(list, index);
  }
}
//oprava haldy nahoru
public static void up(int[] list, int i) {
  int child = i; //ulozim syna
  int parent, temp;
  while (child != 0) {
    parent = (child - 1) / 2; //otec
    if (list[parent] < list[child]) { //detekce chyby
      temp = list[parent]; //prohozeni syna s otcem
      list[parent] = list[child];
      list[child] = temp;
      child = parent; //novy syn
    }
    else
      return; //ok
    }
}

//oprava haldy dolu
public static void down(int[] list, int last) {
  int parent = 0;
  int child, temp;
  while (parent * 2 + 1 <= last) {
    child = parent * 2 + 1;
    //pokud je vybran mensi syn
    if ((child < last) && (list[child] < list[child + 1]))
    child++; //vybereme toho vetsiho
    if (list[parent] < list[child]) { //detekce chyby
      temp = list[parent]; //prohozeni syna s otcem
      list[parent] = list[child];
      list[child] = temp;
      parent = child; //novy otec
    }
    else
      return;
    }
}

// postaveni haldy z pole
public static void heapify(int[] list) {
  for (int i = 1; i < list.Length; i++)
  up(list, i);
}

// samotne trideni
public static void heapsort(int[] list) {
  heapify(list);
  int index = list.Length - 1; //posledni prvek
  int temp;
  while (index > 0) {
    temp = list[0]; // prohození posledního prvku s maximem
    list[0] = list[index];
    list[index] = temp;
    index -= 1; //nastaveni noveho posledniho prvku
    down(list, index);
  }
}
// oprava haldy nahoru
procedure up(var list: array of integer; i: integer);
var parent, child, temp: integer;
begin
  child:=i; // ulozim syna
  while (child <> 0) do begin
    parent:=(child - 1) div 2; // otec
    if (list[parent] < list[child]) then begin // detekce chyby
      temp:=list[parent]; // prohozeni syna s otcem
      list[parent]:=list[child];
      list[child]:=temp;
      child:=parent; // novy syn
    end
    else exit; // OK
  end;
end;

// oprava haldy dolu
procedure down(var list: array of integer; last: integer);
var parent, child, temp: integer;
begin
  parent:=0;
  while (parent * 2 + 1 <= last) do begin
    child:=parent * 2 + 1;
    // pokud je vybran mensi syn
    if ((child < last) and (list[child] < list[child + 1])) then
    inc(child); // vybereme toho vetsiho
    if (list[parent] < list[child]) then begin // detekce chyby
      temp:=list[parent]; // prohozeni syna s otcem
      list[parent]:=list[child];
      list[child]:=temp;
      parent:=child; // novy otec
    end else exit;
  end;
end;

// postaveni haldy z pole
procedure heapify(var list: array of integer);
var i: integer;
begin
  for i:=1 to (length(list) - 1) do
  up(list, i);
end;

// samotne trideni
procedure heapsort(var list: array of integer);
var temp, index: integer;
begin
  heapify(list);
  index:=length(list) - 1; // posledni prvek
  while (index > 0) do begin
    temp:=list[0]; // prohozeni posledniho prvku s maximem
    list[0]:=list[index];
    list[index]:=temp;
    dec(index); // nastaveni noveho posledniho prvku
    down(list, index);
  end;
end;
# oprava haldy nahoru
def up(list, i)
  child = i # ulozim syna
  while (child != 0)
    parent = (child - 1) / 2 # otec
    if (list[parent] < list[child]) # detekce chyby
      temp = list[parent] # prohozeni syna s otcem
      list[parent] = list[child]
      list[child] = temp
      child = parent # novy syn
    else
      return # OK
    end
  end
end

# oprava haldy dolu
def down(list, last)
  parent = 0
  while (parent * 2 + 1 <= last)
    child = parent * 2 + 1
    # pokud je vybran mensi syn
    if ((child < last) && (list[child] < list[child + 1]))
      child += 1 # vybereme toho vetsiho
    end
    if (list[parent] < list[child]) # detekce chyby
      temp = list[parent] # prohozeni syna s otcem
      list[parent] = list[child]
      list[child] = temp
      parent = child # novy otec
    else
      return
    end
  end
end

# postaveni haldy z pole
def heapify(list)
  1.upto(list.length - 1) { |i| up(list, i) }
end

# samotne trideni
def heapsort(list)
  heapify(list)
  index = list.length - 1 # posledni prvek
  while (index > 0)
    temp = list[0] # prohozeni posledniho prvku s maximem
    list[0] = list[index]
    list[index] = temp
    index -= 1 # nastaveni noveho posledniho prvku
    down(list, index)
  end
end
Implementácia tohto algoritmu je založená na materiáloch Unicorn College od profesora Ondreja Kučeru.

 

Všetky články v sekcii
Triediace / radiacej algoritmy
Článok pre vás napísal David Hartinger
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
David je zakladatelem ITnetwork a programování se profesionálně věnuje 15 let. Má rád Nirvanu, nemovitosti a svobodu podnikání.
Unicorn university David sa informačné technológie naučil na Unicorn University - prestížnej súkromnej vysokej škole IT a ekonómie.
Aktivity