web-dev-qa-db-fra.com

Créer une arborescence de recherche binaire équilibrée à partir de la liste chaînée triée

Quel est le meilleur moyen de créer un arbre de recherche binaire équilibré à partir d'une liste triée de liens simples?

20
Timothy Chen

Pourquoi ne pas créer des nœuds de bas en haut?

La complexité temporelle de cette solution est O (N). Explication détaillée dans mon article de blog:

http://www.leetcode.com/2010/11/convert-sorted-list-to-balanced-binary.html

Il suffit de parcourir deux fois la liste chaînée. Commencez par parcourir pour obtenir la longueur de la liste (qui est ensuite transmise comme paramètre n dans la fonction), puis créez des nœuds selon l'ordre de la liste.

BinaryTree* sortedListToBST(ListNode *& list, int start, int end) {
  if (start > end) return NULL;
  // same as (start+end)/2, avoids overflow
  int mid = start + (end - start) / 2;
  BinaryTree *leftChild = sortedListToBST(list, start, mid-1);
  BinaryTree *parent = new BinaryTree(list->data);
  parent->left = leftChild;
  list = list->next;
  parent->right = sortedListToBST(list, mid+1, end);
  return parent;
}

BinaryTree* sortedListToBST(ListNode *head, int n) {
  return sortedListToBST(head, 0, n-1);
}
25
1337c0d3r

Vous ne pouvez pas faire mieux que le temps linéaire, car vous devez au moins lire tous les éléments de la liste. Vous pouvez donc tout aussi bien copier la liste dans un tableau (temps linéaire), puis construire efficacement l'arbre de la manière habituelle, c'est-à-dire que si vous aviez la liste [9,12,18,23,24,51,84], vous commenceriez par en faire 23 la racine, avec les enfants 12 et 51, puis 9 et 18 deviennent enfants de 12 et 24 et 84 deviennent des enfants de 51. Globalement, devrait être O(n) si vous le faites correctement.

L'algorithme lui-même, à ce qu'il mérite, est "prend l'élément central de la liste comme racine et construit de manière récursive des BST pour les sous-listes situées à gauche et à droite de l'élément central et les attache sous la racine".

3
Stuart Golodetz

Ceci est une implémentation en python:

def sll_to_bbst(sll, start, end):
    """Build a balanced binary search tree from sorted linked list.

    This assumes that you have a class BinarySearchTree, with properties
    'l_child' and 'r_child'.

    Params:
        sll: sorted linked list, any data structure with 'popleft()' method,
            which removes and returns the leftmost element of the list. The
            easiest thing to do is to use 'collections.deque' for the sorted
            list.
        start: int, start index, on initial call set to 0
        end: int, on initial call should be set to len(sll)

    Returns:
        A balanced instance of BinarySearchTree

    This is a python implementation of solution found here: 
    http://leetcode.com/2010/11/convert-sorted-list-to-balanced-binary.html

    """

    if start >= end:
        return None

    middle = (start + end) // 2
    l_child = sll_to_bbst(sll, start, middle)
    root = BinarySearchTree(sll.popleft())
    root.l_child = l_child
    root.r_child = sll_to_bbst(sll, middle+1, end)

    return root
2
m.kocikowski

Le meilleur ne concerne pas seulement le temps d'exécution asynmptopique. La liste chaînée contient toutes les informations nécessaires pour créer l’arbre binaire directement, et je pense que c’est probablement ce qu’ils recherchent. 

Notez que les première et troisième entrées deviennent des enfants du deuxième, puis que le quatrième nœud a les enfants des deuxième et sixième (qui ont les cinquième et septième enfants) et ainsi de suite ...

en code pseudo

read three elements, make a node from them, mark as level 1, Push on stack
loop
    read three elemeents and make a node of them
    mark as level 1
    Push on stack
    loop while top two enties on stack have same level (n)
         make node of top two entries, mark as level n + 1, Push on stack
while elements remain in list

