Come determinare se l'albero binario è bilanciato?


113

È passato un po 'di tempo da quegli anni scolastici. Ho trovato lavoro come specialista IT in un ospedale. Sto cercando di passare a fare un po 'di programmazione ora. Sto lavorando su alberi binari ora e mi chiedevo quale sarebbe il modo migliore per determinare se l'albero è bilanciato in altezza.

Stavo pensando a qualcosa lungo questo:

public boolean isBalanced(Node root){
    if(root==null){
        return true;  //tree is empty
    }
    else{
        int lh = root.left.height();
        int rh = root.right.height();
        if(lh - rh > 1 || rh - lh > 1){
            return false;
        }
    }
    return true;
}

È una buona implementazione? Oppure mi sfugge qualcosa?


Se desideri vedere l'albero binario ascii di Donal Fellows con un'immagine: i.imgur.com/97C27Ek.png
user7643681

1
Buona risposta, mi ha aiutato a entrare negli Stati Uniti. (scherzi)
Henry

Risposte:


165

Mi sono imbattuto in questa vecchia domanda mentre cercavo qualcos'altro. Ho notato che non hai mai ricevuto una risposta completa.

Il modo per risolvere questo problema è iniziare scrivendo una specifica per la funzione che si sta tentando di scrivere.

Specifica: un albero binario ben formato si dice "bilanciato in altezza" se (1) è vuoto, o (2) i suoi figli destro e sinistro sono bilanciati in altezza e l'altezza dell'albero sinistro è entro 1 altezza dell'albero di destra.

Ora che hai le specifiche, il codice è banale da scrivere. Basta seguire le specifiche:

IsHeightBalanced(tree)
    return (tree is empty) or 
           (IsHeightBalanced(tree.left) and
            IsHeightBalanced(tree.right) and
            abs(Height(tree.left) - Height(tree.right)) <= 1)

Tradurre questo nel linguaggio di programmazione di tua scelta dovrebbe essere banale.

Esercizio bonus : questo ingenuo schizzo di codice attraversa l'albero troppe volte durante il calcolo delle altezze. Puoi renderlo più efficiente?

Esercizio super bonus : supponiamo che l'albero sia enormemente sbilanciato. Ad esempio, un milione di nodi profondi da un lato e tre profondi dall'altro. C'è uno scenario in cui questo algoritmo fa saltare lo stack? Potete aggiustare l'implementazione in modo che non salti mai lo stack, anche quando viene dato un albero enormemente sbilanciato?

AGGIORNAMENTO : Donal Fellows sottolinea nella sua risposta che ci sono diverse definizioni di "equilibrato" che si possono scegliere. Ad esempio, si potrebbe prendere una definizione più rigida di "altezza bilanciata" e richiedere che la lunghezza del percorso verso il bambino vuoto più vicino si trovi all'interno di uno dei percorsi verso il bambino vuoto più lontano . La mia definizione è meno rigida di quella e quindi ammette più alberi.

Si può anche essere meno rigorosi della mia definizione; si potrebbe dire che un albero bilanciato è quello in cui la lunghezza massima del percorso verso un albero vuoto su ciascun ramo differisce di non più di due, o tre, o qualche altra costante. O che la lunghezza massima del percorso è una frazione della lunghezza minima del percorso, come metà o un quarto.

Di solito non importa davvero. Lo scopo di qualsiasi algoritmo di bilanciamento degli alberi è assicurarsi di non finire nella situazione in cui si hanno un milione di nodi su un lato e tre sull'altro. La definizione di Donal va bene in teoria, ma in pratica è un problema trovare un algoritmo di bilanciamento degli alberi che soddisfi quel livello di rigore. Il risparmio in termini di prestazioni di solito non giustifica il costo di implementazione. Trascorri molto tempo facendo riorganizzazioni non necessarie per raggiungere un livello di equilibrio che in pratica fa poca differenza. Chi se ne frega se a volte occorrono quaranta rami per arrivare alla foglia più lontana di un albero imperfettamente bilanciato da un milione di nodi quando in teoria potrebbe bastare solo venti in un albero perfettamente bilanciato? Il punto è che non ci vuole mai un milione. Passare da un caso peggiore di un milione a un caso peggiore di quaranta di solito è abbastanza buono; non devi andare fino in fondo al caso ottimale.


19
+1 per la sola risposta corretta, non posso credere che nessuno sia stato in grado di rispondere per 8 mesi ...
BlueRaja - Danny Pflughoeft

1
Rispondi agli "esercizi" di seguito ...
Potatoswatter

Esercizio bonus con risposta di seguito.
Brian

La risposta di sdk di seguito sembra essere corretta e fa solo 2 attraversamenti di alberi, quindi O (n). A meno che non mi manchi qualcosa, questo non risolve almeno la tua prima domanda sui bonus. Ovviamente puoi anche utilizzare la programmazione dinamica e la tua soluzione per memorizzare nella cache altezze intermedie
Aly

Teoricamente, dovrei ancora schierarmi con la definizione di Donal Fellows.
Dhruv Gairola

26

