TlačiťTlačiť Slovenčina English Hľadať RSS

© 2005 – 2024 Roman Horváth, všetky práva vyhradené. Dnes je 19. 4. 2024.

Stránka sa načítava, prosím čakajte…

Dátum: 25. 6. 2015, pred niekoľkými rokmi

Táto stránka obsahuje opis postupu pri implementácii algoritmu rýchleho triedenia (anglicky quick sort). Dá sa povedať, že táto stránka nadväzuje na reťaz stránok s opisom implementácie iných algoritmov, posledným bol algoritmus triedenia výberom. Je totiž vhodné, keď sa pred štúdiom tejto stránky budete zaoberať pochopením uvedených algoritmov.

 

Najskôr opíšme ako algoritmus rýchleho triedenia funguje. Algoritmus rýchleho triedenia vychádza z myšlienky, ktorá sa pokúša napodobniť nasledujúci intuitívny postup (samozrejme, že algoritmus to robí trošku efektívnejšie): Majme náhodne dlhý číselný rad. Za predpokladu, že triedime vzostupne (od najmenšieho prvku po najväčší) – rozdeľme ho na dve časti (nesúmerne) a zoberme ľubovoľný malý prvok z hornej časti číselného radu a súčasne veľký prvok zo spodnej časti radu. Vzájomne ich vymeňme. Opakujme to dovtedy, kým nie je rad čísiel zotriedený.

Prejdime k terminológii, ktorá je bližšie k programovaniu. Majme celočíselné pole. Chceme ho zotriediť vzostupne. Rozdeľme toto pole na dve časti (kritérium si povieme neskôr) a začnime ich prehľadávať. V „spodnej“ (ľavej) časti budeme hľadať prvý „veľký“ prvok (s vysokou hodnotou), v „hornej“ (pravej) časti zase prvý „malý“ prvok (s nízkou hodnotou). Vzájomne ich vymeňme. Toto opakujme až kým sa ukazovatele (indexy) prehľadávaní nepretnú. Zopakujme to pre každú „menšiu časť“ poľa, ktorá pri tom vznikne. Týmto postupom sa budú malé prvky postupne dostávať nižšie a veľké vyššie.

Zostáva spresniť to, ako pole rozdelíme a ako určíme tzv. „menšie časti“ poľa (v zmysle menších intervalov), pre ktoré budeme algoritmus vykonávať. Najlepšie by algoritmus fungoval vtedy, keby sme hodnoty prvkov poľa posudzovali podľa tzv. mediánu (stredná hodnota – rozdeľuje pole na dve rovnako početné časti). Lenže hľadanie mediánu by bolo výpočtovo také náročné, že by sa celá efektívnosť algoritmu stratila (v skutočnosti je najjednoduchším spôsobom na nájdenie mediánu zotriedenie poľa a vybratie prostredného prvku; lenže triedenie je predsa presne to, čo teraz robíme).

Preto sa v našej (a mnohých iných) implementácii jednoducho zoberie prvok, ktorý leží v strede prehľadávaného intervalu. Pozor, neznamená to, že tým rozdelíme interval na dve rovnaké polovice (to by sme dosiahli práve s pomocou mediánu). V skutočnosti ho rozdelíme veľmi neurčito: na „malé“ a „veľké“ hodnoty, pričom nevieme, kde presne ležia „malé“ alebo „veľké“ hodnoty. To sa ukáže až v priebehu vykonávania algoritmu.

Prvok, ktorým sa budeme riadiť (ten, ktorý vyberieme zo stredu poľa) sa nazýva pivot. Ako bolo naznačené vyššie: Náš pivot nebude rozdeľovať pole na dve rovnaké polovice, ale určí to, ktoré hodnoty budeme považovať za malé a ktoré za veľké. To, kde sa presne ukazovatele prehľadávaní pretnú, nie je nikdy vopred známe. Jednoducho sa zľava hľadajú hodnoty, ktoré sú väčšie alebo rovné pivotu a naopak.

