[Algoritmi] verifica Albero binario completo

xneo1
salve a tutti,
come il titolo suggerisce dovrei implementare un algoritmo che mi verifichi se l'albero che passo come input è completo.

Per chi non lo sapesse un albero si dice completo se è pieno almeno fino al penultimo livello e nell'ultimo livello le foglie sono compattate a sinistra.
Un albero binario è pieno se ogni nodo interno ha tutti e due i figli e le foglie si trovano tutte alla stessa profondità.

Non ho nessuna difficoltà a verificare se un albero è pieno.
Mostro l'algoritmo (il codice è un misto di Java)

boolean verificaPieno(Albero a, int livello) {
    if(livello<0) return false;
    if(livello==0) return a!=null;
    
    if(a==null) return false;

    Albero figlioSin=a.sin();
    Albero figlioDes=a.des();

     if(figlioSin==null && figlioDes!=null) return false;
     if(figlioSin!=null && figlioDes==null) return false;

    return pieno(figlioSin, livello-1) && pieno(figlioDes, livello-1);
}


Per quanto riguarda L'albero completo ho pensato a questo approccio

if(verificaPieno(a, livello-1)) {
controllaFoglieASinistra(a);
}

purtroppo ho difficoltà a controllare che tutte le foglie sono controllate a sinistra

Risposte
Giova411
Se si potesse avere un attributo tipo int come v.livello che possiede ciascun nodo sarebbe fattibile, mi da informazione utile. Quindi si potrebbe creare un solo array per il solo livello ultimo (dato in input) con le sentinelle messe quando ho nodi NIL. Dimensione sarà $A[1..2^(livellol-1)]$ :?:
Poi ricerca dicotomica che divide la ricerca per 2 ogni volta :?

Facciamo il codice? XNEO?!?!?!?!? :smt066

xneo1
Per la complessità temporale di verificaCompleto provo a fare questo ragionamento:

verificaPieno() ha complessità O(n)
poi c'è la fase di riempimento della coda, che è una sorta di visita per livelli e richiede tempo O(n)
infine abbiamo la verifica se le foglie sono tutte a sinistra e questa fase richiede tempo O(2^livello)
per cui ne deduco che la complessità spaziale è O(n)

Per quanto riguarda quella temporale:
verifica pieno richiede O(log(n)) spazio
mentre per la coda richiede al più O(2^livello) di spazio.

Quindi la complessità spaziale è 2^livello.

Se si riuscisse a non usare nessuna struttura dati per controllare che le foglie siano compattate a sinistra si avrebbe complessità spaziale logaritmica. Tuttavia così come stanno le cose siamo sotto una complessità spaziale lineare

xneo1
secondo me non serve fare la ricerca binaria. In quanto non si avrebbero significativi vantaggi.

Così come è scritto adesso la complessità temporale è 2O(n)+O(2^livello)
Se facciamo la ricerca binaria la complessità diventa 2O(n)+O(log(n))

Attenzione:
2^livello sembrerebbe un termine esponenziale, ma in realtà è minore di n, anzi sono (n/2)+1.

Giova411
Ma io ho proposto una ricerca binaria in un array che contiene le sole foglie. Per l'albero che sta sopra non opero con nessuna struttura dati. Le sole foglie le metto in questo array e poi lì verifico se sono accatastate a sinistra.
Cmq non dare troppo peso alle mie deduzioni, Only potrebbe davvero segnarti una strada ALTERNATIVA E MIGLIORE a quella che hai trovato solo.
Però rileggi le cose che dicevamo. Se poi vuoi possiamo scrivere il codice che, in queso caso, prende quello mio "semplice" ed "elegante" :-) fino al penultimo livello. Poi arrivati all'ultimo "giochiamo" con un semplice array (e non una coda!) che non contiene tutti gli n elementi! Sole le foglie ed i NIL. Verifichiamo poi con sentinelle se codesti NIL sono tutti consecutivi a destra ed i valori reali delle foglie consecutive tutte a sinistra.
Che dici? Ma aspettiamo Only che, invece di fare la tesi, ha una pazienza $+oo$ verso gli utenti di Matematicamente ma, soprattutto, verso di noi :goodman:

Giova411
boolean completo(TREE t, integer l)
 if t=nil return true
 if t.left=nil and t.right=nil then return true
% t.livello si presuppone che sia memorizzato all'interno dei nodi in un campo speciale
else if (t.livello < l) then     
    if t.left=nil or t.right=nil then return false
    if t.left!=nil and t.right!=nil then 
               return completo(t.left, t.livello) and completo(t.right, t.livello)
    else return false
else if (t.livello = l) then
 % siamo all'ultimo livello "problematico" delle foglie dato in input
    integer[] A =  new integer[1 .. 2^(livello−1) ]
    integer i=1
    while i<=2^(livello−1) do
     if t.left != nil then 
        A[i] = t.value
        i++
     if t.right != nil then
        A[i] = t.value
        i++    
      else 
         A[i] = +oo
       	 i++