L'equilibrio è una proprietà davvero sottile; pensi di sapere cos'è, ma è così facile sbagliare. In particolare, anche la (buona) risposta di Eric Lippert è spenta. Questo perché la nozione di altezza non è sufficiente. Devi avere il concetto di altezza minima e massima di un albero (dove l'altezza minima è il numero minimo di gradini dalla radice a una foglia, e il massimo è ... beh, ottieni l'immagine). Detto questo, possiamo definire l'equilibrio come:

Un albero in cui l'altezza massima di qualsiasi ramo non è più di uno in più dell'altezza minima di qualsiasi ramo.

(Questo in realtà implica che i rami siano essi stessi bilanciati; puoi scegliere lo stesso ramo sia per il massimo che per il minimo.)

Tutto quello che devi fare per verificare questa proprietà è un semplice attraversamento dell'albero che tiene traccia della profondità corrente. La prima volta che torni indietro, ottieni una profondità della linea di base. Dopo ogni volta che torni indietro, confronti la nuova profondità con la linea di base

  • se è uguale alla linea di base, allora continui
  • se è più di uno diverso, l'albero non è equilibrato
  • se è una tantum, ora conosci l'intervallo per il bilanciamento e tutte le profondità successive (quando stai per tornare indietro) devono essere il primo o il secondo valore.

In codice:

class Tree {
    Tree left, right;
    static interface Observer {
        public void before();
        public void after();
        public boolean end();
    }
    static boolean traverse(Tree t, Observer o) {
        if (t == null) {
            return o.end();
        } else {
            o.before();
            try {
                if (traverse(left, o))
                    return traverse(right, o);
                return false;
            } finally {
                o.after();
            }
        }
    }
    boolean balanced() {
        final Integer[] heights = new Integer[2];
        return traverse(this, new Observer() {
            int h;
            public void before() { h++; }
            public void after() { h--; }
            public boolean end() {
                if (heights[0] == null) {
                    heights[0] = h;
                } else if (Math.abs(heights[0] - h) > 1) {
                    return false;
                } else if (heights[0] != h) {
                    if (heights[1] == null) {
                        heights[1] = h;
                    } else if (heights[1] != h) {
                        return false;
                    }
                }
                return true;
            }
        });
    }
}

Suppongo che potresti farlo senza usare il pattern Observer, ma trovo più facile ragionare in questo modo.


[EDIT]: Perché non puoi semplicemente prendere l'altezza di ciascun lato. Considera questo albero:

        /\
       /  \
      /    \
     /      \_____
    /\      /     \_
   /  \    /      / \
  /\   C  /\     /   \
 /  \    /  \   /\   /\
A    B  D    E F  G H  J