Menšie prehľadávané intervaly poľa („menšie časti“ poľa) budeme určovať s pomocou hodnôt hraníc prehľadávaného intervalu a indexov prehľadávaní. Takže ide o vhodné porovnanie nasledujúcich štyroch hodnôt:

  • spodná hranica prehľadávaného intervalu: lo – na začiatku rovná nule, čo je index prvého prvku poľa,
  • horná hranica prehľadávaného intervalu: hi – na začiatku rovná hodnote o jedno menšie než je dĺžka poľa, čo je index posledného prvku poľa,
  • index prehľadávania prvkov zľava: i – na začiatku stotožnená so spodnou hranicou intervalu,
  • index prehľadávania prvkov sprava: j – na začiatku stotožnená s hornou hranicou intervalu.

Indexy prehľadávania (ij) sa počas vykonávania algoritmu k sebe postupne približujú až kým sa prekrížia. Menšie prehľadávané intervaly potom určíme takto:

  • Ak premenné loj vymedzujú aspoň dva prvky poľa (t. j. lo != j, resp. lo < j), tak hodnota lo bude nová spodná hranica prehľadávaného intervalu a j zase horná.
  • Ak to isté platí pre premenné ihi (v tomto poradí: i < hi), tak hodnota i bude nová spodná hranica a hodnota hi nová vrchná hranica intervalu.

Naša implementácia je rekurzívna. Keďže sú dve možnosti na určenie menšieho prehľadávaného intervalu, budú sa v zápise algoritmu vyskytovať dve rekurzívne volania, ktoré budú samozrejme podmienené.

 

Podobne ako pri predchádzajúcich triedeniach budeme napĺňať spočiatku prázdnu kostru programu:

~

public class QuickSort extends Konzola
{
	private int pole[] = new int[10];

	private QuickSort()
	{
		for (int i = 0; i < pole.length; ++i)
			pole[i] = (int)náhodnéCeléČíslo(1, 20);

		vypíšRiadok(pole);
		quickSort();
		vypíšRiadok(pole);
	}

	// Toto bude rekurzívna metóda rýchleho vyhľadávania:
	private void quickSort(int lo, int hi)
	{
		/*# Na tomto mieste budeme vytvárať algoritmus. */
	}

	/**
	 * Táto metóda spustí algoritmus rýchleho vyhľadávania na celom
	 * intervale poľa:
	 * od spodnej hranice (0 – prvý prvok poľa)
	 * po hornú (pole.length - 1 – posledný prvok poľa).
	 */
	public void quickSort()
	{
		quickSort(0, pole.length - 1);
	}

	/**
	 * Táto metóda vymení prvky poľa na pozíciách i a j.
	 */
	public void vymeň(int i, int j)
	{
		int c = pole[i];
		pole[i] = pole[j];
		pole[j] = c;
	}

	public static void main(String[] args)
	{
		new QuickSort();
	}
}

 

Najprv potrebujeme určiť pivot:

~

int stred = (lo + hi) / 2;
int pivot = pole[stred];

Predchádzajúce dva riadky môžeme zlúčiť do jedného (odpadne tým potreba definovania premennej stred):

~

int pivot = pole[(lo + hi) / 2];

(Prejdite na definíciu cyklu.)

 

Potom definujeme cyklus, ktorý bude v rámci zužujúceho sa intervalu vyhľadávať a vymieňať vhodné prvky. Cyklus bude potrebovať na svoju činnosť dve pomocné premenné – už sme ich spomínali, sú to indexy i a j. Index i bude inicializovaný (stotožnený so) spodnou hranicou intervalu lo a index j s hornou hranicou intervalu hi:

~

int i = lo, j = hi;

Cyklus sa bude vykonávať dovtedy, kým bude hodnota indexu i menšia/nanajvýš rovná hodnote indexu j:

~

