web-dev-qa-db-fra.com

La meilleure façon de calculer la hauteur dans un arbre de recherche binaire? (équilibrer un arbre AVL)

Je cherche le meilleur moyen de calculer un solde de nœuds dans un AVL-tree . Je pensais que cela fonctionnait, mais après quelques insertions/mises à jour lourdes, je peux voir que cela ne fonctionne pas correctement (du tout).

C'est une sorte de question en deux parties, la première partie serait comment calculer la hauteur d'un sous-arbre, je connais la définition "La hauteur d'un nœud est la longueur du plus long chemin descendant vers un feuille de ce noeud. " et je le comprends, mais je ne parviens pas à le mettre en oeuvre. Et pour m'embrouiller davantage, cette citation peut être trouvée sur Wikipedia sur les hauteurs d'arbre "Classiquement, la valeur -1 correspond à un sous-arbre sans nœuds, alors que zéro correspond à un sous-arbre avec un seul nœud."

Et la deuxième partie consiste à obtenir le facteur d'équilibre d'un sous-arbre dans un arbre AVL. Je n'ai aucun problème à comprendre le concept, "obtenir la hauteur de votre L et R sous-arbres et soustrayez R de L ". Et ceci est défini comme quelque chose comme ceci: BALANCE = NODE[L][HEIGHT] - NODE[R][HEIGT]

La lecture sur wikipedia indique ceci sur les premières lignes décrivant les insertions dans une arborescence AVL: "Si le facteur de balance devient -1, 0 ou 1, l’arborescence est toujours au format AVL et aucune rotation n’est nécessaire."

Il continue ensuite en disant ceci "Si le facteur d’équilibre devient 2 ou -2, alors l’arbre enraciné à ce noeud est déséquilibré et une rotation de l’arbre est nécessaire. Au plus, une rotation simple ou double sera nécessaire pour équilibrer l’arbre. " - que je n’ai aucun mal à saisir.

Mais (oui, il y a toujours un mais).

Voici où cela prête à confusion, le texte indique "Si le facteur d'équilibre de R est 1, cela signifie que l'insertion a eu lieu à la droite (externe) de ce noeud et qu'une rotation à gauche est nécessaire" . Mais si je comprends bien, le texte dit (comme je l’ai déjà cité) que si le facteur d’équilibre était dans les limites de [-1, 1] alors il n'y avait pas besoin d'équilibrer?

Je pense que je suis si près de saisir le concept, j'ai réduit la rotation des arbres, mis en place un arbre de recherche binaire normal et sur le point de saisir les arbres AVL, mais il me semble qu'il me manque cette épiphanie essentielle.

Edit: Les exemples de code sont préférés aux formules classiques, car j'ai toujours eu plus de facilité à saisir quelque chose dans le code, mais toute aide est grandement appréciée.

Edit: J'aimerais pouvoir marquer toutes les réponses comme "acceptées", mais pour moi, la réponse de Nck était la première qui me poussait à "aha".

60
thr

Partie 1 - hauteur

Comme le dit starblue, la hauteur est simplement récursive. En pseudo-code:

height(node) = max(height(node.L), height(node.R)) + 1

Maintenant, la hauteur peut être définie de deux manières. Cela peut être le nombre de nœuds dans le chemin de la racine à ce nœud, ou le nombre de liens. Selon le page que vous avez référencée , la définition la plus courante est le nombre de liens. Dans ce cas, le pseudo-code complet serait:

height(node): 
   if node == null:
        return -1
   else:
        return max(height(node.L), height(node.R)) + 1

Si vous vouliez le nombre de nœuds, le code serait:

height(node): 
   if node == null:
        return 0
   else:
        return max(height(node.L), height(node.R)) + 1

De toute façon, l'algorithme de rééquilibrage devrait, à mon avis, fonctionner de la même manière.

Cependant, votre arbre sera beaucoup plus efficace ( O (ln (n)) ) si vous stockez et mettez à jour les informations de hauteur dans l’arbre, plutôt que de calculer à chaque fois. ( O (n) )

Partie 2 - équilibrage

Quand il dit "Si le facteur d'équilibre de R est 1", il est question du facteur d'équilibre de la branche droite, alors que le facteur d'équilibre en haut est de 2. Il vous dit comment choisir entre une rotation ou une rotation unique. une double rotation. En pseudo-code (ressemblant à python):

