Un parere tecnico

di il
3 risposte

Un parere tecnico

Ciao a tutti,

avrei bisogno di un parere: ho realizzato un progetto che implementa la gestione di un albero rosso nero che, per specifiche di progetto, è sottoclasse di albero binario di ricerca.

Da un punto di vista teorico, un albero rosso nero ha a disposizione un nodo sentinella, indicato con NIL, per gestire più facilmente alcuni controlli nel corso degli algoritmi (per esempio, tutti i NULL dei metodi di un albero binario sono sostituiti con NIL). La radice ha come padre il nodo sentinella, e tutte le foglie sono rimpiazzate da un unico nodo NIL, collegato con tutti i nodi posti al livello immediatamente superiore.

Nel mio programma, per gestire la presenza del nodo sentinella, ho seguito questo ragionamento: era inutile riscrivere il codice dei metodi della superclasse, dovendo sfruttare l'ereditarietà. Il mio problema, però, nasceva dal fatto che i metodi dell'albero binario non tengono conto del nodo sentinella (si veda tutti i vari NULL al posto di NIL), né del tipo di ritorno. Ho pensato, quindi, di aggiungere alla classe albero rosso nero un metodo privato, converti_albero(), che "trasforma” temporaneamente un albero rosso nero in un albero binario e viceversa. La conversione viene effettuata subito prima e subito dopo l’invocazione di uno dei metodi della superclasse, nello specifico quello per l'inserimento di un nuovo nodo e quello per la ricerca di un nodo. Tutti i nodi dell’albero collegati con il nodo sentinella vengono momentaneamente “sganciati” settando i campi puntatore a NULL (o “agganciati” settando i campi puntatore a NIL, nel caso di un albero binario), trasformando l’albero rosso nero in un binario di ricerca (o in un rosso nero, nel caso contrario). A questo punto, viene invocato il metodo della superclasse che potrà funzionare senza problemi eseguendo le operazioni necessarie.

Questo è il codice del metodo per l'inserimento:
    /* Metodo della classe "albero_RB" per l'inserimento di un nodo. Utilizzeremo il metodo per l'inserimento della classe padre
        evitando, così, di doverlo riscrivere. Il problema è che tale metodo è basato sui puntatori a NULL dei nodi foglia, assenti
        in un albero red-black grazie al nodo sentinella. A questo proposito, invocheremo un secondo metodo, "converti_albero", atto
        a "trasformare" temporaneamente un albero red-black in un albero binario di ricerca e viceversa. */
        void albero_RB::inserisci_nodo(string chiave)
        {
           nodo_RB* nodo_nuovo;
           nodo_nuovo = new nodo_RB(); // allocazione dinamica del nuovo nodo RB da inserire nell'albero
     
           if (radice->get_chiave()!="NIL") // se nell'albero c'è almeno un nodo...
              RB_a_BST = true; // ...andrà convertito da red-black in binario di ricerca
     
           /* se l'albero è vuoto, basta soltanto far puntare il puntatore alla radice a NULL. In presenza di almeno un nodo, invece
           questo va "sganciato" dal nodo sentinella per trasformare l'albero red-black in binario di ricerca. L'operazione va ripetuta
           per tutti i nodi dell'albero connessi col nodo sentinella. */
     
           if (RB_a_BST == true)
           {
              converti_albero (get_radice(), RB_a_BST);
              radice->set_padre(NULL); // stacco la radice dal nodo sentinella
           }
           else // l'albero è vuoto, la radice deve puntare a NULL
           {
              radice = NULL;
           }
           
           // invocazione del metodo della classe padre per l'inserimento
           albero_BST::inserisci_nodo(static_cast <nodo_BST*> (nodo_nuovo), chiave);
     
           /* Inserito il nuovo nodo, dobbiamo ritrasformare l'albero binario di ricerca in un red-black. Bisogna riagganciare tutti
           i nodi esaminati in precedenza con il nodo sentinella. */
           
           RB_a_BST = false; // l'albero binario di ricerca deve essere riconvertito in red-black
           converti_albero (get_radice(), RB_a_BST);
           radice->set_padre(NIL);
     
           bilancia_albero(nodo_nuovo); // invocazione del metodo per bilanciare l'albero red-black
        }
La sua complessità di tempo è pari a O(logn).

Questo è il codice del metodo per la conversione:
void albero_RB::converti_albero(nodo_RB* nodo, bool RB_a_BST)
        {
           if (RB_a_BST == false) // converti da BST a RB
           {
              if (nodo!=NULL)
              {
                 if (nodo->get_sx() == NULL) // controllo il figlio sinistro
                 {
                    nodo->set_sx(NIL); // se non c'è, collego quel lato del nodo al nodo sentinella
                 }
                 else // c'è il figlio
                 {
                    converti_albero (nodo->get_sx(), RB_a_BST); // scendo a sinistra
                 }
                 
                 if (nodo->get_dx() == NULL)
                 {
                    nodo->set_dx(NIL);
                 }
     
                 else // c'è il figlio
                 {
                    converti_albero (nodo->get_dx(), RB_a_BST); // scendo a destra
                 }
              }
           }
           
           else if (RB_a_BST == true) // converti da RB a BST
           {
              if (nodo!=NIL)
              {
                 if (nodo->get_sx() == NIL) // controllo il figlio sinistro
                 {
                    nodo->set_sx(NULL); // se è il nodo sentinella, sgancio quel lato del nodo  
                 }
                 else // c'è il figlio
                 {
                    converti_albero (nodo->get_sx(), RB_a_BST); // scendo a sinistra
                 }
                 
                 if (nodo->get_dx() == NIL)
                 {
                    nodo->set_dx(NULL);
                 }
     
                 else // c'è il figlio
                 {
                    converti_albero (nodo->get_dx(), RB_a_BST); // scendo a destra
                 }
              }
           }
        }