OK, un po disordinato, ma ciascun lato della radice è bilanciato: Cè la profondità 2, A, B, D, Esono la profondità 3, e F, G, H, Jsono profondità 4. L'altezza del ramo sinistro è 2 (ricordiamo l'altezza diminuisce mentre attraversate il ramo), l'altezza del ramo destro è 3. Tuttavia l'albero complessivo non è bilanciato in quanto vi è un dislivello di 2 tra Ce F. È necessaria una specifica minimax (sebbene l'algoritmo effettivo possa essere meno complesso in quanto dovrebbero esserci solo due altezze consentite).


Ah, buon punto. Potresti avere un albero che è h (LL) = 4, h (LR) = 3, h (RL) = 3, h (RR) = 2. Pertanto, h (L) = 4 e h (R) = 3, quindi sembrerebbe bilanciato all'algoritmo precedente, ma con una profondità max / min di 4/2, questo non è bilanciato. Questo probabilmente avrebbe più senso con un'immagine.
Tim

1
Questo è quello che ho appena aggiunto (con l'albero grafico ASCII più cattivo del mondo).
Donal Fellows

@DonalFellows: hai menzionato l'altezza del ramo sinistro è 2. ma il ramo sinistro ha 4 nodi tra cui la radice e la foglia A. L'altezza sarà 3 in questo caso corretto
tempesta cerebrale

22

Questo determina solo se il livello superiore dell'albero è bilanciato. Cioè, potresti avere un albero con due lunghi rami all'estrema sinistra e all'estrema destra, senza nulla al centro, e questo sarebbe vero. È necessario controllare ricorsivamente root.lefte root.rightper vedere se sono bilanciati internamente prima di restituire true.


Tuttavia, se il codice avesse un metodo di altezza massima e minima, se è bilanciato globalmente, sarebbe bilanciato anche localmente.
Ari

22

Bonus risposta all'esercizio. La soluzione semplice. Ovviamente in un'implementazione reale si potrebbe includere questo o qualcosa del genere per evitare di richiedere all'utente di includere l'altezza nella risposta.

IsHeightBalanced(tree, out height)
    if (tree is empty)
        height = 0
        return true
    balance = IsHeightBalanced(tree.left, heightleft) and IsHeightBalanced(tree.right, heightright)
    height = max(heightleft, heightright)+1
    return balance and abs(heightleft - heightright) <= 1     

Se l'albero è più grande di poche centinaia di strati, ottieni un'eccezione stackoverflow. L'hai fatto in modo efficiente, ma non gestisce set di dati di medie o grandi dimensioni.
Eric Leschinski

È questo pseudocodice che hai appena inventato o è una lingua reale? (Intendo la " out height" notazione variabile)
kap

@kap: questo è pseudocodice, ma la sintassi out è presa da C #. Fondamentalmente, significa che il parametro viaggia dalla funzione chiamata al chiamante (al contrario dei parametri standard, che viaggiano dal chiamante alla funzione chiamata o ai parametri ref, che viaggiano dal chiamante alla funzione chiamata e viceversa). Ciò consente effettivamente alle funzioni di restituire più di un valore.
Brian

20

Soluzione post-ordine, attraversare l'albero solo una volta. La complessità temporale è O (n), lo spazio è O (1), è meglio della soluzione top-down. Ti do un'implementazione della versione java.

public static <T> boolean isBalanced(TreeNode<T> root){
    return checkBalance(root) != -1;
}

private static <T> int checkBalance(TreeNode<T> node){
    if(node == null) return 0;
    int left = checkBalance(node.getLeft());

    if(left == -1) return -1;

    int right = checkBalance(node.getRight());

    if(right == -1) return -1;

    if(Math.abs(left - right) > 1){
        return -1;
    }else{
        return 1 + Math.max(left, right);
    }
}

4
bella soluzione, ma la complessità dello spazio dovrebbe essere O (H) dove H è l'altezza dell'albero. Questo perché l'allocazione dello stack per la ricorsione.
legrass

Cosa left == -1significa? Quando mai sarebbe stato così? Diamo per scontato che la chiamata ricorsiva implica che left == -1è vero se tutti i sottoalberi dei bambini di sinistra sono sbilanciati?
Aspen

left == 1significa che la sottostruttura sinistra è sbilanciata, quindi l'intero albero è sbilanciato. Non dobbiamo più controllare la sottostruttura destra e possiamo tornare -1.
tning

La complessità temporale è O (n) perché devi passare attraverso tutti gli elementi. E, se avessi x nodi e ci vorrà del tempo y per controllare il saldo; se avessi 2x nodi, ci vorranno 2y tempo per controllare il saldo. Va tutto bene?
Jack

Bene, la spiegazione con il disegno è qui: algoritmi.tutorialhorizon.com/…
Shir

15

La definizione di un albero binario con bilanciamento in altezza è:

Albero binario in cui l'altezza dei due sottoalberi di ogni nodo non differisce mai di più di 1.

Quindi, un albero binario vuoto è sempre bilanciato in altezza.
Un albero binario non vuoto è bilanciato in altezza se:

  1. La sua sottostruttura sinistra è bilanciata in altezza.
  2. La sua sottostruttura destra è bilanciata in altezza.
  3. La differenza tra le altezze della sottostruttura sinistra e destra non è maggiore di 1.

Considera l'albero:

    A
     \ 
      B
     / \
    C   D

Come si è visto, la sottostruttura sinistra di Aè bilanciata in altezza (poiché è vuota), così come la sottostruttura destra. Ma ancora l'albero non è bilanciato in altezza poiché la condizione 3 non è soddisfatta come l'altezza del sottoalbero sinistro 0e l'altezza del sottoalbero destro lo è 2.

Anche l'albero seguente non è bilanciato in altezza anche se l'altezza del sottoalbero sinistro e destro sono uguali. Il codice esistente restituirà true per questo.

       A
     /  \ 
    B    C
   /      \
  D        G
 /          \
E            H

Quindi la parola ogni nella definizione è molto importante.

Questo funzionerà:

int height(treeNodePtr root) {
        return (!root) ? 0: 1 + MAX(height(root->left),height(root->right));
}

bool isHeightBalanced(treeNodePtr root) {
        return (root == NULL) ||
                (isHeightBalanced(root->left) &&
                isHeightBalanced(root->right) &&
                abs(height(root->left) - height(root->right)) <=1);
}

Ideone Link


Quindi questa risposta mi ha aiutato molto. Tuttavia, ho scoperto che il [corso introduttivo agli algoritmi del MIT] sembra contraddire la condizione 3. Pagina 4 mostra un albero RB in cui l'altezza del ramo sinistro è 2 e quello destro è 4. Potete offrirmi qualche chiarimento? Forse non ho la definizione di sottostruttura. [1]: ocw.mit.edu/courses/electrical-engineering-and-computer-science/…
i8abug

La differenza sembra derivare da questa definizione nelle note del corso. Tutti i percorsi semplici da qualsiasi nodo x a una foglia discendente hanno lo stesso numero di nodi neri = altezza nera (x)
i8abug

Giusto per proseguire, ho trovato una definizione che cambia il punto (3) nella tua risposta a "ogni foglia è 'non più di una certa distanza' dalla radice rispetto a qualsiasi altra foglia". Questo sembra soddisfare entrambi i casi. Ecco il collegamento da alcuni articoli di corsi casuali
i8abug

8

Se l'albero binario è bilanciato o meno può essere verificato dall'attraversamento dell'ordine del livello:

private boolean isLeaf(TreeNode root) {
    if (root.left == null && root.right == null)
        return true;
    return false;
}

private boolean isBalanced(TreeNode root) {
    if (root == null)
        return true;
    Vector<TreeNode> queue = new Vector<TreeNode>();
    int level = 1, minLevel = Integer.MAX_VALUE, maxLevel = Integer.MIN_VALUE;
    queue.add(root);
    while (!queue.isEmpty()) {
        int elementCount = queue.size();
        while (elementCount > 0) {
            TreeNode node = queue.remove(0);
            if (isLeaf(node)) {
                if (minLevel > level)
                    minLevel = level;
                if (maxLevel < level)
                    maxLevel = level;
            } else {
                if (node.left != null)
                    queue.add(node.left);
                if (node.right != null)
                    queue.add(node.right);
            }
            elementCount--;
        }
        if (abs(maxLevel - minLevel) > 1) {
            return false;
        }
        level++;
    }

    return true;
}

1
Ottima risposta. Penso che soddisfi tutti i requisiti che Eric ha pubblicato in merito a Bonus e Super-Bonus. È iterativo (utilizzando una coda) e non ricorsivo, quindi lo stack di chiamate non verrà sovraccaricato e spostiamo tutti i problemi di memoria nell'heap. Non richiede nemmeno l'attraversamento dell'intero albero. Si sposta livello per livello, quindi se un albero è grossolanamente sbilanciato su un lato, lo troverà molto presto (il più presto? Ben prima della maggior parte degli algoritmi ricorsivi, anche se potresti implementare un algoritmo iterativo di attraversamento post-ordine che troverà l'ultimo livello squilibri prima, ma agirà in modo peggiore ai primi livelli) Quindi +1 :-)
David Refaeli

