====== B-stromy ======
Skripta 2.3.1
nebo http://cis.stvincent.edu/html/tutorials/swd/btree/btree.html
* B-stromy jsou vyvažované stromy se stejnou délkou cesty od kořene ke všem listům.
* Každý uzel obsahuje několik hodnot a několik ukazatelů na podstromy (zobecnění binárních stromů).
* Každý uzel, který není listem, má alespoň dva podstromy (důležité pro efektivní vyhledávání)
B-stromy používají databázové aplikace pro ukládání setříděných hodnot (indexů) na disk. \\
Maximální počet prvků v uzlu se volí podle velikosti sektorů nebo alokačních bloků na disku.
http://cs.wikipedia.org/wiki/B-strom \\
http://en.wikipedia.org/wiki/B-tree
Jak zmiňuje poslední odkaz, terminologie a implementace se může v jednotlivých případech mírně lišit. \\
V následující podkapitole shrnu pravidla používaná v našem programu pro vkládání hodnot do B-stromu.
Videa: \\
* B-Tree https://www.youtube.com/watch?v=C_q5ccN84C8
* Nebo jiné B-Tree http://www.youtube.com/watch?v=GKa_t7fF8o0
* B Trees v databázových aplikacích http://www.youtube.com/watch?v=aZjYr87r1b8
===== Požadavky na B-strom =====
* V každém uzlu, kromě kořene, musí být alespoň **Max/2** hodnot
* V každém uzlu smí být nejvýše **Max** hodnot
* Pokud má uzel, který není koncovým listem, **cnt** hodnot, musí mít **cnt+1** podstromů
* Všechny cesty od kořene k libovolnému listu jsou stejně dlouhé
* Prázdný strom budeme reprezentovat nulovým ukazatelem
* Strom s méně než **Max/2** hodnotami kořenovým uzlem s těmito hodnotami
* Stejně ukládáme i strom s nejvýše **Max** hodnotami
* Jednotlivé podstromy a hodnoty indexuejeme od **0**
* Hodnoty v jednom uzlu tvoří rostoucí posloupnost
* Zaměříme se na jeden uzel, který má **cnt** hodnot, hodnoty v tomto uzlu budeme nazývat klíče
* Zmíněný uzel má klíče s indexy **0** ... **cnt-1** a podstromy indexy **0** ... **cnt**
* První podstrom (s indexem **0**) obsahuje hodnoty menší než první klíč (a tedy menší než všechny klíče)
* Poslední podstrom (s indexem **cnt**) obsahuje hodnoty větší než poslední klíč (s indexem **cnt-1**), (a tedy větší než všechny klíče)
* Podstrom s idexem **i** obsahuje hodnoty větší než klíč s indexem **i-1** a menší než klíč s indexem **i**, pro i = 1, ... , cnt-1
:!: Pokud **Max/2** je větší nebo rovno **2**, má každý uzel (kromě listů) alespoň dva podstromy
===== Příklad =====
Do prázdného stromu vložíme hodnotu **30**.
{{zalg::btree-new.png}}
==== Vložíme celkem 5 hodnot a přeplníme kořen stromu ====
Postupně přidáváme hodnoty **20, 10, 40, 50**. \\
Pro **Max=4** se hodnoty zatřiďují mezi předcházející hodnoty v kořenovém prvku. \\
Hodnota **50** přeplní kořen stromu.
{{zalg::btree-before-root-split.png}}
Jako dělící bod vybereme hodnotu **30**. \\
Původní kořenový prvek se rozdělí na dva. \\
A ještě musíme přidat nový kořenový prvek, do kterého umístíme **30**.
{{zalg::btree-after-root-split.png}}
==== Po vložení několika prvků přibývají listy stromu ====
Přidáváme 21, ..., 29 a potom 51, ... 52. \\
Pokud se jednotlivé listy přeplní, rozdělí se a do kořene přibývá další hodnota.
{{zalg::btree-full.png}}
==== Další hodnota přeplní list ====
Vložíme *53* a opět se přeplní list, jako dělící bod bude použita hodnota **51**.
{{zalg::btree-before-node-split.png}}
Hodnota **51** přeplní kořen stromu, musíme kořen rozdělit, dělícím bodem bude **27**.
{{zalg::btree-after-node-split.png}}
Výsledný stav
{{zalg::btree-complete.png}}
===== Datová struktura pro jeden uzel =====
* V každém uzlu bude uloženo **cnt** hodnot, cnt < = Max.
* V poli **key** bude **cnt** hodnot
* V poli **ref** bude **cnt+1** ukazatelů na podstromy
* Prazdný strom reprezentujeme nulovým ukazatelem
* Kořenový prkek má **cnt** od **1** do **Max**
* Ostatní prkeky mají **cnt** od **Max/2** do **Max**
:?: Pro snažši vkládání nové hodnoty deklarujeme pole **key** a **ref** o jeden prvek delší
const int Max = 4;
struct Item
{
int cnt; // pocet klicu
int key [Max+1]; // key[0], ... key[cnt-1], a jeden navic
Item * ref [Max+2]; // ptr [0], ... ptr [cnt], a jeden navic
Item();
};
Celý strom budeme reprezentovat ukazatelem na **Item**
Konstruktor struktury **Item** nastaví **cnt** na **0** (ale prvek s nulovým počtem hodnot se ve stromu nakonec nikdy neobjeví). \\
Hodnoty inicializuje na **-1** (to nám může usnadnit ladění programu). \\
Ukazatele na podstromy nastaví na **nullptr** (tak to zůstane u listů).
:?: V poli **key** smíme používat jen indexy **0**, ... **cnt-1**. \\
V poli **ref** jen indexy **0**, ... **cnt**. \\
Item::Item()
{
cnt = 0;
for (int i = 0; i < Max + 1; i++) key[i] = -1;
for (int i = 0; i < Max + 2; i++) ref [i] = nullptr;
}
===== Funkce enter0 =====
Funkce **enter0** vloží do prvku zadaného ukazatelem **p** novou hodnotu **val**.
Funkce **enter0** není //určena pro vkládání do kořene stromu//, ale pro //"nižší"// patra.
void enter0 (Item * p, int val);
Vyhledáme vhodné místo pro naši novou hodnotu
int i = 0;
while (i < p->cnt && p->key[i] < val) i++;
Přeskočíme klíče s menší hodnotou: **p->key[i] < val** \\
Před porovnáním zkontrolujeme zda index **i** má přípustnou hodnotu: **i < p->cnt**
A) Pokud **i** je menší než **p->cnt**, vložíme novou hodnotu na pozici s indexem **i** a původní hodnoty od indexu **i** výše posuneme o jedno políčko. \\
B) Pokud se cyklus zastaví s indexem **i** rovným **p->cnt**, patří nová hodnota na konec pole **key**, za všechny současné hodnoty.
:?: A) **i < p->cnt** , **key[j] < val** pro **j < i**, **val < = key[i]** \\
:?: B) **i = = p->cnt** , **key[j] < val** pro **j = 0, ..., cnt-1**
Pokud ve variantě A) nastane rovnost, klíč je již ve stromu uložen. Můžeme ohlásit chybu nebo prostě klíč ve stromu ponechat-
if (i < p->cnt && p->key[i] == val) { }
===== Jsme na dně =====
Podívám se, zda již nejsme na nejnižším patře, zda náš prvek není listem. \\
:?: Pokud pod náma je nulový ukazatel jsou i ostatní ukazatele nulové, neboť všechny cesty jsou stejně dlouhé.
Uvolníme místo pro novou hodnotu, kterou vložíme do **key[i]**. Posuneme key [i], ..., key [cnt-1] o políčko dále. \\
:?: Kopírování začneme od vyšších indexů, jinak si hodnoty s vyššími indexy přepíšeme.
Posuneme ref [i], ..., ref [cnt] o políčko dále, ukazatelů je o jeden více než klíčů.
Do pole **key** na pozici **i** uložíme novou hodnotu. \\
Do **ref** na pozici **i** uložíme nulový ukazatel (jsme na nejnižším patře). \\
Nezapomene zvětšit počet hodnot **cnt**. \\
:?: Možná jsme překročili povolený počet prvků **Max**, ale v polích máme místo pro jeden prvek navíc.
Item * t = p->ref[i];
if (t == nullptr)
{
for (int k = p->cnt-1; k >= i; k--) p->key[k+1] = p->key[k];
for (int k = p->cnt; k >= i; k--) p->ref[k+1] = p->ref[k];
p->key[i] = val;
p->ref[i] = nullptr;
p->cnt++;
}
===== Pokud nejsme na dně =====
Pokud nejsme na nejižším patře, zavoláme **enter0** pro vkládání do příslušného podstromu.
:?: Pokud nová hodnota byla menší než **key[i]**, patří nová hodnota do podstromu **ref[i]** (varianta A, rovnost jsem již vyloučili). \\
:?: Pokud nová hodnota byla větší než všechny hodnoty **key**, patří nová hodnota do posledního podstromu (varianta B).
Item * t = p->ref[i];
if (t == nullptr)
{
/* již bylo popsáno */
}
else
{
enter0 (t, val); // vložíme do nižšího patra
/* musíme zkontrolovat, zda prvek t není přeplněn */
}
Rekurzivní volání **enter0** uloží hodnotu do podstromu. \\
( Tím by skončilo vkládání do //nevyvažovaného binárního// stromu. [[strom_add|funkce insert v binárních stromech]] ) \\
My ještě musíme zkontrolovat, zda v uzlu **t** není příliš mnoho hodnot.
===== Pod náma je příliš mnoho prvků, musíme rozlomit přeplněný prvek =====
Pokud je uvnitř uzlu **t** příliš mnoho prvků, \\
ponecháme Max/2 hodnot v původním uzlu **t**, \\
jednu hodnotu //"vytáhneme"// o patro výše a vložíme ji do uzlu **p**, \\
ostatní hodnoty přesuneme do nového uzlu **n**, ktrý umísíme vpravo od **t**.
==== Naplánujeme si rozdělení ====
Označíme **a** a **b** počty prvků, které se mají ocitnout v uzlech **t** a **n**. \\
Proměnná **d** bude obsahovat hodnotu, která má být vložena do uzlu **p**.
if (t->cnt == Max + 1)
{
int a = Max / 2; // a klicu zustane v t
int b = t->cnt-a-1; // b klicu do noveho bloku
int d = t->key[a]; // delici bod
{{zalg::btree-before.png}}
==== Vytvoříme nový uzel pro pravou část prvků ====
Vytvoříme nový uzel **n**. \\
Do něj patří **b** hodnot.
Z uzlu **t** přesuneme hodnoty **t->key[a+1]**, ..., **t->key[cnt-1]** do nového **n->key[0]**, ..., **n->key[b-1]**.
Ukazatele na podstromy **t->ref[a+1]**, ..., **t->ref[cnt]** přesuneme do **n->ref[0]**, ..., **n->ref[b]**
Item * n = new Item;
n->cnt = b;
for (int k = a + 1; k < t->cnt; k++) n->key[k-a-1] = t->key[k];
for (int k = a + 1; k < t->cnt+1; k++) n->ref[k-a-1] = t->ref[k];
{{zalg::btree-after.png}}
==== Uklidíme v původně přeplněném prvku ====
:?: V uzlu **t** zůstane **a** hodnot s indexy **0**, ..., **a-1**. \\
Hodnota s indexem **a** je dělícím bodem **d**. \\
Ostatní hodnoty patří do **n**.
:?: V levém uzlu **t** zůstane i ukazatel **ref[a]**. (Ukazatelů je o jedna více než hodnot.) \\
Ukazatel **ref[a+1]** a následující patří do **n**.
V původním uzlu **t** nastavíme počet hodnot na **a**. \\
A pro přehlednější ladění odstraníme přebytečné hodnoty.
for (int k = a + 1; k < t->cnt; k++) t->key[k] = -1;
for (int k = a + 1; k < t->cnt + 1; k++) t->ref[k] = nullptr;
t->cnt = a;
==== Vložíme jednu hodnotu a nový prvek do původního podstromu ====
Zbývá vložit hodnotu **d** na pozici s indexem **i** do //"nadřazeného"// prvku **p**.
Nejdříve posuneme původní hodnoty **p->key[i]**, ..., **p->key[p->cnt-1]** o jedno políčko doprava. \\
A také posuneme ukazatatele **p->ref[i+1]**, ..., **p->ref[p->cnt]**.
Ukazatel **p->ref[i]** stále míří na **t**. \\
Do **p->key[i]** vložíme //dělící// hodnotu **d**. \\
Ukazatel **p->ref[i+1]** směřuje na nové **n**. \\
for (int k = p->cnt-1; k >= i; k--) p->key[k + 1] = p->key[k];
for (int k = p->cnt; k > i; k--) p->ref[k + 1] = p->ref[k];
p->key[i] = d;
p->ref[i+1] = n;
p->cnt ++;
Zvýšíme počet prvků uvnitř **p**.
Pokud **p** přeplníme, ať si to vyřeší ten, kdo tuto funkci **enter0** zavolal.
Pokud jsme zavolali enter0 rekurzivně ve funkci enter0, tak jsme přeplnění právě řešíli.
Zbývá ještě vyřešit případné přeplnění kořene stromu.
Tam je postup odlišný musíme přidat nový kořen a pod něj umístit dva prvky, které vzniky z původního přeplněného kořenu.
===== Celá funkce enter0 =====
void enter0 (Item * p, int val)
{
int i = 0;
while (i < p->cnt && p->key[i] < val) i++;
// i == p->cnt , vsechny byly mensi, pridat
// i < p->cnt, p->key[i] >= val
if (i < p->cnt && p->key[i] == val) { }
else {
Item * t = p->ref[i];
if (t == nullptr)
{
for (int k = p->cnt - 1; k >= i; k--) p->key[k + 1] = p->key[k];
for (int k = p->cnt; k >= i; k--) p->ref[k + 1] = p->ref[k];
p->key[i] = val;
p->ref[i] = nullptr;
p->cnt++;
}
else
{
enter0 (t, val);
if (t->cnt == Max + 1)
{
int a = Max / 2; // a klicu zustane v t
int b = t->cnt - a -1; // b klicu do noveho bloku
int d = t->key[a]; // delici bod
Item * n = new Item;
n->cnt = b;
for (int k = a + 1; k < t->cnt; k++) n->key[k - a - 1] = t->key[k];
for (int k = a + 1; k < t->cnt+1; k++) n->ref[k - a - 1] = t->ref[k];
for (int k = a + 1; k < t->cnt; k++) t->key[k] = -1;
for (int k = a + 1; k < t->cnt + 1; k++) t->ref[k] = nullptr;
t->cnt = a;
for (int k = p->cnt - 1; k >= i; k--) p->key[k + 1] = p->key[k];
for (int k = p->cnt; k > i; k--) p->ref[k + 1] = p->ref[k];
p->key[i] = d;
p->ref[i+1] = n;
p->cnt++;
}
}
}
}
===== Funkce enter =====
Funkce **enter** se stará o vkládání do kořene B-stromu.
Pokud byl původní ukazatel **p** nulový, vytvoříme nový prvek s jednou novou hodnotou. \\
Tento prvek se stane kořenem stromu. \\
Parametr **p** je předáván odkazem (**&**), aby se změna promítla i do proměěnné použité jako skutečný parametr při volání funkce **enter**.
Například do proměnné **root** uložíme **nullptr**, představující prázdný strom. \\
A první volání funkce **enter** do proměnné **root** uloží nový prvek.
Item * root = nullptr;
enter (root, 30);
enter (root, 20);
==== Od prázdného stromu k jednoprvkovému ====
Jednoprvkový strom bude obsahovat jednu (novou) hodnotu a dva nulové ukazatele.
void enter (Item * & p, int val)
{
if (p == nullptr)
{
p = new Item;
p->cnt = 1;
p->key[0] = val;
p->ref[0] = nullptr;
p->ref[1] = nullptr;
}
else
{
/* strom již existuje */
}
}
==== Vložení hodnoty pomocí funkce enter0 ====
Pokud kořen stromu již existuje, použijeme funkci **enter0** pro vložení hodnoty. \\
A podíváme se zda kořen stromu není přeplněný. \\
Kořen stromu v tomto případě připomíná proměnnou **t** z minulé funkce **enter0**.
if (p == nullptr)
{
}
else
{
enter0 (p, val);
Item * t = p;
if (t->cnt == Max + 1)
{
/* přeplněný kořen stromu */
}
}
==== Rozdělění kořene stromu ====
Stejně jako ve funkci **enter0** vytvoříme nový prvek **n** a přesuneme do něj polovinu hodnot.
int a = Max / 2; // a klicu zustane v t
int b = t->cnt - a - 1; // b klicu do noveho bloku
int d = t->key[a]; // delici bod
Item * n = new Item;
n->cnt = b;
for (int k = a + 1; k < t->cnt; k++) n->key[k-a-1] = t->key[k];
for (int k = a + 1; k < t->cnt + 1; k++) n->ref[k-a-1] = t->ref[k];
for (int k = a + 1; k < t->cnt; k++) t->key[k] = -1;
for (int k = a + 1; k < t->cnt + 1; k++) t->ref[k] = nullptr;
t->cnt = a;
==== Přidání nového patra ====
//B-stromy rostou tak, že je u kořene uřízneme, zvedneme a vložíme nový špalek.//
//Já vám říkal, že programátoři nejsou dobří zahradníci.//
A potom je postup odlišný od funkce **enter0**. \\
Vytvoříme nový prvek, který se stane novým kořenem stromu. \\
Opět připomínám, že **p** je předávaná //odkazem//.
V novém kořeni stromu bude uložena dělící hodnota **d**. \\
A bude mít za podstromy **t** (původní kořen stromu) a nově vytvořený **n**.
:?: Ukazatel na původní kořen stromu jsme si schovali do obyčejného ukazatele **t**.
p = new Item;
p->cnt = 1;
p->ref[0] = t;
p->key[0] = d;
p->ref[1] = n;
===== Celý program =====
#include
using namespace std;
const int Max = 4;
struct Item
{
int cnt; // pocet klicu
int key [Max+1]; // key[0], ... key[cnt-1], a jeden navic
Item * ref [Max+2]; // ptr [0], ... ptr [cnt], a jeden navic
Item();
};
Item::Item()
{
cnt = 0;
for (int i = 0; i < Max + 1; i++) key[i] = -1;
for (int i = 0; i < Max + 2; i++) ref [i] = nullptr;
}
void enter0 (Item * p, int val)
{
int i = 0;
while (i < p->cnt && p->key[i] < val) i++;
// i == p->cnt , vsechny byly mensi, pridat
// i < p->cnt, p->key[i] >= val
if (i < p->cnt && p->key[i] == val) { }
else {
Item * t = p->ref[i];
if (t == nullptr)
{
for (int k = p->cnt - 1; k >= i; k--) p->key[k + 1] = p->key[k];
for (int k = p->cnt; k >= i; k--) p->ref[k + 1] = p->ref[k];
p->key[i] = val;
p->ref[i] = nullptr;
p->cnt++;
}
else
{
enter0 (t, val);
if (t->cnt == Max + 1)
{
int a = Max / 2; // a klicu zustane v t
int b = t->cnt - a -1; // b klicu do noveho bloku
int d = t->key[a]; // delici bod
Item * n = new Item;
n->cnt = b;
for (int k = a + 1; k < t->cnt; k++) n->key[k - a - 1] = t->key[k];
for (int k = a + 1; k < t->cnt+1; k++) n->ref[k - a - 1] = t->ref[k];
for (int k = a + 1; k < t->cnt; k++) t->key[k] = -1;
for (int k = a + 1; k < t->cnt + 1; k++) t->ref[k] = nullptr;
t->cnt = a;
for (int k = p->cnt - 1; k >= i; k--) p->key[k + 1] = p->key[k];
for (int k = p->cnt; k > i; k--) p->ref[k + 1] = p->ref[k];
p->key[i] = d;
p->ref[i+1] = n;
p->cnt++;
}
}
}
}
void enter (Item * & p, int val)
{
if (p == nullptr)
{
p = new Item;
p->cnt = 1;
p->key[0] = val;
p->ref[0] = nullptr;
p->ref[1] = nullptr;
}
else
{
enter0 (p, val);
Item * t = p;
if (t->cnt == Max + 1)
{
int a = Max / 2; // a klicu zustane v t
int b = t->cnt - a - 1; // b klicu do noveho bloku
int d = t->key[a]; // delici bod
Item * n = new Item;
n->cnt = b;
for (int k = a + 1; k < t->cnt; k++) n->key[k - a - 1] = t->key[k];
for (int k = a + 1; k < t->cnt + 1; k++) n->ref[k - a - 1] = t->ref[k];
for (int k = a + 1; k < t->cnt; k++) t->key[k] = -1;
for (int k = a + 1; k < t->cnt + 1; k++) t->ref[k] = nullptr;
t->cnt = a;
p = new Item;
p->cnt = 1;
p->ref[0] = t;
p->key[0] = d;
p->ref[1] = n;
}
}
}
void print (Item * p)
// alespon vytiskneme hodnoty v rostoucim poradi
{
if (p != nullptr)
{
for (int i = 0; i < p->cnt; i++)
{
print (p->ref[i]);
cout << p->key[i] << endl;
}
print (p->ref[p->cnt]);
}
}
int main ()
{
Item * root = nullptr;
enter(root, 30);
enter(root, 20);
enter(root, 10);
enter(root, 40);
enter(root, 50);
for (int k = 21; k <= 29; k++) enter(root, k);
for (int k = 51; k <= 800; k++) enter(root, k);
print (root);
}
===== Grafický výstup do HTML souboru s pomocí JavaScriptu =====
Jen pro ilustraci: zobrazování stromů.
{{zalg::btree.png}}
http://gitlab.fjfi.cvut.cz/culikzde/zalg/-/tree/master/btree-js \\
Náš program vytvoří textový soubor //output.html//. \\
V něm bude popis stromu ve tvaru
[ 'text zobrazený v uzlu', [ první podstrom ], [ druhý podstrom ] ]
['27, 54, 63, 72',
['21, 24',
['10, 20'],
['22, 23'],
['25, 26']
],
['30, 51',
['28, 29'],
['40, 50'],
['52, 53']
],
['57, 60',
['55, 56'],
['58, 59'],
['61, 62']
],
['66, 69',
['64, 65'],
['67, 68'],
['70, 71']
],
['75, 78',
['73, 74'],
['76, 77'],
['79, 80']
]
]
Výpis v tomto tvaru zajistí funkce **display**.
void display (ofstream & f, Item * p, bool comma = false, int level = 0)
{
if (p != nullptr)
{
bool list = (p->ref[0] == nullptr);
for (int i = 1; i <= level; i++) f << " ";
f << "[";
// klice v jednoduchych uvozovkach
f << "'";
for (int i = 0; i < p->cnt; i++)
{
f << p->key [i];
if (i < p->cnt-1) f << ", ";
}
f << "'";
if (!list)
{
f << ","; // carka pred nasledujicimi hodnotam
f << endl;
for (int i = 0; i < p->cnt + 1; i++)
{
display (f, p->ref[i], i < p->cnt, level+1);
}
}
if (!list)
for (int i = 1; i <= level; i++) f << " ";
f << "]";
if (comma) f << ",";
f << endl;
}
}
Funkce **displayData** přidá program v JavaScriptu, využívající knihovnu **mxGraph**.
http://jgraph.github.io/mxgraph \\
http://jgraph.github.io/mxgraph/javascript/index.html \\
http://github.com/jgraph/mxgraph
void displayData (Item * p)
{
ofstream f ("output.html");
f << "" << endl;
f << "" << endl;
f << "B-Tree with mxGraph" << endl;
f << "" << endl;
f << "" << endl;
f << "" << endl;
f << "" << endl;
f << "" << endl;
f << "" << endl;
f << "
" << endl;
f << "" << endl;
f << "" << endl;
f.close ();
}
int main()
{
Item * root = nullptr;
enter(root, 30);
enter(root, 20);
enter(root, 10);
enter(root, 40);
enter(root, 50);
for (int k = 21; k <= 29; k++) enter(root, k);
for (int k = 51; k <= 80; k++) enter(root, k);
displayData (root);
}
Zde je výsledný soubor //output.html//. \\
Stačí uložit do textového souboru a podívat se na něj webovým prohížečem.
B-Tree with mxGraph