Hlavní navigace

GLib: Mnohonásobné stromy (4)

10. 10. 2000
Doba čtení: 7 minut

Sdílet

Dnes výklad o mnohonásobných stromech dokončíme. Čekají nás i nějaké příklady ze života.

Další funkce

GNode* g_node_get_root(GNode *node); 

…nalezne kořen stromu, jehož příslušníkem je uzel node.

#define G_NODE_IS_LEAF(node) 

…makro, jehož výsledkem je TRUE, pokud uzel node je listem (nemá žádné následovníky).

#define G_NODE_IS_ROOT(node) 

…makro vracející TRUE, pokud uzel node je kořenem stromu (nemá žádné předky).

guint g_node_depth(GNode *node); 

…vrací hloubku daného uzlu node. Je-li node rovno NULL, návratovou hodnotou bude 0, je-li node kořenem, vrátí se 1. Pro přímé potomky (syny) kořenového uzlu je hloubka rovna 2 a tak dále.

Jinými slovy, hloubkou uzlu rozumějte pořadí generace (předek – potomek), ke které uzel náleží.

guint g_node_n_nodes(GNode *root, GTraverseFlags flags);

…vrátí počet uzlů ve stromu s kořenem root. Jestliže uzel root není kořenem stromu, vrátí se počet prvků v podstromu s kořenem root. Parametrem flags můžete specifikovat, mají-li se počítat všechny uzly ( G_TRAVERSE_ALL) nebo jen listy ( G_TRAVERSE_LEAFS) či jen ne-listové uzly ( G_TRAVERSE_NON_LEAFS). Více viz GLib část 21.

gboolean g_node_is_ancestor(GNode *node, GNode *descendant); 

Funkce g_node_is_ancestor() vrátí TRUE, pokud uzel node je předchůdce (v širším slova smyslu) potomka descendant. Vrácená hodnota je TRUE, pokud node je rodič, prarodič atd. uzlu descendant.

guint g_node_max_height(GNode *root); 

…vrátí maximální výšku větví od uzlu root. Jinými slovy, vrácenou hodnotou je maximální vzdálenost od uzlu root k listům. Je-li root rovno NULL, vrátí se 0. Jestliže root nemá potomky, vrátí se 1. Jestliže root má nějaké potomky, vrátí se 2. Jestliže i potomci uzlu root mají potomky, vrátí se 3 atd.

Příklad 1

#include <glib.h>

/* funkce pro vytisknuti jednoho uzlu
 * (neni urcena pro samostatne pouziti!) */
gboolean __print_node(GNode *node, gpointer data)
{
  gint x; /* pomocna promenna */

  if (!(G_NODE_IS_ROOT(node))) {
    /* node neni korenem - posun od zacatku radku */
    x = (g_node_depth(node) - 1);
    while (x > 0) {
      printf("   ");
      x--;
    }
  }

  printf("%s\n", node->data);
  return (FALSE);
}


/* funkce pro tisk stromu */
void my_node_print_tree(GNode *root)
{
  /* skonci, neni-li root korenem! */
  g_return_if_fail(G_NODE_IS_ROOT(root));

  g_node_traverse(root, G_PRE_ORDER, G_TRAVERSE_ALL,
    -1, __print_node, NULL);
}


/* hlavni telo programu */
gint main(void)
{
  GNode *otec, *syn, *vnuk;

  /* inicializace stromu */
  otec = g_node_new("otec");

  /* potomci otce */
  syn = g_node_append_data(otec, "syn1");

  /* potomci syna1 */
  g_node_append_data(syn, "vnuk1.1");
  vnuk = g_node_append_data(syn, "vnuk1.2");
  g_node_append_data(syn, "vnuk1.3");

  /* potomci vnuka1.2 */
  g_node_append_data(vnuk, "pravnuk1.2.1");

  /* potomci otce */
  g_node_append_data(otec, "syn2");
  syn = g_node_append_data(otec, "syn3");

  /* potomci syna3 */
  vnuk = g_node_append_data(syn, "vnuk3.1");

  /* potomci vnuka3.1 */
  g_node_append_data(vnuk, "pravnuk3.1.1");
  g_node_append_data(vnuk, "pravnuk3.1.2");


  /* tisk stromu */
  puts("Puvodni strom:");
  my_node_print_tree(otec);

  /* nyni z puvodniho stromu "vykousneme" podstrom
   * s korenem v uzlu syn3 */
  g_node_unlink(syn);

  /* tisk stromu */
  puts("\nPuvodni strom po uprave:");
  my_node_print_tree(otec);

  puts("\nNovy strom:");
  my_node_print_tree(syn);

  /* stromy opet spojime, ale trochu jinak
   * (jako potomky uzlu vnuk1.2) */
  g_node_prepend(otec->children->children->next, syn);

  /* tisk stromu */
  puts("\nPuvodni strom po druhe uprave:");
  my_node_print_tree(otec);


  /* dealokace stromu */
  g_node_destroy(otec);
}