7

Questo viene reso molto più complicato di quanto non sia in realtà.

L'algoritmo è il seguente:

  1. Sia A = profondità del nodo di livello più alto
  2. Sia B = profondità del nodo di livello più basso

  3. Se abs (AB) <= 1, l'albero è bilanciato


Semplice e lineare!
Wasim Thabraze

3
Due problemi, non è efficiente come potrebbe essere, stai facendo due passaggi sull'intero albero. E per gli alberi che hanno un nodo a sinistra e migliaia a destra, passi inutilmente attraverso l'intera cosa, quando avresti potuto fermarti dopo 3 controlli.
Eric Leschinski

5

Ciò che significa equilibrato dipende un po 'dalla struttura a portata di mano. Ad esempio, A B-Tree non può avere nodi più di una certa profondità dalla radice, o meno se è per questo, tutti i dati vivono a una profondità fissa dalla radice, ma può essere sbilanciato se la distribuzione delle foglie alle foglie -ma-uno dei nodi non è uniforme. Elenchi da saltare Non avere alcuna nozione di equilibrio, affidandoti invece alla probabilità per ottenere prestazioni decenti. Gli alberi di Fibonacci cadono intenzionalmente fuori equilibrio, posticipando il riequilibrio per ottenere prestazioni asintotiche superiori in cambio di aggiornamenti occasionalmente più lunghi. Gli alberi AVL e Rosso-Nero allegano metadati a ciascun nodo per ottenere un'invariante di bilanciamento della profondità.

Tutte queste strutture e altre ancora sono presenti nelle librerie standard dei più comuni sistemi di programmazione (eccetto python, RAGE!). L'implementazione di uno o due è una buona pratica di programmazione, ma probabilmente non è un buon uso del tempo per tirare il proprio per la produzione, a meno che il tuo problema non abbia alcune prestazioni peculiari che non devono essere soddisfatte da nessuna raccolta disponibile.


4

Nota 1: l'altezza di qualsiasi sottoalbero viene calcolata una sola volta.

Nota 2: se il sottoalbero di sinistra è sbilanciato, il calcolo del sottoalbero di destra, potenzialmente contenente milioni di elementi, viene saltato.

// return height of tree rooted at "tn" if, and only if, it is a balanced subtree
// else return -1
int maxHeight( TreeNode const * tn ) {
    if( tn ) {
        int const lh = maxHeight( tn->left );
        if( lh == -1 ) return -1;
        int const rh = maxHeight( tn->right );
        if( rh == -1 ) return -1;
        if( abs( lh - rh ) > 1 ) return -1;
        return 1 + max( lh, rh );
    }
    return 0;
}

bool isBalanced( TreeNode const * root ) {
    // Unless the maxHeight is -1, the subtree under "root" is balanced
    return maxHeight( root ) != -1;
}

3

Il bilanciamento di solito dipende dalla lunghezza del percorso più lungo in ciascuna direzione. L'algoritmo di cui sopra non lo farà per te.

Cosa stai cercando di implementare? Ci sono alberi che si autobilanciano intorno (AVL / Rosso-nero). In effetti, gli alberi Java sono bilanciati.



3
public boolean isBalanced(TreeNode root)
{
    return (maxDepth(root) - minDepth(root) <= 1);
}

public int maxDepth(TreeNode root)
{
    if (root == null) return 0;

    return 1 + max(maxDepth(root.left), maxDepth(root.right));
}

public int minDepth (TreeNode root)
{
    if (root == null) return 0;

    return 1 + min(minDepth(root.left), minDepth(root.right));
}

Penso che questa soluzione non sia corretta. Se passi un albero che ha un singolo nodo, cioè una radice, tornerà come maxDepth 1(lo stesso per minDepth). La profondità corretta però dovrebbe essere 0. La radice di un albero ha sempre 0profondità
Cratylus

3

