30.Prednaska

Z Pascal
Prejsť na: navigácia, hľadanie
30. Prednáška

úlohy | cvičenie


Triedenie

Algoritmus triedenia má za úlohu vzostupne (alebo zostupne) usporiadať nejakú údajovú štruktúru podľa niektorých hodnôt. Pre jednoduchosť budeme vzostupne usporadovať postupnosť celých čísel. U všetkých triedení okrem triedenia zlučovaním (MergeSort) budeme triediť jednorozmerné N-prvkové celočíselné pole Pole a niekedy použijeme pomocnú procedúru Vymen. Spoločné deklarácie pre všetky triedenia:

var
  Pole: array [0..N - 1] of Integer;
 
procedure Vymen(I, J: Integer);
var
  T: Integer;
begin
  T := Pole[I];
  Pole[I] := Pole[J];
  Pole[J] := T;
end;

Pri rôznych algoritmoch triedení nás bude zaujímať ako efektívne tento pracuje. Pre malý počet prvkov asi nie je veľmi podstatné, či algoritmus bežal 5 alebo 6 milisekúnd, ale pre väčšie postupnosti hodnôt (napr. milión) už môže zavážiť, či triedenie pobeží 30 alebo 1 000 000 sekúnd. Vzhľadom na efektívnosť algoritmu sú dôležité informácie napr. o počte porovnávaní a počte výmen medzi prvkami.



Vizualizácia triedenia



Priebeh algoritmu triedenia sa dá rôzne vizualizovať, napr. [1], [2], [3] alebo [4].

My na testovanie rôznych triedení použijeme tento projekt, v ktorom zadefinujeme triedu TSort na vizualizáciu priebehu triedenia. Samotný triediaci algoritmus sa bude nachádzať v metóde Sort. Po zatlačení tlačidla Button1 sa vygeneruje náhodná postupnosť čísel, ktorú bude treba utriediť. Do algoritmu triedenia sme pridali aj zvyšovanie hodnoty premennej Pocet po každom porovnaní, aby sme lepšie videli "zložitosť" tohto algoritmu.

Potom samotný projekt vyzerá takto:

procedure TForm1.Button1Click(Sender: TObject);
var
  Sort: TSort;
  Pole: TPole;
  I: Integer;
begin
  Image1.Canvas.FillRect(Image1.ClientRect);
  SetLength(Pole, Image1.Width div 2);
  for I := 0 to High(Pole) do
    Pole[I] := Random(Image1.Height - 15) + 10;
  Sort := TSort.Create(Pole, Image1);
  Sort.Sort;
  Label2.Caption := 'počet porovnaní = ' + IntToStr(Sort.Pocet);
  Sort.Free;
end;

A definícia triedy TSort:

type
  TPole = array of Integer;
 
  TSort = class
  private
    Image: TImage;
    Vyska, Sirka: Integer;
    FPole: TPole;
    function DajN: Integer;
    function DajPrvokPola(Index: Integer): Integer;
    procedure PriradDoPola(Index, Hodnota: Integer);
    procedure KresliPrvok(Index: Integer; Farba: TColor);
    procedure Kresli(Farba: TColor);
    procedure Vymen(Index1, Index2: Integer);
    property Pole[Index: Integer]: Integer read DajPrvokPola write PriradDoPola;
    property N: Integer read DajN;
  public
    Pocet: Integer;                  // počet porovnaní
    constructor Create(const NovePole: TPole; NovyImage: TImage);
    procedure Sort;
  end;
 
{ TSort }
 
constructor TSort.Create(const NovePole: TPole; NovyImage: TImage);
begin
  FPole := Copy(NovePole, 0, Length(NovePole));
  Vyska := NovyImage.Height;
  Sirka := NovyImage.Width div Length(NovePole);
  Image := NovyImage;
  Kresli(clBlue);
  Pocet := 0;
end;
 
function TSort.DajPrvokPola(Index: Integer): Integer;
begin
  Result := FPole[Index];
end;
 
function TSort.DajN: Integer;
begin
  Result := Length(FPole);
end;
 