if balance factor(top) = 2: // right is imbalanced
     if balance factor(R) = 1: // 
          do a left rotation
     else if balance factor(R) = -1:
          do a double rotation
else: // must be -2, left is imbalanced
     if balance factor(L) = 1: // 
          do a left rotation
     else if balance factor(L) = -1:
          do a double rotation

J'espère que cela a du sens

78
Nick Fortescue
  • La hauteur est facilement implémentée par la récursivité, prenez le maximum de la hauteur des sous-arbres plus un.

  • Le "facteur d'équilibre de R" se réfère au sous-arbre droit de l'arbre qui est déséquilibré, je suppose.

4
starblue

Vous n'avez pas besoin de calculer la profondeur des arbres à la volée.

Vous pouvez les maintenir pendant que vous effectuez des opérations.

De plus, vous n’avez pas réellement besoin de vous tenir au courant des profondeurs; vous pouvez simplement suivre la différence entre les profondeurs des arbres gauche et droit.

http://www.eternallyconfuzzled.com/tuts/datastructures/jsw_tut_avl.aspx

Il est plus facile de suivre le facteur d’équilibre (différence entre les sous-arbres de gauche et de droite) à partir d’un POV de programmation, sauf que le tri du facteur d’équilibre après une rotation est un PITA ...

4
user82238

Voici une autre façon de trouver la hauteur. Ajoutez un attribut supplémentaire à votre nœud appelé height:

class Node
{
data value; //data is a custom data type
node right;
node left;
int height;
}

Maintenant, nous allons faire une simple traversée largeur-premier de l’arbre et continuer à mettre à jour la valeur de hauteur pour chaque nœud:

int height (Node root)
{
Queue<Node> q = Queue<Node>();
Node lastnode;
//reset height
root.height = 0;

q.Enqueue(root);
while(q.Count > 0)
{
   lastnode = q.Dequeue();
   if (lastnode.left != null){
      lastnode.left.height = lastnode.height + 1; 
      q.Enqueue(lastnode.left);
   }

   if (lastnode.right != null){
      lastnode.right.height = lastnode.height + 1;
      q.Enqueue(lastnode.right);
   }
}
return lastnode.height; //this will return a 0-based height, so just a root has a height of 0
}

À votre santé,

2
L8Again

Eh bien, vous pouvez calculer la hauteur d’un arbre avec la fonction récursive suivante:

int height(struct tree *t) {
    if (t == NULL)
        return 0;
    else
        return max(height(t->left), height(t->right)) + 1;
}

avec une définition appropriée de max() et struct tree. Vous devriez prendre le temps de comprendre pourquoi cela correspond à la définition basée sur la longueur du chemin que vous citez. Cette fonction utilise zéro comme hauteur de l’arbre vide.

Cependant, pour quelque chose comme un arbre AVL, je ne pense pas que vous calculiez réellement la hauteur à chaque fois que vous en avez besoin. Au lieu de cela, chaque nœud de l’arbre est complété par un champ supplémentaire qui mémorise la hauteur du sous-arbre ayant sa racine sur ce nœud. Ce champ doit être tenu à jour au fur et à mesure que l'arborescence est modifiée par des insertions et des suppressions.

Je suppose que, si vous calculez la hauteur à chaque fois au lieu de la mettre en cache dans l'arborescence, comme suggéré ci-dessus, la forme de l'arborescence AVL sera correcte, mais n'aura pas les performances logarithmiques attendues.

1
Dale Hagglund

Voici où cela prête à confusion, le texte indique "Si le facteur d'équilibre de R est 1, cela signifie que l'insertion a eu lieu à la droite (externe) de ce noeud et qu'une rotation à gauche est nécessaire". Mais si je comprends bien, le texte dit (comme je l’ai déjà cité) que si le facteur d’équilibre était compris entre [-1, 1], il n’était pas nécessaire de procéder à un équilibrage?

R est l'enfant de droite du nœud actuel N.

Si balance(N) = +2, vous avez besoin d'une rotation. Mais quelle rotation utiliser? Cela dépend bien de balance(R): si balance(R) = +1, vous avez besoin d’une rotation à gauche sur N; mais si balance(R) = -1, vous aurez besoin d'une double rotation.

1
yfeldblum