Ecco una soluzione testata ed elaborata completa in C # (mi dispiace, non sono java dev) (basta copiare e incollare nell'app console). So che la definizione di bilanciato varia, quindi non tutti potrebbero apprezzare i risultati del mio test, ma per favore guarda l'approccio leggermente diverso di controllare profondità / altezza in un ciclo ricorsivo ed uscire al primo disadattamento senza salvare altezza / livello / profondità del nodo su ciascun nodo (mantenendolo solo in una chiamata di funzione).

using System;
using System.Linq;
using System.Text;

namespace BalancedTree
{
    class Program
    {
        public static void Main()
        {
            //Value Gathering
            Console.WriteLine(RunTreeTests(new[] { 0 }));
            Console.WriteLine(RunTreeTests(new int[] { }));

            Console.WriteLine(RunTreeTests(new[] { 0, 1, 2, 3, 4, -1, -4, -3, -2 }));
            Console.WriteLine(RunTreeTests(null));
            Console.WriteLine(RunTreeTests(new[] { 10, 8, 12, 8, 4, 14, 8, 10 }));
            Console.WriteLine(RunTreeTests(new int[] { 20, 10, 30, 5, 15, 25, 35, 3, 8, 12, 17, 22, 27, 32, 37 }));

            Console.ReadKey();
        }

        static string RunTreeTests(int[] scores)
        {
            if (scores == null || scores.Count() == 0)
            {
                return null;
            }

            var tree = new BinarySearchTree();

            foreach (var score in scores)
            {
                tree.InsertScore(score);
            }

            Console.WriteLine(tree.IsBalanced());

            var sb = tree.GetBreadthWardsTraversedNodes();

            return sb.ToString(0, sb.Length - 1);
        }
    }

    public class Node
    {
        public int Value { get; set; }
        public int Count { get; set; }
        public Node RightChild { get; set; }
        public Node LeftChild { get; set; }
        public Node(int value)
        {
            Value = value;
            Count = 1;
        }

        public override string ToString()
        {
            return Value + ":" + Count;
        }

        public bool IsLeafNode()
        {
            return LeftChild == null && RightChild == null;
        }

        public void AddValue(int value)
        {
            if (value == Value)
            {
                Count++;
            }
            else
            {
                if (value > Value)
                {
                    if (RightChild == null)
                    {
                        RightChild = new Node(value);
                    }
                    else
                    {
                        RightChild.AddValue(value);
                    }
                }
                else
                {
                    if (LeftChild == null)
                    {
                        LeftChild = new Node(value);
                    }
                    else
                    {
                        LeftChild.AddValue(value);
                    }
                }
            }
        }
    }

    public class BinarySearchTree
    {
        public Node Root { get; set; }

        public void InsertScore(int score)
        {
            if (Root == null)
            {
                Root = new Node(score);
            }
            else
            {
                Root.AddValue(score);
            }
        }

        private static int _heightCheck;
        public bool IsBalanced()
        {
            _heightCheck = 0;
            var height = 0;
            if (Root == null) return true;
            var result = CheckHeight(Root, ref height);
            height--;
            return (result && height == 0);
        }

        private static bool CheckHeight(Node node, ref int height)
        {
            height++;
            if (node.LeftChild == null)
            {
                if (node.RightChild != null) return false;
                if (_heightCheck != 0) return _heightCheck == height;
                _heightCheck = height;
                return true;
            }
            if (node.RightChild == null)
            {
                return false;
            }

            var leftCheck = CheckHeight(node.LeftChild, ref height);
            if (!leftCheck) return false;
            height--;
            var rightCheck = CheckHeight(node.RightChild, ref height);
            if (!rightCheck) return false;
            height--;
            return true;
        }


        public StringBuilder GetBreadthWardsTraversedNodes()
        {
            if (Root == null) return null;
            var traversQueue = new StringBuilder();
            traversQueue.Append(Root + ",");
            if (Root.IsLeafNode()) return traversQueue;
            TraversBreadthWards(traversQueue, Root);
            return traversQueue;
        }

        private static void TraversBreadthWards(StringBuilder sb, Node node)
        {
            if (node == null) return;
            sb.Append(node.LeftChild + ",");
            sb.Append(node.RightChild + ",");
            if (node.LeftChild != null && !node.LeftChild.IsLeafNode())
            {
                TraversBreadthWards(sb, node.LeftChild);
            }
            if (node.RightChild != null && !node.RightChild.IsLeafNode())
            {
                TraversBreadthWards(sb, node.RightChild);
            }
        }
    }
}

Non capisco come qualcuno possa votare negativamente questa risposta entro 2 minuti dall'invio della risposta ?? Il voto negativo va bene, ma potresti spiegare cosa c'è di sbagliato in questa soluzione?
sbp

2
#include <iostream>
#include <deque>
#include <queue>

struct node
{
    int data;
    node *left;
    node *right;
};

