9. Stromy

9.5 Binárny vyhľadávací strom

Veľmi užitočným prípadom binárneho stromu je tzv. binárny vyhľadávací strom (binary search tree). Využíva sa výhodne v mnohých triediacich a vyhľadávacích algoritmoch, kde sa kladie dôraz na rýchlosť. Má špeciálnu vlastnosť:
 
Vrcholy stromu sú ohodnotené celými číslami. Pre každý vrchol platí, že v ľavom synovi je uložená menšiav pravom synovi hodnota väčšia ako v danom vrchole. Je teda charakteristický tým, že všetky hodnoty v ľavom podstrome sú menšie a v pravom podstrome zase väčšie ako hodnota v koreni stromu. To isté platí pre ľubovoľný podstrom.
 
Na obrázku je príklad binárneho vyhľadávacieho stromu:
 

 
Napíšeme funkciu na vloženie novej hodnoty do binárneho vyhľadávacieho stromu:

typedef struct vrchol {
	int info;
	struct vrchol *ls, *ps;
} TVrchol;


void vloz(TVrchol **koren, int x)
{
	if (*koren == NULL) {
		*koren = (TVrchol*)malloc(sizeof(TVrchol));
		(*koren)->info = x;
		(*koren)->ls = NULL;
		(*koren)->ps = NULL;
	}
	else {
		if (x < (*koren)->info) vloz(&((*koren)->ls), x);
		else vloz(&((*koren)->ps), x);
	}
}

Pozor!
Prvý parameter musí byť volaný odkazom – t. j. ide pointer na pointer na typ TVrchol. Pri vkladaní nového prvku ako syna nejakého vrcholu musíme totiž po jeho vytvorení v pamäti nastaviť v otcovi príslušný ukazovateľ! Ukazovateľ na pridaný vrchol sa vyvezie „o poschodie“ vyššie.


Či sa nejaká hodnota nachádza v binárnom vyhľadávacom strome, môžeme zistiť iteračne. Prechádzame stromom od koreňa, pokračujeme v jednom z podstromov, podľa toho, či je hľadaná hodnota väčšia alebo menšia ako hodnota v koreni. Funkcia najdi vracia ukazovateľ na nájdený vrchol resp. NULL ak sa vrchol s takou hodnotou v strome nenachádza.

TVrchol *najdi(int x, TVrchol *koren)
{
	int najdeny = 0; /* false */

	while ((koren != NULL) && (!najdeny)) {
		if (koren->info == x) najdeny = 1; /* true */
		else if (x < koren->info) koren = koren->ls;
			else koren = koren->ps;
	}
	return (koren);
}


Rušenie vrcholov môžeme považovať za inverzný problém k pridávaniu vrcholov do stromu. Je však o niečo zložitejší. Ak je vrchol listom alebo má jediného syna, riešenie je jednoduché:
 
Zrušenie vrcholu bez synov:
Potrebujeme poznať ukazovateľ na otca rušeného vrcholu. Nech je to smerník v a chceme zrušiť jeho ľavého syna. Stačia na to dva príkazy:


 
free((void*)v->ls);
v->ls = NULL;

 

Zrušenie vrcholu s jediným synom:
Potrebujeme poznať ukazovateľ v na otca rušeného vrcholu. Ten nech je napr. jeho ľavým synom. Nech má rušený vrchol jediného napr. pravého syna. Zapamätáme si ukazovateľ na rušený vrchol (on), prepojíme otca s novým synom a zrušíme príslušný vrchol:


on = v->ls;
v->ls = on->ps
free((void*)on);

 

Situácia sa skomplikuje, ak potrebujeme odstrániť vrchol, ktorý má dvoch synov (je vo vnútri stromu) – nemôžeme predsa nahradiť jeden smerník dvoma! Preto sa v takýchto prípadoch rušený prvok nahradí buď najpravejším prvkom ľavého podstromu alebo najľavejším prvkom pravého podstromu.

Ak by sme chceli zo stromu na úvodnom obázku odstrániť napr. vrchol s hodnotou 15, nájdeme najpravejší prvok jeho ľavého podstromu – má hodnotu 12. Nahradíme hodnotu 15 novou hodnotou a najpravejší vrchol zrušíme. Ak by mal najpravejší vrchol ľavého syna, nastavíme ešte príslušný ukazovateľ vo vrchole 10 na tohto syna resp. na NULL ak žiadneho nemá (ako v tomto prípade).
 


Uvedená funkcia rozlišuje všetky tri spomenuté prípady:

TVrchol *on;  /* ukazovateľ na rušený prvok */

void nahrad(TVrchol **v)   /* pomocná funkcia */
{
	if ((*v)->ps != NULL) nahrad(&(*v)->ps);
	else {
		on->info = (*v)->info;
		on = *v;
		*v = (*v)->ls;
	}
}

void odstran(int x, TVrchol **koren)
{
	if (*koren == NULL) printf(„Take cislo v strome nie je\n“);
	else {
		if (x < (*koren)->info) odstran(x, &(*koren)->ls);
		else if (x > (*koren)->info) odstran(x, &(*koren)->ps);
			else {
				on = *koren;
				if (on->ps == NULL) *koren = on->ls;
				else if (on->ls == NULL)  *koren = on->ps;
					else nahrad(&on->ls);
				free((void*)on);
			}
	}
}

Funkcia nahrad sa volá len v treťom prípade – nájde najpravejší prvok ľavého podstromu, nahradí hodnotu rušeného vrcholu, pripraví situáciu na odstránenie najpravejšieho prvku v ľavom podstrome (zmení hodnotu ukazovateľa on).


Precvičme si

Výškou stromu rozumieme dĺžku cesty od koreňa k najvzdialenejšiemu listu. Ak je koreň na prvej úrovni, jeho synovia na druhej atď., tak najvzdialenejší list bude na úrovni h + 1. Koľko vrcholov môže mať najviac binárny vyhľadávací strom, ak má výšku h?

[Riešenie]

Vytvorte program, ktorý pomocou menu umožní pracovať s binárnym vyhľadávacím stromom. Využite vyššie uvedené funkcie a doplňte funkcie na výpis všetkými troma metódami. Nakreslite binárny vyhľadávací strom, ktorý vznikne pridaním desiatich prvkov do prázdneho stromu takto:
 
for (int i = 1; i <= 10; i++) vloz(&strom, i);
 
Prezradíme, že ide o tzv. degenerovaný strom. Ako vyzerá?

[Riešenie]