procedure TSort.KresliPrvok(Index: Integer; Farba: TColor);
begin
  with Image.Canvas do
  begin
    Pen.Color := Farba;
    MoveTo(Sirka * Index, Vyska);
    LineTo(Sirka * Index, Vyska - FPole[Index] - 1);
  end;
end;
 
procedure TSort.Vymen(Index1, Index2: Integer);
var
  T: Integer;
begin
  if Index1 = Index2 then
    Exit;
//  T := Pole[Index1]; Pole[Index1] := Pole[Index2]; Pole[Index2] := T;
  KresliPrvok(Index1, clWhite);
  KresliPrvok(Index2, clWhite);
  T := FPole[Index1];
  FPole[Index1] := FPole[Index2];
  FPole[Index2] := T;
  KresliPrvok(Index1, clBlue);
  KresliPrvok(Index2, clBlue);
  Image.Repaint;
end;
 
procedure TSort.PriradDoPola(Index, Hodnota: Integer);
begin
  if FPole[Index] = Hodnota then
    Exit;
  KresliPrvok(Index, clWhite);
  FPole[Index] := Hodnota;
  KresliPrvok(Index, clBlue);
  Image.Repaint;
end;
 
procedure TSort.Kresli(Farba: TColor);
var
  I: Integer;
begin
  for I := 0 to High(FPole) do
    KresliPrvok(I, Farba);
  Image.Repaint;
end;
 
// sem príde nejaký konkrétny algoritmus triedenia:
 
procedure TSort.Sort;
begin
  // algoritmus triedi jednorozmerné pole Pole 
  // prvky poľa sa indexujú od 0 do N-1
  // procedúra by mala zvýšovať o 1 obsah premennej Pocet (počítadlo porovnaní)
  //     pri každom porovnávaní hodnôt dvoch prvkov poľa
end;


Bublinkové triedenie (Bubble Sort)

Toto triedenie - hoci je asi najznámejšie (napr. wikipedia) - je z hľadiska efektívnosti skoro najhoršie, aké môže byť - môžeme ho použiť len pre malé polia v situácii, keď nezáleží na rýchlosti. Pracuje na princípe postupného porovnávania všetkých dvojíc susediacich prvkov: do prvku poľa s menším indexom ide menší z nich. Po jednom prechode celým poľom sa maximálny prvok určite dostane na koniec poľa. Potom ostáva utriediť N-1 prvkov poľa (posledný je už na svojom mieste):

procedure TSort.Sort;
var
  I, J: Integer;
begin
  for I := N - 2 downto 0 do
    for J := 0 to I do
    begin
      if Pole[J] > Pole[J + 1] then
        Vymen(J, J + 1);
      Inc(Pocet);
    end;
end;

Poznámky:

  • počet porovnaní je vždy N*(N-1)/2 (aj keby bolo pole už dopredu utriedené)
  • počet výmen je v "najhoršom prípade" N*(N-1)/2
  • algoritmus sa dá trochu "vylepšiť": vonkajší for-cyklus sa nahradí repeat-cyklom s takou podmienkou ukončenia, ktorá skončí cyklus, ak vnútorný for-cyklus nespravil ani jednu výmenu

Min Sort

V literatúre sa niekedy dá nájsť aj názov Selection Sort.

Nájde v úseku poľa minimálny prvok, vymení ho s prvým prvkom úseku a posunie dolnú hranicu triedeného úseku o 1 (t.j. triedený úsek poľa sa skráti o 1), atď.

procedure TSort.Sort;
var
  I, J, Min: Integer;
begin
  for I := 0 to N - 2 do
  begin
    Min := I;
    for J := I + 1 to N - 1 do
    begin
      if Pole[J] < Pole[Min] then
        Min := J;
      Inc(Pocet);
    end;
    Vymen(I, Min);
  end;
end;

Poznámky:

  • porovnaní je rovnako ako pri triedení BubbleSort, ale výrazne menej výmen (maximálne N-1)
  • toto triedenie môžeme použiť vtedy, keď porovnanie dvoch prvkov poľa je rýchle a pomalú výmenu dvoch prvkov potrebujeme urobiť podľa možnosti čo najmenejkrát
  • naprogramujte analogicky fungujúci "Max Sort" (v každom prechode cyklu nájde maximálny prvok a vymení ho s prvkom na začiatku poľa)