bool isBalanced(node *root)
{
    if ( !root)
    {
        return true;
    }

    std::queue<node *> q1;
    std::queue<int>  q2;
    int level = 0, last_level = -1, node_count = 0;

    q1.push(root);
    q2.push(level);

    while ( !q1.empty() )
    {
        node *current = q1.front();
        level = q2.front();

        q1.pop();
        q2.pop();

        if ( level )
        {
            ++node_count;
        }

                if ( current->left )
                {
                        q1.push(current->left);
                        q2.push(level + 1);
                }

                if ( current->right )
                {
                        q1.push(current->right);
                        q2.push(level + 1);
                }

        if ( level != last_level )
        {
            std::cout << "Check: " << (node_count ? node_count - 1 : 1) << ", Level: " << level << ", Old level: " << last_level << std::endl;
            if ( level && (node_count - 1) != (1 << (level-1)) )
            {
                return false;
            }

            last_level = q2.front();
            if ( level ) node_count = 1;
        }
    }

    return true;
}

int main()
{
    node tree[15];

    tree[0].left  = &tree[1];
    tree[0].right = &tree[2];
    tree[1].left  = &tree[3];
    tree[1].right = &tree[4];
    tree[2].left  = &tree[5];
    tree[2].right = &tree[6];
    tree[3].left  = &tree[7];
    tree[3].right = &tree[8];
    tree[4].left  = &tree[9];   // NULL;
    tree[4].right = &tree[10];  // NULL;
    tree[5].left  = &tree[11];  // NULL;
    tree[5].right = &tree[12];  // NULL;
    tree[6].left  = &tree[13];
    tree[6].right = &tree[14];
    tree[7].left  = &tree[11];
    tree[7].right = &tree[12];
    tree[8].left  = NULL;
    tree[8].right = &tree[10];
    tree[9].left  = NULL;
    tree[9].right = &tree[10];
    tree[10].left = NULL;
    tree[10].right= NULL;
    tree[11].left = NULL;
    tree[11].right= NULL;
    tree[12].left = NULL;
    tree[12].right= NULL;
    tree[13].left = NULL;
    tree[13].right= NULL;
    tree[14].left = NULL;
    tree[14].right= NULL;

    std::cout << "Result: " << isBalanced(tree) << std::endl;

    return 0;
}

potresti voler aggiungere alcuni commenti
jgauffin

2

RE: soluzione di @ lucky che utilizza un BFS per eseguire un attraversamento dell'ordine dei livelli.

Attraversiamo l'albero e manteniamo un riferimento a vars min / max-level che descrivono il livello minimo al quale un nodo è una foglia.

Credo che la soluzione @lucky richieda una modifica. Come suggerito da @codaddict, invece di controllare se un nodo è una foglia, dobbiamo controllare se SIA il figlio sinistro che quello destro è nullo (non entrambi). Altrimenti, l'algoritmo considererebbe questo un albero bilanciato valido:

     1
    / \
   2   4
    \   \
     3   1

In Python:

def is_bal(root):
    if root is None:
        return True

    import queue

    Q = queue.Queue()
    Q.put(root)

    level = 0
    min_level, max_level = sys.maxsize, sys.minsize

    while not Q.empty():
        level_size = Q.qsize()

        for i in range(level_size):
            node = Q.get()

            if not node.left or node.right:
                min_level, max_level = min(min_level, level), max(max_level, level)

            if node.left:
                Q.put(node.left)
            if node.right:
                Q.put(node.right)

        level += 1

        if abs(max_level - min_level) > 1:
            return False

    return True

Questa soluzione dovrebbe soddisfare tutte le disposizioni fornite nella domanda iniziale, operando nel tempo O (n) e nello spazio O (n). L'overflow della memoria verrebbe indirizzato all'heap piuttosto che soffiare uno stack di chiamate ricorsivo.

In alternativa, potremmo inizialmente attraversare l'albero per calcolare + cache altezze massime per ogni sottostruttura radice in modo iterativo. Quindi, in un'altra esecuzione iterativa, controlla se le altezze memorizzate nella cache delle sottostrutture sinistra e destra per ciascuna radice non differiscono mai di più di una. Questo verrebbe eseguito anche nel tempo O (n) e nello spazio O (n) ma in modo iterativo in modo da non causare overflow dello stack.


1

Bene, hai bisogno di un modo per determinare le altezze di sinistra e destra e se sinistra e destra sono bilanciate.

E io solo return height(node->left) == height(node->right);

Per quanto riguarda la scrittura di una heightfunzione, leggi: Capire la ricorsione


3
Vuoi che le altezze sinistra e destra siano entro 1, non necessariamente uguali.
Alex B

1

Di che tipo di albero stai parlando? Ci sono auto-bilanciamento alberi là fuori. Controlla i loro algoritmi dove determinano se hanno bisogno di riordinare l'albero per mantenere l'equilibrio.


1

Ecco una versione basata su un generico attraversamento in profondità. Dovrebbe essere più veloce dell'altra risposta corretta e gestire tutte le "sfide" menzionate. Mi scuso per lo stile, non conosco davvero Java.

Puoi ancora renderlo molto più veloce tornando in anticipo se max e min sono entrambi impostati e hanno una differenza> 1.

public boolean isBalanced( Node root ) {
    int curDepth = 0, maxLeaf = 0, minLeaf = INT_MAX;
    if ( root == null ) return true;
    while ( root != null ) {
        if ( root.left == null || root.right == null ) {
            maxLeaf = max( maxLeaf, curDepth );
            minLeaf = min( minLeaf, curDepth );
        }
        if ( root.left != null ) {
            curDepth += 1;
            root = root.left;
        } else {
            Node last = root;
            while ( root != null
             && ( root.right == null || root.right == last ) ) {
                curDepth -= 1;
                last = root;
                root = root.parent;
            }
            if ( root != null ) {
                curDepth += 1;
                root = root.right;
            }
        }
    }
    return ( maxLeaf - minLeaf <= 1 );
}