Tento příklad definuje jednu funkci, kterou možná ve svých programech použijete pro ladicí účely. Je to funkce my_node_print_tree(). Jak už její název napovídá, funkce tiskne vzhled stromu. Pojďme se podívat, jak to všechno funguje:

Funkce my_node_print_tree() začíná makrem g_return_if_fa­il(), které zkontroluje, jestli její jediný parametr root je kořenem. (O makru g_return_if_fa­il() se dočtete více, až se budeme bavit o podpůrných ladicích funkcích knihovny GLib.)

Následuje volání funkce g_node_traverse() s uživatelskou funkcí __print_node(), která má za úkol tisknout jeden uzel. Funkcí g_node_traverse() se projdou všechny uzly ( G_TRAVERSE_ALL) v pořadí G_PRE_ORDER (= nejprve uzel, pak jeho potomci) do neomezené hloubky (max_depth = –1).

Funkce __print_node() tedy bude zavolána na každý uzel stromu. Pokud uzel node není kořenem, dojde k odsazení textu od začátku řádku. O kolik znaků, to určí hloubka daného uzlu ( g_node_depth()). Pak se už jen vytisknou data daného uzlu a funkce s návratovou hodnotou FALSE končí. ( FALSE proto, aby nikdy nedošlo k předčasnému ukončení procházení (traverse) stromem.)

Hlavní tělo programu se stará o toto:

Nejprve se inicializuje strom s kořenem otec tak, jako na obrázku 1. Zbylé dvě proměnné ( syn a vnuk) z inicializace vyjdou tak, že syn bude ukazovat na uzel syn3 a vnuk na uzel vnuk3.1.

Programem vytvořený strom

Obrázek 1 – Programem vytvořený strom

Strom se ukázkově funkcí my_node_print_tree() vytiskne a aby nebyla nuda, ještě se s ním trochu zacvičí. Nejprve se od něj funkcí g_node_unlink() oddělí podstrom s kořenem syn3 (proměnná syn), který se následně funkcí g_node_prepend() znovu ke stromu připojí, tentokrát ale jako potomek uzlu vnuk1.2.

Všimněte si, jak byl ve volání g_node_prepend() uadresován uzel vnuk1.2:  otec->children->children->next.

Mezi každou operací se přitom provádějí kontrolní tisky, abychom měli možnost názorně vidět, co se se stromy děje.

Nakonec se celý strom dealokuje. Podstrom syn není třeba dealokovat zvlášť, protože jsme jej nakonec zase spojili s hlavním stromem.

Výstup programu by měl být takovýto:

Puvodni strom:
otec
   syn1
      vnuk1.1
      vnuk1.2
         pravnuk1.2.1
      vnuk1.3
   syn2
   syn3
      vnuk3.1
         pravnuk3.1.1
         pravnuk3.1.2

Puvodni strom po uprave:
otec
   syn1
      vnuk1.1
      vnuk1.2
         pravnuk1.2.1
      vnuk1.3
   syn2

Novy strom:
syn3
   vnuk3.1
      pravnuk3.1.1
      pravnuk3.1.2

Puvodni strom po druhe uprave:
otec
   syn1
      vnuk1.1
      vnuk1.2
         syn3
            vnuk3.1
               pravnuk3.1.1
               pravnuk3.1.2
         pravnuk1.2.1
      vnuk1.3
   syn2

Příklad 2

Ukažme si ale ještě jeden příklad. V této ukázce se pracuje se stejným stromem, jako v příkladu 1, proto i inicializační část programu je stejná.