La sua complessità dovrebbe essere pari a theta(n), in quanto andranno visitati tutti i nodi dell'albero.

In totale, quindi, la complessità dovrebbe essere pari a:

theta(n) (conversione red-black->binario) + O(logn) (inserimento) + theta(n) (conversione binario->red-black)

Purtroppo, la complessità è un po' "pesantuccia", so che l'approccio non è dei migliori: un'altra soluzione, per esempio, sarebbe potuta essere quella di inserire il nodo NIL come attributo della classe padre. Diciamo che io ho preferito seguire la strada della "correttezza teorica": un albero binario NON ha un nodo sentinella per definizione.

Vorrei sapere che ne pensate di questa cosa ed, eventualmente, come si potrebbe migliorare.

Grazie in anticipo per le risposte.

3 Risposte

  • Re: Un parere tecnico

    Ciao MagoAntò,
    Vediamo se (finalmente ) ho capito:

    La classe base contiene dei metodi che testano se le foglie di un nodo sono NULL, ad esempio:
    
    if (dx == NULL)
       ...
    
    Nella classe derivata il fatto che le foglie sono a NULL viene invece espresso dal fatto di puntare ad un certo oggetto chiamato NIL, quindi il codice dovrebbe essere riscritto in questo modo:
    
    if (dx == NIL)
       ...
    
    Per evitare di riscrivere i metodi della classe base in quella derivata hai fatto quel meccanismo di conversione.

    Fin qui ho solo ripetuto quello che hai detto tu (o che credo tu abbia detto )

    Sono d' accordo che riscrivere i metodi non è una bella soluzione, sono pure d' accordo che usare il NIL nella classe base è ancora più brutto.

    Esiste un' altra soluzione:
    La classe base effettua il test sulla foglia a null in un metodo virtuale:
    
    if (IsNull(dx))
       ...
    
    virtual bool IsNull(nodo_BST* node)
    {
       return node == NULL;
    }
    
    la classe derivata ridefinisce IsNull in questo modo:
    
    virtual bool IsNull(nodo_BST* node)
    {
       return node == NIL;
    }
    
    E il tutto dovrebbe funzionare. Dimmi se può andare bene.
  • Re: Un parere tecnico

    Ciao barba59,

    l'idea è proprio ottima, risolve alla grande il problema della differenza "NIL/NULL" in alcuni metodi!

    Ho solo un problema nel metodo per l'inserimento:
    /* Metodo della classe "albero_BST" per l'inserimento di un nodo nell'albero. */
    void albero_BST::inserisci_nodo(nodo_BST* nodo, string chiave)
    {
    	/* x = puntatore al nodo corrente, partendo dalla radice
    	y = puntatore al padre del nodo corrente
    	z = puntatore al nodo da inserire */
    	nodo_BST *x, *y, *z;
    
    	z = nodo;
    	z->set_chiave(chiave); // inserimento della definizione
    
    	y = NULL;
    	x = radice;
    	
    	// Ricerca posizione del nuovo nodo
    
    	while (!(isNull(x))) // Se non sono sul nodo sentinella...
    	{
    		y = x;
    
    		if (z->get_chiave()<x->get_chiave()) // se la chiave del nuovo nodo è minore della chiave del nodo corrente...
    		{
    			x = x->get_sx(); // scendo a sinistra
    		}
    		else
    		{
    			x = x->get_dx(); // scendo a destra
    		}
    	}
    
    	z->set_padre(y); // Ho trovato il padre del nuovo nodo 
    	
    	if ((isNull(y))) // Albero vuoto
    	{
    		radice = z;
    	}
    	
    	else if (z->get_chiave()<y->get_chiave()) // la chiave del nuovo nodo è minore della chiave del nodo padre
    	{
    		y->set_sx(z); // il nuovo nodo è figlio sx del nodo padre, per rispettare la proprietà di un albero binario di ricerca
    	}
    	else
    	{
    		y->set_dx(z); // il nuovo nodo è figlio dx del nodo padre, per rispettare la proprietà di un albero binario di ricerca
    	}
    }
    Il puntatore y, che deve puntare al padre del nodo corrente puntato da x. Sto provando a settarlo a NULL o a NIL in base alla "provenienza" della chiamata, ma non ci sto riuscendo.

    Edit delle 20.02
    Forse ce l'ho fatta!

    Ho dichiarato altri due metodi virtuali...
    	virtual nodo_BST* setNodo(nodo_BST* node)
    	{
    		return NULL;
    	}
    ...e...
    	virtual nodo_BST* setNodo(nodo_BST* node)
    	{
    		return NIL;
    	}
    ...ed ho modificato il codice del metodo per l'inserimento in questo modo:
    
    z = nodo;
    	z->set_chiave(chiave); // inserimento della definizione
    y = NULL;
    	y = setNodo(y);
    	x = radice;
    Ho dovuto inizializzare y a NULL altrimenti il compilatore restituiva un warning dicendo che y veniva usato senza essere stato inizializzato, ma per il resto sembra tutto funzionare al 101%!!!!
    Quanti caffè devo offrirti? Come posso sdebitarmi?
  • Re: Un parere tecnico

    Una birretta va benone, alla salute
Devi accedere o registrarti per scrivere nel forum
3 risposte