1
Un bel tentativo ma chiaramente non funziona. Sia x un nodo nullo. Lascia che un nodo dell'albero non nullo sia indicato come (LEFT VALUE RIGHT). Considera l'albero (x A (x B x)). "root" punta ai nodi A, B, A, B, A, B ... per sempre. Ti va di riprovare? Un suggerimento: in realtà è più facile senza i puntatori genitore.
Eric Lippert

@ Eric: Oops, risolto (credo). Bene, sto cercando di farlo senza memoria O (profondità) e se la struttura non ha puntatori genitore (spesso lo fa), è necessario utilizzare uno stack.
Potatoswatter

Quindi quello che mi stai dicendo è che preferiresti usare la memoria permanente O (n) nei puntatori genitore per evitare di allocare la memoria temporanea O (d), dove log n <= d <= n? Questa sembra una falsa economia.
Eric Lippert

Sfortunatamente, sebbene tu abbia risolto il problema con l'attraversamento, qui c'è un problema molto più grande. Questo non verifica se un albero è bilanciato, ma verifica se un albero ha tutte le sue foglie vicine allo stesso livello. Questa non è la definizione di "equilibrato" che ho dato. Considera l'albero ((((x D x) C x) B x) A x). Il tuo codice segnala che questo è "bilanciato" quando ovviamente è sbilanciato al massimo. Ti va di riprovare?
Eric Lippert

@Eric risposta 1: non una falsa economia se usi già i puntatori genitore per qualcos'altro. risposta 2: certo, perché no. Questo è un modo bizzarro di eseguire il debug ... Non dovrei scrivere ciecamente traversate di nulla alle 4 del mattino ...
Potatoswatter

1
/* Returns true if Tree is balanced, i.e. if the difference between the longest path and the shortest path from the root to a leaf node is no more than than 1. This difference can be changed to any arbitrary positive number. */
boolean isBalanced(Node root) {
    if (longestPath(root) - shortestPath(root) > 1)
        return false;
    else
        return true;
}


int longestPath(Node root) {
    if (root == null);
        return 0;
    else {
        int leftPathLength = longestPath(root.left);
        int rightPathLength = longestPath(root.right);
        if (leftPathLength >= rightPathLength)
            return leftPathLength + 1;
        else
            return rightPathLength + 1;
    }
}

int shortestPath(Node root) {
    if (root == null);
        return 0;
    else {
        int leftPathLength = shortestPath(root.left);
        int rightPathLength = shortestPath(root.right);
        if (leftPathLength <= rightPathLength)
            return leftPathLength + 1;
        else
            return rightPathLength + 1;
    }
}

1
Dovresti aggiungere una descrizione alla tua risposta e / o commenti al tuo esempio di codice.
Brad Campbell

1
class Node {
    int data;
    Node left;
    Node right;

    // assign variable with constructor
    public Node(int data) {
        this.data = data;
    }
}

public class BinaryTree {

    Node root;

    // get max depth
    public static int maxDepth(Node node) {
        if (node == null)
            return 0;

        return 1 + Math.max(maxDepth(node.left), maxDepth(node.right));
    }

    // get min depth
    public static int minDepth(Node node) {
        if (node == null)
            return 0;

        return 1 + Math.min(minDepth(node.left), minDepth(node.right));
    }

    // return max-min<=1 to check if tree balanced
    public boolean isBalanced(Node node) {

        if (Math.abs(maxDepth(node) - minDepth(node)) <= 1)
            return true;

        return false;
    }

    public static void main(String... strings) {
        BinaryTree tree = new BinaryTree();
        tree.root = new Node(1);
        tree.root.left = new Node(2);
        tree.root.right = new Node(3);


        if (tree.isBalanced(tree.root))
            System.out.println("Tree is balanced");
        else
            System.out.println("Tree is not balanced");
    }
}

0

Ecco cosa ho provato per l'esercizio bonus di Eric. Cerco di sciogliere i miei loop ricorsivi e di tornare non appena trovo che un sottoalbero non è bilanciato.

int heightBalanced(node *root){
    int i = 1;
    heightBalancedRecursive(root, &i);
    return i; 
} 

int heightBalancedRecursive(node *root, int *i){

    int lb = 0, rb = 0;

    if(!root || ! *i)  // if node is null or a subtree is not height balanced
           return 0;  

    lb = heightBalancedRecursive(root -> left,i);

    if (!*i)         // subtree is not balanced. Skip traversing the tree anymore
        return 0;

    rb = heightBalancedRecursive(root -> right,i)

    if (abs(lb - rb) > 1)  // not balanced. Make i zero.
        *i = 0;

    return ( lb > rb ? lb +1 : rb + 1); // return the current height of the subtree
}