Voici où cela prête à confusion, le texte indique "Si le facteur d'équilibre de R est 1, cela signifie que l'insertion a eu lieu à la droite (externe) de ce noeud et qu'une rotation à gauche est nécessaire". Mais si je comprends bien, le texte dit (comme je l’ai déjà mentionné) que si le facteur d’équilibre était compris entre [-1, 1], il n’était pas nécessaire d’équilibrer la balance.

Ok, le temps de l'épiphanie.

Considérez ce que fait une rotation. Pensons à une rotation à gauche.

 P = parent
 O = ourself (the element we're rotating)
 RC = right child
 LC = left child (of the right child, not of ourself)

 P
  \
   O
    \
     RC
    /
   LC

  P
   \
    RC
   /
  O
   \
    LC

 10
   \
    15
      \
       20
      /
    18

 10
   \
    20
   /
 15
   \
    18 

 basically, what happens is;

 1. our right child moves into our position
 2. we become the left child of our right child
 3. our right child's left child becomes our right

Maintenant, le gros problème que vous devez remarquer ici - cette rotation à gauche N'A PAS CHANGÉ LA PROFONDEUR DE L'ARBRE. Nous ne sommes pas plus équilibrés pour l'avoir fait.

Mais - et voici la magie d'AVL - si nous faisions pivoter le bon enfant vers le bon EN PREMIER, voici ce que nous aurions ...

 P
  \
   O
    \
     LC
      \
       RC

Et MAINTENANT, si nous faisons tourner O à gauche, nous obtenons ceci ...

 P
  \
   LC
  /  \
 O    RC

La magie! nous avons réussi à supprimer un niveau de l’arbre - nous avons équilibré l’arbre.

Équilibrer l’arbre, c’est se débarrasser de la profondeur excessive et tasser les niveaux supérieurs de manière plus complète - c’est exactement ce que nous venons de faire.

Tout ce qui concerne les rotations simples/doubles est simplement que votre sous-arbre doit ressembler à ceci;

 P
  \
   O
    \
     LC
      \
       RC

avant de faire une rotation - et vous devrez peut-être faire une rotation à droite pour entrer dans cet état. Mais si vous êtes déjà dans cet état, il vous suffit de faire la rotation à gauche.

1
user82238

Cette solution de type BFS est assez simple. Saute simplement les niveaux un par un.

def getHeight(self,root, method='links'):
    c_node = root
    cur_lvl_nodes = [root]
    nxt_lvl_nodes = []
    height = {'links': -1, 'nodes': 0}[method]

    while(cur_lvl_nodes or nxt_lvl_nodes):
        for c_node in cur_lvl_nodes:
            for n_node in filter(lambda x: x is not None, [c_node.left, c_node.right]):
                nxt_lvl_nodes.append(n_node)

        cur_lvl_nodes = nxt_lvl_nodes
        nxt_lvl_nodes = []
        height += 1

    return height
0
realmaniek

Donner BinaryTree<T, Comparator>::Node _ un membre de données subtreeHeight, initialisé à 0 dans son constructeur et mis à jour automatiquement à chaque fois avec:

template <typename T, typename Comparator>
inline void BinaryTree<T, Comparator>::Node::setLeft (std::shared_ptr<Node>& node) {
    const std::size_t formerLeftSubtreeSize = left ? left->subtreeSize : 0;
    left = node;
    if (node) {
        node->parent = this->shared_from_this();
        subtreeSize++;
        node->depthFromRoot = depthFromRoot + 1;
        const std::size_t h = node->subtreeHeight;
        if (right)
            subtreeHeight = std::max (right->subtreeHeight, h) + 1;
        else
            subtreeHeight = h + 1;
    }
    else {
        subtreeSize -= formerLeftSubtreeSize;
        subtreeHeight = right ? right->subtreeHeight + 1 : 0;
    }
}

template <typename T, typename Comparator>
inline void BinaryTree<T, Comparator>::Node::setRight (std::shared_ptr<Node>& node) {
    const std::size_t formerRightSubtreeSize = right ? right->subtreeSize : 0;
    right = node;
    if (node) {
        node->parent = this->shared_from_this();
        subtreeSize++;
        node->depthFromRoot = depthFromRoot + 1;
        const std::size_t h = node->subtreeHeight;
        if (left)
            subtreeHeight = std::max (left->subtreeHeight, h) + 1;
        else
            subtreeHeight = h + 1;
    }
    else {
        subtreeSize -= formerRightSubtreeSize;
        subtreeHeight = left ? left->subtreeHeight + 1 : 0;
    }
}

Notez que les membres de données subtreeSize et depthFromRoot sont également mis à jour. Ces fonctions sont appelées lors de l’insertion d’un nœud (tous testés), par ex.

template <typename T, typename Comparator>
inline std::shared_ptr<typename BinaryTree<T, Comparator>::Node>
BinaryTree<T, Comparator>::Node::insert (BinaryTree& tree, const T& t, std::shared_ptr<Node>& node) {
    if (!node) {
        std::shared_ptr<Node> newNode = std::make_shared<Node>(tree, t);
        node = newNode;
        return newNode;
    }
    if (getComparator()(t, node->value)) {
        std::shared_ptr<Node> newLeft = insert(tree, t, node->left);
        node->setLeft(newLeft);
    }
    else {
        std::shared_ptr<Node> newRight = insert(tree, t, node->right);
        node->setRight(newRight);
    }
    return node;
}

Si vous supprimez un nœud, utilisez une version différente de removeLeft et removeRight en remplaçant subtreeSize++; avec subtreeSize--;. Les algorithmes pour rotateLeft et rotateRight peuvent être adaptés sans trop de problèmes. Ce qui suit a été testé et passé:

template <typename T, typename Comparator>
void BinaryTree<T, Comparator>::rotateLeft (std::shared_ptr<Node>& node) {  // The root of the rotation is 'node', and its right child is the pivot of the rotation.  The pivot will rotate counter-clockwise and become the new parent of 'node'.
    std::shared_ptr<Node> pivot = node->right;
    pivot->subtreeSize = node->subtreeSize;
    pivot->depthFromRoot--;
    node->subtreeSize--;  // Since 'pivot' will no longer be in the subtree rooted at 'node'.
    const std::size_t a = pivot->left ? pivot->left->subtreeHeight + 1 : 0;  // Need to establish node->heightOfSubtree before pivot->heightOfSubtree is established, since pivot->heightOfSubtree depends on it.
    node->subtreeHeight = node->left ? std::max(a, node->left->subtreeHeight + 1) : std::max<std::size_t>(a,1);
    if (pivot->right) {
        node->subtreeSize -= pivot->right->subtreeSize;  // The subtree rooted at 'node' loses the subtree rooted at pivot->right.
        pivot->subtreeHeight = std::max (pivot->right->subtreeHeight, node->subtreeHeight) + 1;
    }
    else
        pivot->subtreeHeight = node->subtreeHeight + 1;
    node->depthFromRoot++;
    decreaseDepthFromRoot(pivot->right);  // Recursive call for the entire subtree rooted at pivot->right.
    increaseDepthFromRoot(node->left);  // Recursive call for the entire subtree rooted at node->left.
    pivot->parent = node->parent;
    if (pivot->parent) {  // pivot's new parent will be its former grandparent, which is not nullptr, so the grandparent must be updated with a new left or right child (depending on whether 'node' was its left or right child).
        if (pivot->parent->left == node)
            pivot->parent->left = pivot;
        else
            pivot->parent->right = pivot;
    }
    node->setRightSimple(pivot->left);  // Since pivot->left->value is less than pivot->value but greater than node->value.  We use the NoSizeAdjustment version because the 'subtreeSize' values of 'node' and 'pivot' are correct already.
    pivot->setLeftSimple(node);
    if (node == root) {
        root = pivot;
        root->parent = nullptr; 
    }
}

inline void decreaseDepthFromRoot (std::shared_ptr<Node>& node) {adjustDepthFromRoot(node, -1);}
inline void increaseDepthFromRoot (std::shared_ptr<Node>& node) {adjustDepthFromRoot(node, 1);}

template <typename T, typename Comparator>
inline void BinaryTree<T, Comparator>::adjustDepthFromRoot (std::shared_ptr<Node>& node, int adjustment) {
    if (!node)
        return;
    node->depthFromRoot += adjustment;
    adjustDepthFromRoot (node->left, adjustment);
    adjustDepthFromRoot (node->right, adjustment);
}

Voici le code complet: http://ideone.com/d6arrv

0
prestokeys