(avec un peu d'ajustement pour quand il reste moins de trois éléments ou un arbre déséquilibré à tout moment)

MODIFIER:

En tout point, il y a un nœud gauche de hauteur N sur la pile. L'étape suivante consiste à lire un élément, puis à lire et à construire un autre nœud de hauteur N sur la pile. Pour construire un nœud de hauteur N, établissez et poussez un nœud de hauteur N -1 sur la pile, puis lisez un élément, puis créez un autre nœud de hauteur N-1 sur la pile, ce qui est un appel récursif.

En fait, cela signifie que l'algorithme (même modifié) ne produira pas d'arborescence équilibrée. S'il y a 2N + 1 nœuds, il produira un arbre avec 2N-1 valeurs à gauche et 1 à droite. 

Donc, je pense que la réponse de @ sgolodetz est meilleure, à moins que je ne puisse penser à un moyen de rééquilibrer l'arbre tel qu'il est construit.

2

Question piège!

Le meilleur moyen consiste à utiliser la STL et à tirer parti du fait que le conteneur associatif trié ADT, dont le jeu est une implémentation, exige l'insertion de plages triées avec un temps linéaire amorti. Tout ensemble de structures de données de base passables pour toutes les langues devrait offrir une garantie similaire. Pour une réponse concrète, reportez-vous aux solutions plutôt intelligentes proposées par d’autres. 


Qu'est-ce que c'est? Je devrais offrir quelque chose d'utile?

Hum ...

Que dis-tu de ça?

Le plus petit arbre significatif possible dans un arbre binaire équilibré est constitué de 3 nœuds. Un parent et deux enfants. La toute première instance d'un tel arbre est constituée des trois premiers éléments. Enfant-parent-enfant. Imaginons maintenant cela comme un seul noeud. D'accord, eh bien, nous n'avons plus d'arbre. Mais nous savons que la forme que nous voulons est Child-Parent-Child.
Fait pendant un moment avec notre imagination, nous voulons garder un pointeur sur le parent dans ce triumvirat initial. Mais c'est singulièrement lié!
Nous voudrons avoir quatre pointeurs, que j'appellerai A, B, C et D. Donc, nous passons de A à 1, fixons B à A et le faisons avancer d'un. Placez C égal à B et avancez-le de deux. Le nœud sous B pointe déjà vers son futur enfant. Nous construisons notre arbre initial. Nous laissons B au parent de Tree one. C est assis au nœud qui aura nos deux arbres minimaux comme enfants. Définissez A égal à C et avancez-le. Définissez D égal à A et avancez-le d'un. Nous pouvons maintenant construire notre prochain arbre minimal. D pointe vers la racine de cet arbre, B pointe vers la racine de l'autre et C pointe vers ... la nouvelle racine à partir de laquelle nous accrocherons nos deux arbres minimaux.

Que diriez-vous de quelques photos?

[A][B][-][C]  

Avec notre image d'un arbre minimal en tant que nœud ... 

[B = Tree][C][A][D][-]

Et alors

[Tree A][C][Tree B]

Sauf que nous avons un problème. Le nœud deux après D est notre prochaine racine. 

[B = Tree A][C][A][D][-][Roooooot?!]  

Ce serait beaucoup plus facile pour nous si nous pouvions simplement maintenir un pointeur sur lui plutôt que sur lui et C. Il s'avère que, puisque nous savons qu'il pointera sur C, nous pouvons continuer et commencer à construire le nœud dans l'arbre binaire qui le tiendra, et dans le cadre de cela, nous pouvons entrer C en tant que nœud gauche. Comment pouvons-nous faire cela avec élégance?

Définissez le pointeur du nœud sous C sur le nœud sous B.
C'est tricher dans tous les sens de la Parole, mais en utilisant cette astuce, nous libérons B.
Alternativement, vous pouvez être sain d’esprit et commencer à construire la structure du nœud. Après tout, vous ne pouvez vraiment pas réutiliser les nœuds de la SLL, ce sont probablement des structures POD.

Alors maintenant...

[TreeA]<-[C][A][D][-][B]  
[TreeA]<-[C]->[TreeB][B] 

Et ... Attends une seconde. Nous pouvons utiliser cette même astuce pour libérer C, si nous nous contentons de le considérer comme un seul nœud plutôt que comme un arbre. Car après tout, il ne s'agit en réalité que d'un seul nœud. 

[TreeC]<-[B][A][D][-][C]  

Nous pouvons encore généraliser nos astuces. 

[TreeC]<-[B][TreeD]<-[C][-]<-[D][-][A]    
[TreeC]<-[B][TreeD]<-[C]->[TreeE][A]  
[TreeC]<-[B]->[TreeF][A]  
[TreeG]<-[A][B][C][-][D]
[TreeG]<-[A][-]<-[C][-][D]  
[TreeG]<-[A][TreeH]<-[D][B][C][-]  
[TreeG]<-[A][TreeH]<-[D][-]<-[C][-][B]  
[TreeG]<-[A][TreeJ]<-[B][-]<-[C][-][D]  
[TreeG]<-[A][TreeJ]<-[B][TreeK]<-[D][-]<-[C][-]      
[TreeG]<-[A][TreeJ]<-[B][TreeK]<-[D][-]<-[C][-]  

Il nous manque une étape critique! 

[TreeG]<-[A]->([TreeJ]<-[B]->([TreeK]<-[D][-]<-[C][-]))  

Devient :

[TreeG]<-[A]->[TreeL->([TreeK]<-[D][-]<-[C][-])][B]    
[TreeG]<-[A]->[TreeL->([TreeK]<-[D]->[TreeM])][B]  
[TreeG]<-[A]->[TreeL->[TreeN]][B]  
[TreeG]<-[A]->[TreeO][B]  
[TreeP]<-[B]  

Bien évidemment, l’algorithme peut être considérablement nettoyé, mais j’ai pensé qu’il serait intéressant de montrer comment on peut optimiser au fur et à mesure en concevant votre algorithme de façon itérative. Je pense que ce genre de processus est ce qu'un bon employeur devrait rechercher plus que tout.

Le truc, en gros, est que chaque fois que nous atteignons le prochain point milieu, dont nous savons qu’il est un futur parent, nous savons que son sous-arbre de gauche est déjà terminé. L'autre astuce est que nous en avons fini avec un nœud une fois qu'il a deux enfants et quelque chose qui le pointe, même si tous les sous-arbres ne sont pas terminés. En utilisant cela, nous pouvons obtenir ce dont je suis à peu près certain qu'il s'agit d'une solution temporelle linéaire, chaque élément n'étant touché que 4 fois au maximum. Le problème est que cela nécessite de recevoir une liste qui constituera un arbre de recherche binaire vraiment équilibré. En d’autres termes, certaines contraintes cachées peuvent rendre cette solution beaucoup plus difficile à appliquer, voire impossible. Par exemple, si vous avez un nombre impair d'éléments ou s'il y a beaucoup de valeurs non uniques, cela commence à produire un arbre assez stupide. 

Considérations:

  • Rendre l'élément unique. 
  • Insérez un élément factice à la fin si le nombre de nœuds est impair. 
  • Chantez avec envie pour une mise en œuvre plus naïve.
  • Utilisez un deque pour conserver les racines des sous-arbres terminés et les points centraux, au lieu de perdre votre temps avec mon deuxième tour.
2
Jake Kurzer

Au lieu de la liste chaînée triée, on m'a demandé sur un tableau trié (peu importe logiquement, mais le temps d'exécution varie) pour créer un fichier BST de hauteur minimale. Voici le code que je pourrais obtenir:

typedef struct Node{
     struct Node *left;
     int info;
     struct Node  *right;
}Node_t;

Node_t* Bin(int low, int high) {

     Node_t* node = NULL;
     int mid = 0;

     if(low <= high) {
         mid = (low+high)/2;
         node = CreateNode(a[mid]);
         printf("DEBUG: creating node for %d\n", a[mid]);

        if(node->left == NULL) {
            node->left = Bin(low, mid-1);
        }

        if(node->right == NULL) {
            node->right = Bin(mid+1, high);
        }

        return node;
    }//if(low <=high)
    else {
        return NULL;
    }
}//Bin(low,high)


Node_t* CreateNode(int info) {

    Node_t* node = malloc(sizeof(Node_t));
    memset(node, 0, sizeof(Node_t));
    node->info = info;
    node->left = NULL;
    node->right = NULL;

    return node;

}//CreateNode(info)

// call function for an array example: 6 7 8 9 10 11 12, it gets you desired 
// result

 Bin(0,6); 

HTH Quelqu'un ..

1
Cleonjoys

Une implémentation légèrement améliorée de @ 1337c0d3r dans mon blog .

// create a balanced BST using @len elements starting from @head & move @head forward by @len
TreeNode *sortedListToBSTHelper(ListNode *&head, int len) {
    if (0 == len)   return NULL;

    auto left = sortedListToBSTHelper(head, len / 2);
    auto root = new TreeNode(head->val);
    root->left = left;
    head = head->next;
    root->right = sortedListToBSTHelper(head, (len - 1) / 2);
    return root;
}