Kód vytváří novou vyhledávací funkci my_node_hledej(), která hledá uzly podle řetězce uloženého v datech. K úspěšnému nalezení uzlu tedy nemusíte znát přímo pointer, ale jen správný text.

#include <glib.h> 
/* funkce pro porovnani retezce v datech s hledanym
 * retezcem (fce neni urcena pro samostatne pouziti!) */
gboolean __porovnej_retezce(GNode *node, gpointer data)
{
  gpointer *p;

  p = data;

  if (strcmp((gchar *) node->data, (gchar *) p[0]) == 0) {
    /* nasel - koncime */
    p[1] = node;
    return (TRUE);
  } else {
    /* nenasel */
    return (FALSE);
  }
}


/* hledani uzlu podle retezce */
GNode* my_node_hledej(GNode *root, gchar *what)
{
  gpointer p[2];

  /* kontrola parametru */
  g_return_if_fail(G_NODE_IS_ROOT(root));
  g_return_if_fail(what != NULL);

  /* nastaveni pole */
  p[0] = what;
  p[1] = NULL;

  g_node_traverse(root, G_PRE_ORDER, G_TRAVERSE_ALL,
    -1, __porovnej_retezce, p);

  return (p[1]);
}


/* hlavni telo programu */
gint main(void)
{
  GNode *otec, *syn, *vnuk, *nalezeny;

  /* inicializace stromu */
  otec = g_node_new("otec");
  syn = g_node_append_data(otec, "syn1");
  g_node_append_data(syn, "vnuk1.1");
  vnuk = g_node_append_data(syn, "vnuk1.2");
  g_node_append_data(syn, "vnuk1.3");
  g_node_append_data(vnuk, "pravnuk1.2.1");
  g_node_append_data(otec, "syn2");
  syn = g_node_append_data(otec, "syn3");
  vnuk = g_node_append_data(syn, "vnuk3.1");
  g_node_append_data(vnuk, "pravnuk3.1.1");
  g_node_append_data(vnuk, "pravnuk3.1.2");


  /* vyhledani uzlu */
  nalezeny = my_node_hledej(otec, "vnuk3.1");

  /* kontrolni tisk */
  if (nalezeny != NULL)
    printf("%s\n", nalezeny->data);


  /* dealokace stromu */
  g_node_destroy(otec);
}

Hlavní tělo programu po inicializaci stromu zavolá funkci my_node_hledej() a její výsledek uloží do pointeru nalezeny. Hledá se uzel s textem „vnuk3.1“. Následuje kontrolní tisk, který vypíše data nalezeného uzlu, a uvolnění stromu z paměti.

Funkce my_node_hledej() očekává dva parametry: kořen root stromu a hledaný řetězec what. K prohledání stromu využijeme standardní funkci knihovny GLib, g_node_traverse() a předáme jí takovou uživatelskou funkci, která v případě nalezení uzlu vrátí TRUE (čímž se procházení stromu zastaví). Jak ale zjistit, na kterém uzlu prohledávání stromu skončilo? Funkce g_node_traverse() nic nevrací a tak nám zbývají jen malinká vrátka v podobě uživatelských dat (poslední parametr funkce g_node_traver­se()).

Funkce my_node_hledej() definuje dvouprvkové pole pointerů ( p). p[0] bude sloužit pro uložení hledaného řetězce a p[1] pro nalezený uzel. (Prozatím se inicializuje na NULL.) Takto nastavené pole p se spolu s uživatelskou funkcí __porovnej_retezce() předá funkci g_node_traver­se().

__porovnej_retezce() 

Vraťme se nyní zpět do funkce my_node_hledej(). Po dokončení volání g_node_traverse() i tato funkce končí a jako návratová hodnota se použije pointer uložený v  p[1] (pointer na nalezený uzel nebo NULL).

Použití parametru data je asi jediným rozumným způsobem jak zjistit uzel, na kterém procházení stromu skončilo. Škoda, že tvůrci knihovny GLib na toto nemysleli a nevymysleli funkci g_node_traverse() tak, aby pointer vracela automaticky sama.

Byl pro vás článek přínosný?

Autor článku

Michal Burda vystudoval informatiku a aplikovanou matematiku a nyní pracuje na Ostravské univerzitě jako odborný asistent. Zajímá se o data mining, Javu a Linux.