Triedenie vsúvaním (Insert Sort)

Predpokladajme, že istá časť poľa je už utriedená. Počas tohto triedenia sa ďalší (ešte neutriedený) prvok zaradí na svoje miesto a tým sa predĺži utriedený úsek o 1. Takéto triedenie nazývame triedenie vsúvaním, t.j. InsertSort (pozri napr. wikipediu).Opäť je to veľmi pomalý algoritmus - porovnateľný s Bubble Sortom.

Námety:

  • naprogramujte triedenie vsúvaním: prechádzame už utriedenú časť poľa od konca a všetky väčšie prvky posúvame vpravo, tým sa vyrobí voľné miesto pre vsúvaný prvok
  • algoritmus by sa dal urýchliť, ak by sme miesto na vsunutie nového prvku hľadali binárnym vyhľadávaním a zároveň by sme dokázali čo najrýchlejšie rozsunúť prvky poľa (napr. štandardnou procedúrou Move)


Triedenie zlučovaním (Merge Sort)

Vychádza z metódy rozdeľuj a panuj, t.j. ľahšie sa utriedi menej položiek ako veľa. Tento algoritmus môžeme popísať takto (pozri napr. wikipediu):

  • pole najprv rozdelíme na dve približne rovnako veľké časti (pri nepárnom počte je jedna časť o 1 väčšia),
  • ďalej sa budeme zaoberať každou z týchto dvoch častí zvlášť a to takým istým spôsobom, t.j. rozdelíme ich na dve časti,
  • znovu vezmeme prvú z nich a rozdelíme ju na dve, atď. ... až kým nedostaneme jednoprvkové úseky,
  • je zrejmé, že jednoprvkové úseky sú vždy utriedené,
  • teraz použijeme druhú časť algoritmu: zlúčenie dvoch utriedených častí tak, aby aj novovzniknutá časť bola utriedená, t.j. dostávame časť s dvoma položkami,
  • podobne sa rozdelí a zlúči aj druhá časť z rozdelenia a dostávame utriedenú druhú dvojprvkovú časť,
  • ten istý algoritmus zlúčenia dvoch utriedených častí použijeme aj teraz a dostávame utriedenú štvorprvkovú časť ...
  • algoritmus sa bude opakovať, až kým nebude utriedené celé pole.

Toto triedenie sa používa najmä pri triedení prvkov spájaných zoznamov - zlučovanie dvoch polí bez pomocného poľa je veľmi náročná operácia a môže pokaziť efektívnosť celého algoritmu. Toto triedenie patrí medzi tzv. rýchle triedenia - "efektívnosť" je rádovo N*log2 N.

Námety:

  • usporiadať prvky spájaného zoznamu
  • usporiadať v poli pomocou pomocného poľa
  • usporiadať v poli bez pomocného poľa (asi bude priveľa posunov poľa)
  • triediť binárny súbor s dvoma pomocnými súbormi
  • triediť zoznam (alebo pole) bez rekurzie


Rýchle triedenie (Quick Sort)

Je to najznámejší algoritmus triedenia, ktorý v roku 1960 vymyslel Hoare (pozri napr. wikipediu). V predchádzajúcom triedení sme museli dva už utriedené zoznamy zlúčiť tak, aby aj výsledok bol utriedený. Triedenie QuickSort na rozdiel od MergeSortu, vytvorí dve samostatné časti poľa tak, aby ich už nebolo treba nejako špeciálne zlučovať, ale aby ich stačilo iba položiť za seba:

  • v prvom kroku sa jeden prvok vyberie za tzv. pivota (v princípe to môže byť ľubovoľný prvok, my vyberieme prvý prvok z danej postupnosti)
  • podľa tohto pivota rozdelíme vstupnú postupnosť prvkov na dve časti: v prvej z nich budú len čísla menšie ako pivot a v druhej zase len čísla väčšie a rovné ako pivot (ideálne by bolo, keby sa postupnosť rozdelila na dve približne rovnako veľké časti, v najhoršom prípade to môže dopadnúť tak, že v jednej časti je nula prvkov a v druhej sú všetky ostatné prvky)
  • po takomto rozdelení postupnosti dostávame tri časti - časť s jediným prvkom, ktorým je pivot; časť, v ktorej sú prvky menšie ako pivot; a časť s prvkami väčšími alebo rovnými ako je pivot
  • potom rovnakým spôsobom utriedime druhú časť (prvky menšie ako pivot) a tretiu časť (prvky väčšie alebo rovné ako pivot)
  • po ich utriedení druhú a tretiu časť "položíme" vedľa seba, pričom pivot dáme na správne miesto