TreeNode *sortedListToBST(ListNode *head) {
    int n = length(head);
    return sortedListToBSTHelper(head, n);
}
0
sinoTrinity

C'est l'algorithme pseudo-récursif que je vais suggérer. 


createTree(treenode *root, linknode *start, linknode *end)
{
   if(start == end or start = end->next)
   {
      return; 
   } 
   ptrsingle=start;
   ptrdouble=start;
   while(ptrdouble != end and ptrdouble->next !=end)
   {
    ptrsignle=ptrsingle->next;
    ptrdouble=ptrdouble->next->next;
   }
   //ptrsignle will now be at the middle element. 
   treenode cur_node=Allocatememory;
   cur_node->data = ptrsingle->data;
   if(root = null)
       {
           root = cur_node; 
       }
   else
      {
         if(cur_node->data (less than) root->data)
          root->left=cur_node
         else
           root->right=cur_node
      }
   createTree(cur_node, start, ptrSingle);
   createTree(cur_node, ptrSingle, End); 
}

Root = null; L'appel initial sera createtree (Root, list, null);

Nous faisons la construction récursive de l'arbre, mais sans utiliser le tableau intermédiaire. Pour atteindre l'élément du milieu à chaque fois que nous faisons avancer deux pointeurs, un par un, l'autre par deux. Au moment où le deuxième pointeur est à la fin, le premier pointeur sera au milieu. 

Le temps d'exécution sera o (nlogn). L'espace supplémentaire sera o (logn). Pas une solution efficace pour une situation réelle où vous pouvez avoir un arbre R-B qui garantit l’insertion de nlogn. Mais assez bon pour une interview. 

0
Manoj R

J'espère que l'explication détaillée de ce message vous aidera: http://preparefortechinterview.blogspot.com/2013/10/planting-trees_1.html

0
Furquan

Comme pour @Stuart Golodetz et @Jake Kurzer, l'important est que la liste soit déjà triée. 

Dans la réponse de @ Stuart, le tableau qu'il a présenté est la structure de données de sauvegarde de la BST. L'opération de recherche, par exemple, aurait simplement besoin d'effectuer des calculs de tableau d'index pour parcourir l'arborescence. Cultiver le tableau et supprimer des éléments serait la partie la plus délicate, alors je préférerais un vecteur ou une autre structure de données à recherche de temps constant.

La réponse de @ Jake utilise également ce fait mais vous oblige malheureusement à parcourir la liste pour trouver chaque fois une opération get (index). Mais ne nécessite aucune utilisation de mémoire supplémentaire.