while (i <= j)
{
	// …

(Prejdite na vyhľadávanie prvkov.)

 

V tele cyklu bude prvou úlohou nájdenie vhodných prvkov na výmenu. Pôjde o dve vyhľadávania – jedno bude postupovať zľava, druhé sprava. Pri obidvoch vyhľadávaniach sa budú hodnoty prvkov porovnávať s hodnotou pivotu. Vyhľadávanie, ktoré bude postupovať zľava bude hľadať prvok, ktorý je väčší/nanajvýš rovný hodnote pivotu. Pri tom druhom (tom, ktoré bude postupovať sprava) to bude naopak – prvok musí byť menší/nanajvýš rovný (hodnote) pivotu.

V slangu algoritmov by sa dal princíp obidvoch vyhľadávaní opísať napríklad takto:

  • Pri hľadaní zľava „nechaj na pokoji“ (t. j. preskoč) všetky prvky, ktoré sú malé.
  • Pri hľadaní sprava „nechaj na pokoji“ (t. j. preskoč) všetky prvky, ktoré sú veľké.

~

while (pole[i] < pivot) ++i; // vľavo „nechávam na pokoji“ malé prvky
while (pole[j] > pivot) --j; // vpravo „nechávam na pokoji“ veľké prvky

To znamená, že pri vyhľadávaní zľava nájdeme veľký prvok, ktorý chceme posunúť smerom ku koncu poľa a naopak, pri vyhľavávaní sprava nájdeme malý prvok, ktorý chceme posunúť smerom na začiatok poľa. (Potom tieto prvky vymeníme.)

V slangu algoritmov sa dá povedať, že: „Rýchle triedenie ‚prehadzuje‘ veľké prvky na koniec a malé na začiatok poľa podľa hodnoty pivotu.“

(Prejdite na výmenu prvkov.)

 

Po nájdení vhodných prvkov ich vymeníme, ale pozor(!) – len vtedy, ak je hodnota indexu i menšia/nanajvýš rovná hodnote indexu j. Po úspešnej výmene zároveň zvýšime hodnotu indexu i a znížime hodnotu indexu j:

~

if (i <= j)
{
	vymeň(i, j);
	++i;
	--j;
}

Nasledujúcu trojicu príkazov z bloku podmieneného spracovania môžeme zlúčiť do jedného príkazu:

~

vymeň(i, j);
++i;
--j;

a to s pomocou postfixovej inkrementácie a dekrementácie (hodnoty premenných sa totiž musia zmeniť až po výmene, keby sme použili prefixovú inkrementáciu/dekrementáciu, tak by sa zmenili ešte pred tým, a to by bola chyba):

~

vymeň(i++, j--);

Takže z troch príkazov vznikne jeden, symboly bloku { } môžeme odstrániť a celé podmienené spracovanie napísať takto:

~

if (i <= j) vymeň(i++, j--);

Týmto príkazom sa končí telo cyklu while (i <= j), ktorý tvorí jadro algoritmu. Teraz už len stačí vykonať rekurziu pre všetky menšie intervaly poľa a po skončení vykonávania rekurzie bude pole zotriedené.

(Prejdite na rekurziu.)

 

V úvode sme uviedli, že môžu vzniknúť dve rekurzívne volania. Prvé v intervale od hodnoty aktuálnej spodnej hranice lo po hodnotu indexu j, ktorá bola pôvodne rovná hornej hranici, ale len ak je hodnota indexu j väčšia než hranica lo (alebo inak povedané ak je hodnota hranice lo menšia než hodnota indexu j).

V slangu algoritmov môžeme povedať, že toto rekurzívne volanie prehľadá „to, čo (prípadne) zostalo ‚pod‘ pozíciou indexu j (vrátane), v rámci pôvodného intervalu.“

~

if (lo < j) quickSort(lo, j);

Podobne druhé (rekurzívne volanie) môže vzniknúť v intervale od hodnoty indexu i (ktorá bola pôvodne rovná spodnej hranici) po aktuálnu hornú hranicu hi, ale len ak je hodnota indexu i menšia než hodnota hranice hi.

Opäť, v slangu algoritmov môžeme povedať, že toto rekurzívne volanie prehľadá „to, čo (prípadne) zostalo ‚nad‘ pozíciou indexu i (vrátane), v rámci pôvodného intervalu.“

~

if (i < hi) quickSort(i, hi);

(Prejdite na úplný algoritmus.)

 

Úlpný algoritmus, ktorý je potrebné umiestniť do metódy quickSort(int lo, int hi), vyzerá takto:

~

public void quickSort(int lo, int hi)
{
	int pivot = pole[(lo + hi) / 2];
	int i = lo, j = hi;

	while (i <= j)
	{
		while (pole[i] < pivot) ++i;
		while (pole[j] > pivot) --j;
		if (i <= j) vymeň(i++, j--);
	}

	if (lo < j) quickSort(lo, j);
	if (i < hi) quickSort(i, hi);
}