Rýchle triedenie - QuickSort:

procedure TSort.Sort;
 
  procedure Rozdel(Z, K: Integer; var Ip: Integer);
  var
    Pivot: Integer;
    I: Integer;
  begin
    Pivot := Pole[Z];          // pivot je prvý prvok
    Ip := Z;
    for I := Z + 1 to K do     // prejdi všetky prvky medzi Z+1 a K
    begin
      if Pole[I] < Pivot then  // I-ty prvok patrí do prvej časti poľa
      begin                    // teraz je o jeden viac takých, čo sú menšie ako Pivot
        Inc(Ip);
        Vymen(Ip, I);
      end;
      Inc(Pocet);
    end;
    Vymen(Z, Ip);           // daj Pivot na správne miesto
    // v Ip je teraz index, na ktorom je umiestnený pivot
  end;
 
  procedure Quick(Z, K: Integer);
  var
    IPivot: Integer;    // index, kde je Pivot
  begin
    if Z >= K then
      Exit;
    Rozdel(Z, K, IPivot);
    Quick(Z, IPivot - 1);
    Quick(IPivot + 1, K);
  end;
 
begin
  Quick(0, N - 1);
end;

Poznámky:

  • procedúra Quick je rekurzívna a triedi časť poľa medzi indexmi Z a K, pričom najprv volá procedúru Rozdel na rozdelenie tejto časti a zistenie indexu pivota, potom aplikuje ten istý algoritmus na časť poľa pred indexom, na ktorom je pivot a na časť poľa za pivotom
  • procedúra Rozdel dáva prvky menšie ako pivot na začiatok poľa, prvky väčšie alebo rovné ako pivot na koniec poľa a po skončení tohto upratovania dá ešte pivot na správne miesto (medzi tieto dve časti) a vráti jeho index v parametri Ip
  • ak bolo pole už utriedené ešte pred samotným triedením, tak je zvolený najhorší možný pivot, preto sa na začiatku procedúry Rozdel zvykne pridať:
Vymen(Z, (Z + K) div 2);

Ďalšie námety:

  • v praxi sa väčšinou používa bez rekurzie - preprogramujte toto triedenie bez rekurzie
  • triediť spájaný zoznam
  • triediť binárny súbor
  • prerobte procedúru Rozdel na funkciu s dvoma parametrami Z a K, ktorá vráti index pivota


Radix Sort

Mohli by sme si ho predstaviť ako triedenie po cifrách (pozri napr. wikipediu):

  • pripravíme K pomocných kôpok, do ktorých budeme prehadzovať hodnoty zo vstupnej postupnosti
  • nech K je počet rôznych hodnôt, ktoré môžu nadobúdať jednotlivé cifry (napr. pre čísla 10, pre znaky 26 a pod.)
  • v prvom prechode postupne prehadzujeme všetky hodnoty do príslušnej kôpky podľa poslednej cifry
  • potom všetky kôpky spojíme a tým vytvoríme opäť jednu postupnosť
  • v druhom prechode prehadzujeme hodnoty do kôpok podľa predposlednej cifry
  • opäť ich potom spojíme do jednej postupnosti
  • toto postupne opakujeme pre všetky cifry - na záver je celá postupnosť utriedená vzostupne

Takto sa dobre triedia spájané zoznamy. Pri práci s poliami si tento algoritmus vyžaduje veľa pomocných polí.

Ďalšie námety:

  • naprogramujte toto triedenie pre spájané zoznamy, pole, binárny súbor


späť | ďalej