www.www.zaachi.com » Blog/Php » Traverzování kolem stromu #1

Traverzování kolem stromu je jeden ze způsobů, jak ukládat stromovou strukturu dat v relační databázi. Způsobů, jak ukládat stromová data je samozřejmě spousta, ovšem ne všechny jsou dostatečně efektivní. Můžeme například používat různé rekurzivní způsoby, jež jsou většinou při větší velikosti stromu velice pomalé neboť pro získání celého stromu bude zapotřebí minimálně tolik SQL dotazů, kolik je úrovní rozvětvení. Další možností je použít zásobník, který je sice rychlejší než rekurze, ale i tak při větší velikosti stromu budeme na databázi chrlit spousty SQL dotazů.
Způsob, u kterého si většinou
vystačíme s jedním nebo dvěma SQL dotazy pro jakoukoli operaci, se nazývá
Traverzování kolem stromu.
Základní princip celého
algoritmu je velice jednoduchý. Úkolem je ohodnotit všechny uzly stromu pořadovými
čísly, podle nichž procházíme celým stromem. Pro lepší představu ohodnocení si
prohlédněte obrázek:
Pořadová čísla, kterými
ohodnocujeme strom začínají od nejnižšího čísla a při každém dalším uzlu toto
číslo narůstá o hodnotu 1. Celým stromem procházíme zleva doprava a tím
získáváme celou jeho strukturu.
Home (1, 16) -> First (2, 7) -> Thrid (3, 4) -> Fourth(5, 6) -> First(3, 7) -> Second(8, 12)
-> Fifth(9, 14) -> Sixth (10,
11) -> Seventh(12, 13) -> Fifth(9, 14) -> Second(8, 12)
-> Home(1, 16)
Každý uzel má nyní dva indexy
– pravý a levý. Při ohodnocení uzlů tímto způsobem nám vzniká efekt, který bude
pro naši práci se stromem velice důležitý. Uzel, ve kterém jsou poduzly, má
vždy menší levý index než všechny tyto poduzy a jeho pravý index je větší než
všechny jeho poduzly. Patrné je to z kořene celého stromu, který má
nejmenší levý index a největší pravý index v celém stromu.
Výhoda spočívá v tom, že
nyní pro nás není problém získat kterýkoliv podstrom seřazený přesně podle toho
jak jej máme uložený v databázi, pouze jedním SQL dotazem.
Pro naprogramování tohoto
algoritmu jsem zvolil kombinaci PHP 5 a MySQL 4.1:
Před samotným ukládáním dat si
musíme vytvořit tabulku v databázi. Všechno co v tabulce potřebujeme
je jméno položky, pravý a levý index a navíc budeme ukládat ještě velikost
vnoření položky ve stromu. Popřípadě můžeme ještě ukládat prefix pro jazykovou
mutaci ( v celém kódu budeme předpokládat že prefix jazykové mutace je
uložen v poli SESSION). Struktura tabulky může potom vypadat například
nějak takto:
CREATE TABLE preorder_tree_traversal ( id_traverz INTEGER NOT NULL AUTO_INCREMENT, traverz_name VARCHAR(100), traverz_parent SMALLINT, traverz_left SMALLINT, traverz_right SMALLINT, lang VARCHAR(6), PRIMARY KEY( id_traverz );
Pokud máme vytvořenou
tabulku, můžeme si vytvořit třídu pro obsluhu celého algoritmu. U traverzování
je sice obsluha dat v databázi trochu složitější, než je tomu u jiných
algoritmů, ale přístup k těmto prvkům (jednotlivým uzlům) je o to více
efektivní.
Problém při editaci je právě
v tom, že při každé změně se musí přečíslovat indexy uzlů, aby nám
nevznikali kolize ve stromu.
Celá třída se bude skládat
z metod, které budou obsluhovat traverzování. Jako první si nadefinujeme
konstruktor, do kterého můžeme předat jako parametr název tabulky, ve které je
připravena požadovaná struktura:
class traverz extends connect{
function __construct( $table_name ){
$this->table_name = $table_name;
parent::__construct();
}
}
Pro připojení k databázi
využijeme třídu connect, kterou si do naší třídy pomocí dědičnosti nadědíme.
Jako první si vytvoříme
metodu, která vloží do tabulky první prvek stromu – kořen. Kořen bude musí mít,
za předpokladu že ve stromu nejsou žádné uzly, indexy 1 a 2. Přitom index
parent nastavíme na hodnotu 0, ze které budeme vycházet:
function _insert_first(){
$insert = "INSERT INTO " . $this->table_name . "(traverz_name, traverz_parent, traverz_left, traverz_right, lang )
VALUES(
'HOME', '0', '1', '2', '" . $_SESSION['lang_prefix'] . "' )";
mysql_query($insert, $this->link);
return ( mysql_affected_rows($this->link) == 1 ? 1 : 0);
}
Pro kontrolu, zda má být metoda volána můžeme vytvořit funkci, která zkontroluje, zda je tabulka prázdná, nebo neobsahuje-li žádný uzel s danou jazykovou mutací:
function _check_num_of_rows(){
$select = "SELECT COUNT(*) FROM " . $this->table_name . " WHERE lang = '" . $_SESSION['lang_prefix'] . "'";
if( mysql_result( mysql_query( $select, $this->link ), 0, 0) == 0 ){
self::_insert_first();
return 0;
}
else{
return 0;
}
}
}
else{
return 0;
}
}
Jedna z nejdůležitějších
metod v celé třídě bude přidávání uzlů. Důležité je myslet na to, že při
každém přidání uzlu musíme přečíslovat všechny uzly, které budou za přidaným
uzlem. Jsou to všechny, které mají levý index větší než index prvku, za který
přidáváme, o hodnotu i+1.
Při ukládání uzlu máme
možnost si vybrat za který uzel bude nový uzel vložen. Pro jednoduchost bude
třída umožňovat pouze přidání uzlu, který bude vnořen jako do našeho uzlu.
Budeme si ovšem moci vybrat, zda chceme vložit na začátek uzlu nebo na jeho
konec.
Vysvětlím to ještě jednou na
obrázku. Podívejte se na obrázek, který je uložen na začátku článku a chtějme
vložit další uzel za uzel „First“. Třída nám umožní uložit uzel před uzel
„Thrid“ (na začátek) nebo za uzel „Fourth“ (na konec) s tím, že vložený
uzel bude mít stejný parent index jako uzly „Thrid“ nebo „Fourht“.
Funkce addTree bude mít tedy
tři vstupní parametry: Jméno nového uzlu, levý index uzlu, za který vkládáme a
pozici kam chceme vkládat (na začátek nebo na konec):
function addTree ( $name, $left, $position = 0 ){
if( gettype( $position ) == 'string' )
$position = self::_setup_position( $position );
$position = abs( intval( $position) );
if( $position > 2 )
return 0;
$parent = self::select_parent($left, 1);
switch( $position ){
case 0 : {
if( self::_addTree_h( $name, $left, $parent ) == 1 )
return 1;
else
return 0;
break;
}
case 1 :{
if( self::_addTree_e( $name, $left, $parent, ( $this->traverz_right - 1 ) ) == 1 )
return 1;
else
return 0;
break;
}
}
return 1;
}
Metoda addTree volá nejprve
metodu _setup_position, která je zde jenom z důvodu příjemnějšího
vstupního parametru pro pozici. Hodnota tohoto vstupního parametru, který může
být zadán buď jako hodnota 0, 1, nebo jako hodnoty H, E ( Home, End):
function _setup_position( $position ){
switch( strtolower( $position ) ){
case 'h' : return 0; break;
case 'e' : return 1; break;
default: return 0;
}
}
Dále je zde volána self::select_parent($left,
1), která zjistí parent hodnotu uzlu, za který vkládáme:
function select_parent($left, $info = 0){
$select = "SELECT traverz_parent, traverz_right, id_traverz
FROM " . $this->table_name . "
WHERE traverz_left = '" . $left . "'
AND lang = '" . $_SESSION['lang_prefix'] . "'
LIMIT 1;";
$data = mysql_query($select, $this->link);
if( $info == 1 ){
$this->traverz_right = @mysql_result($data, 0, 1);
$this->id_traverz = @mysql_result( $data, 0, 2 );
}
return ( @mysql_result($data, 0, 0) + 1 );
}
Nepovinný parametr je $info.
Pomocí nějž si uložíme hodnotu pravého indexu a hodnotu ID prvku, pro další
práci.
Nyní již konečně můžeme
vkládat uzel. Pro vkládání musíme vytvořit dvě metody, jednu pro vložení uzlu
na začátek a druhou pro vložení uzlu na konec. Obě metody si budou dost podobné
a nebyl by problém z nich vytvořit jenom jednu metodu, ale pro přehlednost
si vytvoříme metody dvě.
Nejprve metoda pro vložení
poduzlu nazačátek:
function _addTree_h( $name, $left, $parent ){
$insert = "INSERT INTO " . $this->table_name . "(traverz_name, traverz_parent, traverz_left, traverz_right, lang)
VALUES(
'" . trim( htmlspecialchars($name) ) . "',
'" . intval( $parent ) . "',
'" . ( intval( $left ) + 1 ) . "',
'" . ( intval( $left ) + 2 ) . "',
'" . $_SESSION['lang_prefix'] . "',
);";
mysql_query($insert, $this->link);
if( mysql_affected_rows($this->link) == 1 )
return (self::_update_tree(mysql_insert_id(), $left) == 1 ? 1 : 0 );
else
return 0;
}
V SQL dotazu vidíme
vkládaná data. Hodnota parent je hodnota, kterou jsme zjistili metodou
_select_parent a předali si ji jako parametr. Stejně tak jsme si předali
hodnotu názvu. Hodnota levého indexu bude předaná hodnota $left + 1, protože
index $left je hodnota levého indexu uzlu, za který vkládáme. Hodnota pravého
uzlu bude o jednu větší než hodnota levého uzlu, protože tento uzel neobsahuje
žádné poduzly.
Důležitá je zde funkce _update_tree,
do které si předáme dva parametry – ID vloženého prvku a levý index prvku, za
který vkládáme. Metoda upraví tabulku tak, že přečísluje všechny indexy uzlů za
uzlem za který vkládáme s výjimkou uzlu, který jsme nyní uložili, protože
zde jsou indexy již nastaveny správně.
Po vysvětlení se metoda zdá
možná složitá ale je velmi jednoduchá:
function _update_tree($id, $left){
$sql[] = "UPDATE " . $this->table_name . "
SET traverz_left = (traverz_left + 2)
WHERE ( traverz_left > '" . $left . "')
AND id_traverz != '" . $id . "'
AND lang = '" . $_SESSION['lang_prefix'] . "'";
$sql[] = "UPDATE " . $this->table_name . "
SET traverz_right = (traverz_right + 2)
WHERE ( traverz_right > '" . ($left - 1) . "')
AND id_traverz != '" . $id . "'
AND lang = '" . $_SESSION['lang_prefix'] . "'";
foreach($sql as $value){
mysql_query($value, $this->link);
if(mysql_affected_rows($this->link) == 0 )
return 0;
}
}
Po zavolání metody se
provedou dva SQL dotazy. Jeden upraví pravý a druhý levý index uzlů.
Nyní už vytvoříme pouze
metodu pro vložení uzlu na konec, která je vlastně stejná jako metoda pro
vložení, jenom s drobnými úpravami:
function _addTree_e( $name, $left, $parent ){
$insert = "INSERT INTO " . $this->table_name . "(traverz_name, traverz_parent, traverz_left, traverz_right, lang)
VALUES(
'" . trim( htmlspecialchars( $name ) ) . "',
'" . intval( $parent ) . "',
'" . intval( $this->traverz_right ) . "',
'" . ( intval( $this->traverz_right ) + 1 ) . "',
'" . $_SESSION['lang_prefix'] . "', );";
mysql_query($insert, $this->link);
if( mysql_affected_rows($this->link) == 1 )
return (self::_update_tree(mysql_insert_id(), $this->traverz_right) == 1 ? 1 : 0 );
else
return 0;
}
Nyní máme hotový základ celé
třídy. Můžeme ukládat nové uzly. V příštím článku se podíváme na to, jak můžeme
uzly mazat a přesouvat.
Všechno co je nyní hotovo si
můžete ještě jednou prohlídnout:
class traverz extends connect{
public $traverz_right = NULL;
public $id_traverz = NULL;
public $parametrs = array();
public $ids = array();
public $table = NULL;
function __construct( $table_name ){
$this->table_name = $table_name;
parent::__construct();
self::_check_num_of_rows();
}
function _check_num_of_rows(){
$select = "SELECT COUNT(*) FROM " . $this->table_name . " WHERE lang = '" . $_SESSION['lang_prefix'] . "'";
if( mysql_result( mysql_query( $select, $this->link ), 0, 0) == 0 ){
self::_insert_first();
return 0;
}
else{
return 0;
}
}
//Insert HOME into Traverz_table
function _insert_first(){
$insert = "INSERT INTO " . $this->table_name . "(
traverz_name, traverz_parent, traverz_left, traverz_right, lang
)
VALUES(
'HOME', '0', '1', '2', '" . $_SESSION['lang_prefix'] . "', 'home'
)";
mysql_query($insert, $this->link);
return ( mysql_affected_rows($this->link) == 1 ? 1 : 0);
}
function _update_tree($id, $left){
$sql[] = "UPDATE " . $this->table_name . "
SET traverz_left = (traverz_left + 2)
WHERE ( traverz_left > '" . $left . "')
AND id_traverz != '" . $id . "'
AND lang = '" . $_SESSION['lang_prefix'] . "'";
$sql[] = "UPDATE " . $this->table_name . "
SET traverz_right = (traverz_right + 2)
WHERE ( traverz_right > '" . ($left - 1) . "')
AND id_traverz != '" . $id . "'
AND lang = '" . $_SESSION['lang_prefix'] . "'";
foreach($sql as $value){
mysql_query($value, $this->link);
if(mysql_affected_rows($this->link) == 0 )
return 0;
}
}
function select_parent($left, $info = 0){
$select = "SELECT traverz_parent, traverz_right, id_traverz
FROM " . $this->table_name . "
WHERE traverz_left = '" . $left . "'
AND
lang = '" . $_SESSION['lang_prefix'] . "'
LIMIT 1;";
$data = mysql_query($select, $this->link);
if( $info == 1 ){
$this->traverz_right = @mysql_result($data, 0, 1);
$this->id_traverz = @mysql_result( $data, 0, 2 );
}
return ( @mysql_result($data, 0, 0) + 1 );
}
function _setup_position( $position ){
switch( strtolower( $position ) ){
case 'h' : return 0; break;
case 'e' : return 1; break;
default: return 0;
}
}
function addTree ( $name, $left, $position = 0 ){
if( gettype( $position ) == 'string' )
$position = self::_setup_position( $position );
$position = abs( intval( $position) );
if( $position > 2 )
return 0;
$parent = self::select_parent($left, 1);
switch( $position ){
case 0 : {
if( self::_addTree_h( $name, $left, $parent ) == 1 )
return 1;
else
return 0;
break;
}
case 1 :{
if( self::_addTree_e( $name, $left, $parent, ( $this->traverz_right - 1 ) ) == 1 )
return 1;
else
return 0;
break;
}
}
return 1;
}
function _addTree_h( $name, $left, $parent ){
$insert = "INSERT INTO " . $this->table_name . "(
traverz_name, traverz_parent, traverz_left, traverz_right, lang
)
VALUES(
'" . trim( htmlspecialchars($name) ) . "',
'" . intval( $parent ) . "',
'" . ( intval( $left ) + 1 ) . "',
'" . ( intval( $left ) + 2 ) . "',
'" . $_SESSION['lang_prefix'] . "',
);";
mysql_query($insert, $this->link);
if( mysql_affected_rows($this->link) == 1 )
return (self::_update_tree(mysql_insert_id(), $left) == 1 ? 1 : 0 );
else
return 0;
}
function _addTree_e( $name, $left, $parent ){
$insert = "INSERT INTO " . $this->table_name . "(traverz_name, traverz_parent, traverz_left, traverz_right, lang)
VALUES(
'" . trim( htmlspecialchars( $name ) ) . "',
'" . intval( $parent ) . "',
'" . intval( $this->traverz_right ) . "',
'" . ( intval( $this->traverz_right ) + 1 ) . "',
'" . $_SESSION['lang_prefix'] . "',
);";
mysql_query($insert, $this->link);
if( mysql_affected_rows($this->link) == 1 )
return (self::_update_tree(mysql_insert_id(), $this->traverz_right) == 1 ? 1 : 0 );
else
return 0;
}
}

Autor: Zaachi
Publikováno: 18.7.2009 19:26:53
Traverzování kolem stromu #2
Traverzování kolem stromu #3
Traverzování kolem stromu #4 (operace nad stromem)