À moins que l'enquêteur ait expressément indiqué qu'il souhaitait une représentation de l'arbre sous forme de structure d'objet, j'utiliserais la réponse de @ Stuart.

Dans une question comme celle-ci, on vous donnerait des points supplémentaires pour discuter des compromis et de toutes les options que vous avez.

0
StevenWilkins

Si vous savez combien de nœuds sont dans la liste liée, vous pouvez le faire comme ceci:

// Gives path to subtree being built.  If branch[N] is false, branch
// less from the node at depth N, if true branch greater.
bool branch[max depth];

// If rem[N] is true, then for the current subtree at depth N, it's
// greater subtree has one more node than it's less subtree.
bool rem[max depth];

// Depth of root node of current subtree.
unsigned depth = 0;

// Number of nodes in current subtree.
unsigned num_sub = Number of nodes in linked list;

// The algorithm relies on a stack of nodes whose less subtree has
// been built, but whose right subtree has not yet been built.  The
// stack is implemented as linked list.  The nodes are linked
// together by having the "greater" handle of a node set to the
// next node in the list.  "less_parent" is the handle of the first
// node in the list.
Node *less_parent = nullptr;

// h is root of current subtree, child is one of its children.
Node *h, *child;

Node *p = head of the sorted linked list of nodes;

LOOP // loop unconditionally

    LOOP WHILE (num_sub > 2)
        // Subtract one for root of subtree.
        num_sub = num_sub - 1;

        rem[depth] = !!(num_sub & 1); // true if num_sub is an odd number
        branch[depth] = false;
        depth = depth + 1;
        num_sub = num_sub / 2;
    END LOOP

    IF (num_sub == 2)
        // Build a subtree with two nodes, slanting to greater.
        // I arbitrarily chose to always have the extra node in the
        // greater subtree when there is an odd number of nodes to
        // split between the two subtrees.

        h = p;
        p = the node after p in the linked list;
        child = p;
        p = the node after p in the linked list;
        make h and p into a two-element AVL tree;
    ELSE  // num_sub == 1

        // Build a subtree with one node.

        h = p;
        p = the next node in the linked list;
        make h into a leaf node;
    END IF

    LOOP WHILE (depth > 0)
        depth = depth - 1;
        IF (not branch[depth])
            // We've completed a less subtree, exit while loop.
            EXIT LOOP;
        END IF

        // We've completed a greater subtree, so attach it to
        // its parent (that is less than it).  We pop the parent
        // off the stack of less parents.
        child = h;
        h = less_parent;
        less_parent = h->greater_child;
        h->greater_child = child;
        num_sub = 2 * (num_sub - rem[depth]) + rem[depth] + 1;
        IF (num_sub & (num_sub - 1))
          // num_sub is not a power of 2
          h->balance_factor = 0;
        ELSE
          // num_sub is a power of 2
          h->balance_factor = 1;
        END IF
    END LOOP

    IF (num_sub == number of node in original linked list)
        // We've completed the full tree, exit outer unconditional loop
        EXIT LOOP;
    END IF

    // The subtree we've completed is the less subtree of the
    // next node in the sequence.

    child = h;
    h = p;
    p = the next node in the linked list;
    h->less_child = child;

    // Put h onto the stack of less parents.
    h->greater_child = less_parent;
    less_parent = h;

    // Proceed to creating greater than subtree of h.
    branch[depth] = true;
    num_sub = num_sub + rem[depth];
    depth = depth + 1;

END LOOP

// h now points to the root of the completed AVL tree.

Pour un codage de cela en C++, reportez-vous à la fonction membre de génération (actuellement, ligne 361) dans https://github.com/wkaras/C-plus-plus-intrusive-container-templates/blob/master/avl_tree. h . En fait, il s'agit d'un modèle utilisant un itérateur avant plutôt qu'une liste chaînée.

0
WaltK