Řadící algoritmy v Ruby

Jako přípravu ke zkoušce z kurzu Algoritmy jsem v Ruby implementoval čtyři řadící algoritmy. Ruby jsem pro selectsort, insertsort, heapsort a quicksort zvolil, protože se v něm potřebuju trochu procvičit a taky mi to přišlo docela zajímavé. Ke každému algoritmu se sluší napsat, jaké má vlastnosti, takže článek je spíše studijní než v praxi použitelný.

Všechny algoritmy jsou psány pro řazení pole čísel typu integer, které je definováno jako globální. Při své implementaci jsem používal pole definované takto:

$A = [2, 5, 7, 9, 1, 3, 4, 6, 8, 0, 1]

(Znak $ v názvu proměnné označuje v jazyce Ruby globální proměnné.)

Selectsort

Selectsort je algoritmus pracující na principu výběru. Sestává ze dvou cyklů. První cyklus prochází vždy jen neseřazenou část pole. Jinak řečeno začíná nad celým polem o velikosti N, ale každý nový i-tý průchod, kde i = 1..N, prochází vždy o jednu položku méně, tedy vždy N - i položek. Druhý vnořený cyklus hledá ve stále se zmenšující neseřazené části nejmenší hodnotu. Po dokončení druhého cyklu se nejmenší hodnota vymění s i-tou hodnotou cyklu prvního.

Složitost: kvadratická, O(N2)
Stabilita: nestabilní
Přirozenost: nepřirozená

Výhody: snadná implementace
Nevýhody: nízká rychlost

def selectSort
   $A.each_index do |i|
      min = i
      # hledání minima možno nahradit $A.min
      for x in i...$A.length
         min = x if $A[min] > $A[x]
      end
      $A[i], $A[min] = $A[min], $A[i]
   end
end

Insertsort

Insertsort je metoda jednoduchá metoda, která vkládá jednotlivé prvky přímo na své místo. Pole je rozděleno na dvě části - seřazenou část a neseřazenou část - tak, že na začátku je seřazená část tvořena jedním (prvním) prvkem. Algoritmus vyjme první prvek v neseřazené části, takže se jeho místo uvolní. Pokud je vyjmutý prvek větší než největší seřazený prvek, vrátí jej zpět. V opačném případě se největší seřazený prvek posune na uvolněné místo, čímž vznikne volný prostor na jeho původní pozici a vyjmutý prvek se může stejným způsobem porovnat s jeho menším následovníkem. Je-li vyjmutý prvek nejmenší, dojde takto až na začátek pole.

Složitost: kvadratická, O(N2)
Stabilita: stabilní
Přirozenost: přirozená

Výhody: snadná implementace, v nejlepším případě je jeho složitost lineární
Nevýhody: obecně pomalejší algoritmus

def insertSort
   for i in 1...$A.length
      value = $A[i] # hodnota prvního neseřazeného prvku
      j = i - 1 # index největšího seřazeného prvku
      while $A[j] > value and j >= 0
         $A[j + 1] = $A[j] # posun na volné místo
         j += 1
      end
      $A[j + 1] = value # insert
   end
   $A
end

Heapsort

Řazení hromadou je velmi elegantní a pokročilý řadící algoritmus. Pole je nejdříve přeměněno na stromovou strukturu zvanou hromada (halda), která má tu vlastnost, že její vrchol (první prvek) je největší ze všech prvků. Výměnou vrcholu hromady s posledním prvkem pole hromadu sice porušíme, ale efektivně tak řadíme od konce největší prvky, dokud nemá zmenšující se hromada velikost jednoho prvku, nejmenšího v poli. Porušenou hromadu musíme vždy opravit ("zatřepat s ní"), což je provedeno funkcí sift.

Složitost: lineárně logaritmická, O(N * log2N)
Stabilita: nestabilní
Přirozenost: nepřirozená

Výhody: vždy lineárně logaritmická složitost
Nevýhody: nestabilní

def sift(root, length)
   i = root # kořen
   j = 2 * i # levý syn
   value = $A[i] # hodnota kořene
   while j <= length
      if j <  length # uzel má oba syny
         # výběr většího syna
         j += 1 if $A[j] < $A[j + 1]
      end
      if $A[j] < value
         break # kořen je větší, hotovo
      else
         $A[i] = $A[j] # syn stoupá výš
         i = j # syn se stává příštím otcem
         j = 2 * i
      end
   end
   $A[i] = value # uložení původního uzlu
end
 
def heapSort
   $A.insert(0,0) # vyplnění nultého prvku kvůli indexům
   last = $A.length - 1
   (last / 2).downto 1 do |l| # vytvoření hromady
      sift(l, last)
   end
   last.downto 2 do |l| # řazení hromadou
      $A[1], $A[l] = $A[l], $A[1] # vyjmutí největšího
      sift(1, l - 1)
   end
   $A.shift # smazání nultého prvku
   $A
end

Quicksort

Obecně nejrychlejší řadící metoda, která je založena na principu rozdělení. Pole je rozděleno na dvě části prvkem zvaným pivot a poté jsou prvky uspořádány tak, že nalevo od něj jsou prvky menší (než tento pivot) a napravo prvky větší. Tyto části jsou znovu rekurzivně děleny a řazeny na stále menší, až dokud je co dělit. Poté je pole seřazeno. Rychlost algoritmu je ovlivněna nalezením vhodného pivotu, což může být provedeno hned několika způsoby. Pivot se často hledá pomocí pseudomediánu tak, jak to původně vymyslel C. A. R. Hoare, vynálezce quicksortu. Quicksort jde implementovat rekurzivně(zde uvedeno) i nerekurzivně(nutno použít explicitní zásobník).

Složitost: obecně lineárně logaritmická, O(N * log2N)
Stabilita: nestabilní
Přirozenost: nepřirozená

Výhody: na náhodných datech a při dobré volbě pivota nejrychlejší
Nevýhody: v nejhorším případě má kvadratickou složitost O(N * log2N), nestabilní

# wrapper pro počátek rekurze
def quickSort
   qsort(0, $A.length - 1)
   $A
end
 
def qsort(left, right)
   i = left
   j = right
   pseudomedian = $A[(i+j) / 2]
   # hledání pivotu podle C.A.R.Hoare
   until i > j
      while $A[i] < pseudomedian
         i += 1
      end
      while $A[j] > pseudomedian
         j -= 1
      end
      if i <= j
         $A[i], $A[j] = $A[j], $A[i]
         i += 1
         j -= 1
      end
   end
   # zanoření do obou intervalů
   qsort(left, j)  if left < j
   qsort(i, right) if i < right
end

 

Víte, jak napsat tyto algoritmy, aby byly více "rubínové"? Dejte mi, prosím, vědět v komentáři.