% qui non so bene che fare, abbiamo riempito un array con valori e NIL (ma messi sottoforma di +oo)
% dobbiamo controllare questo array, anche in modo sequenziale ma avrei complessità non buona
integer i=1
boolean nodonil=false 
% uso nodonil come semaforo
    while( i<=2^(livello−1) and !nodonil) do
        if !(A[i]< +oo) then nodonil=true
       i++
  i--
    while(i<= 2^(livello−1) and nodonil) do
      if (A[i]<+oo) then return false
       i++    
  return true
:oops: :oops: :oops:

onlyReferee
Non vorrei che mi sfuggisse qualche dettaglio ma...la ricerca binaria (o dicotomica) che dir si voglia presuppone che l'array sia ordinato. Inoltre...per il controllo che abbiamo da implementare non vedo come possa essere utile (sempre se non ho preso abbagli).

Giova411
"onlyReferee":
Non vorrei che mi sfuggisse qualche dettaglio ma...la ricerca binaria (o dicotomica) che dir si voglia presuppone che l'array sia ordinato. Inoltre...per il controllo che abbiamo da implementare non vedo come possa essere utile (sempre se non ho preso abbagli).

Sì non l'ho usata poi!
[size=50]SEI MITICO ONLY[/size]




Ovviamente dipende da cosa ci capita come albero!!! Se è veramente completo e pieno a sinistra abbiamo complessità peggiore in qualsiasi campo

xneo1
Ii in effetti la ricerca binaria non si può fare, e comunque se si potesse fare richiederebbe tempo O(2^livello) (ho sbagliato prima).

Comunque per me il problema è risolto, perchè il codice che ho scritto funziona, con le modifiche proposte da onlyReferee si risparmia qualcosa a livello di occupazione di memoria.

L'unico modo per abbassare la complessità spaziale è implementare una procedura che controlla il compattamento delle foglie a sinistra in tempo O(log(n))

onlyReferee
Vi riporto solo la parte che ho corretto nel codice (commentato nelle parti sistemate):
while i<=2^(livello−1) do
     if t.left != nil then 
        //In A[2 * i] (non in A[i]) devo mettere t.left.value (non t.value) ed in A[2 * i + 1] (non in A[i]) t.right.value (non t.value)
        A[2 * i] = t.left.value
        i++
     if t.right != nil then
        A[2 * i + 1] = t.right.value
        i++    
      else 
         A[i] = +oo
           i++
% qui non so bene che fare, abbiamo riempito un array con valori e NIL (ma messi sottoforma di +oo)
% dobbiamo controllare questo array, anche in modo sequenziale ma avrei complessità non buona
integer i=1
boolean nodonil=false 
% uso nodonil come semaforo
    while( i<=2^(livello−1) and !nodonil) do
        if !(A[i]< +oo) then nodonil=true
       i++
    //Non serve i++, sono già posizionato giusto ed evito di fare un controllo in più
    while(i<= 2^(livello−1) and nodonil) do
      if (A[i]<+oo) then return false
       i++    
  return true

Giova411
@xneo: Secondo me sulla complessità spaziale ti sbagli perché utilizzi una coda in tutti i livelli e non serve. Immagina questa fisarmonica che si riempie e si svuota sommata allo stack che creano le ricorrenze..
Come detto prima, ma sopratutto da Only che è dotto, il codice delle prime pagine dovrebbe andare. =)
Se si hanno più versioni si impara molto in questa materia!!! :)

onlyReferee
Anche io notavo questo fatto della coda, Giova411. Non ci ho dato molto peso in fase iniziale. In realtà quando poi si lavora nell'area stack con la memoria statica ciò che viene memorizzato per la coda al momento della chiamata ricorsiva è la referenza (indirizzo) al primo elemento della stessa e non l'intera struttura (altrimenti si esaurirebbe la memoria nel giro di brevissimo tempo chiaramente). Almeno così funziona per Java (lo dico visto che è il linguaggio che sta usando xneo).
Certo, confrontare le versioni è sempre utilissimo.

Giova411
Non posso spannare!!!!


La prima versione di xneo, però, devo vederla meglio...
E' un po' disordinata :? quello sì :-D

xneo1
Allora il massimo numero di puntatori (perchè alla fine di nella coda ci sono puntatori ai nodi degli alberi) e quindi la massima lunghezza della coda è 2^livello (se non erro).
Inoltre la coda viene riempita in una parte iterativa dell'algoritmo. Ciò che è ricorsivo è il metodo verificaPieno.

Dalle piccole conoscenze che ho (alcune grazie anche a onlyReferee :)) la complessità spaziale non è il risultato della somma della memoria occupata istante per istante durante l'esecuzione dell'algoritmo, ma la massima memoria utilizzata dall'algoritmo.

Faccio un esempio.
Istante t0 --> lunghezza coda=0
Istante t1 --> lunghezza coda=8
Istante t2 --> lunghezza coda=2
Istante t3 --> lunghezza coda=3
Istante t4 --> lunghezza coda=1, e qui termina l'algoritmo.

La complessità spaziale in base al numero di elementi della coda risulterebbe uguale a 8 e non uguale alla somma delle lunghezze istante per istante.

Giova411
Perdonami, ma al livello ultimo non riempi cmq al massimo la coda con valori NIL pure quando non ci sono foglie vere?

xneo1
Si, certo.
Nell'ultimo livello ci sono 2^livello nodi (compresi i nodi=null)

Rispondi
Per rispondere a questa discussione devi prima effettuare il login.