0
public int height(Node node){
    if(node==null)return 0;
    else{
        int l=height(node.leftChild);
        int r=height(node.rightChild);
       return(l>r?l+1:r+1);

}}
public boolean balanced(Node n){

    int l= height(n.leftChild);
    int r= height(n.rightChild);

    System.out.println(l + " " +r);
    if(Math.abs(l-r)>1)
        return false;
    else 
        return true;
    }

0

Un albero vuoto è bilanciato in altezza. Un albero binario T non vuoto è bilanciato se:

1) La sottostruttura sinistra di T è bilanciata

2) La sottostruttura destra di T è bilanciata

3) La differenza tra le altezze del sottoalbero sinistro e del sottoalbero destro non è maggiore di 1.

/* program to check if a tree is height-balanced or not */
#include<stdio.h>
#include<stdlib.h>
#define bool int

/* A binary tree node has data, pointer to left child
   and a pointer to right child */
struct node
{
  int data;
  struct node* left;
  struct node* right;
};

/* The function returns true if root is balanced else false
   The second parameter is to store the height of tree.  
   Initially, we need to pass a pointer to a location with value 
   as 0. We can also write a wrapper over this function */
bool isBalanced(struct node *root, int* height)
{
  /* lh --> Height of left subtree 
     rh --> Height of right subtree */   
  int lh = 0, rh = 0;  

  /* l will be true if left subtree is balanced 
    and r will be true if right subtree is balanced */
  int l = 0, r = 0;

  if(root == NULL)
  {
    *height = 0;
     return 1;
  }

  /* Get the heights of left and right subtrees in lh and rh 
    And store the returned values in l and r */   
  l = isBalanced(root->left, &lh);
  r = isBalanced(root->right,&rh);

  /* Height of current node is max of heights of left and 
     right subtrees plus 1*/   
  *height = (lh > rh? lh: rh) + 1;

  /* If difference between heights of left and right 
     subtrees is more than 2 then this node is not balanced
     so return 0 */
  if((lh - rh >= 2) || (rh - lh >= 2))
    return 0;

  /* If this node is balanced and left and right subtrees 
    are balanced then return true */
  else return l&&r;
}


/* UTILITY FUNCTIONS TO TEST isBalanced() FUNCTION */

/* Helper function that allocates a new node with the
   given data and NULL left and right pointers. */
struct node* newNode(int data)
{
    struct node* node = (struct node*)
                                malloc(sizeof(struct node));
    node->data = data;
    node->left = NULL;
    node->right = NULL;

    return(node);
}

int main()
{
  int height = 0;

  /* Constructed binary tree is
             1
           /   \
         2      3
       /  \    /
     4     5  6
    /
   7
  */   
  struct node *root = newNode(1);  
  root->left = newNode(2);
  root->right = newNode(3);
  root->left->left = newNode(4);
  root->left->right = newNode(5);
  root->right->left = newNode(6);
  root->left->left->left = newNode(7);

  if(isBalanced(root, &height))
    printf("Tree is balanced");
  else
    printf("Tree is not balanced");    

  getchar();
  return 0;
}

Complessità temporale: O (n)


0

Per avere prestazioni migliori specialmente su alberi enormi, puoi salvare l'altezza in ogni nodo in modo che sia un compromesso tra spazio e prestazioni:

class Node {
    Node left;
    Node right;
    int value;
    int height;
}

Esempio di implementazione dell'aggiunta e stesso per la cancellazione

void addNode(Node root,int v)
{    int height =0;
     while(root != null)
     {
         // Since we are adding new node so the height 
         // will increase by one in each node we will pass by
         root.height += 1;
         height++;
         else if(v > root.value){
            root = root.left();
            }
         else{
         root = root.right();
         }

     }

         height++;
         Node n = new Node(v , height);
         root = n;         
}
int treeMaxHeight(Node root)
{
 return Math.Max(root.left.height,root.right.height);
}

int treeMinHeight(Node root)
{
 return Math.Min(root.left.height,root.right.height);

}

Boolean isNodeBlanced(Node root)
{
   if (treeMaxHeight(root) - treeMinHeight(root) > 2)
       return false;

  return true;
}

Boolean isTreeBlanced (Node root)
{
    if(root == null || isTreeBalanced(root.left) && isTreeBalanced(root.right) && isNodeBlanced(root))
    return true;

  return false;

}

-1
    static boolean isBalanced(Node root) {
    //check in the depth of left and right subtree
    int diff = depth(root.getLeft()) - depth(root.getRight());
    if (diff < 0) {
        diff = diff * -1;
    }
    if (diff > 1) {
        return false;
    }
    //go to child nodes
    else {
        if (root.getLeft() == null && root.getRight() == null) {
            return true;
        } else if (root.getLeft() == null) {
            if (depth(root.getRight()) > 1) {
                return false;
            } else {
                return true;
            }
        } else if (root.getRight() == null) {
            if (depth(root.getLeft()) > 1) {
                return false;
            } else {
                return true;
            }
        } else if (root.getLeft() != null && root.getRight() != null && isBalanced(root.getLeft()) && isBalanced(root.getRight())) {
            return true;
        } else {
            return false;
        }
    }
}

-2

Non funzionerebbe?

return ( ( Math.abs( size( root.left ) - size( root.right ) ) < 2 );

Qualsiasi albero sbilanciato fallirebbe sempre.


4
Anche molti alberi equilibrati falliranno.
Brian
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.