Alexandre CANTIN - Blog personnel

30 avr. 2020

Parcours d’arbre : Depth First & Breadth First 🌳

Parcours d’arbre : Depth First & Breadth First

Le parcours d’arbre est un grand classique lors d’entretiens algorithmiques et bien que cet exercice peut sembler intimidant au premier abord, nous allons dĂ©couvrir qu’il y a, en rĂ©alitĂ©, plus de peur que de mal !

Ainsi, dans cet article, nous nous intéressons aux deux méthodes les plus courantes afin de parcourir un arbre que sont :

  • le parcours en profondeur (Depth First), oĂą le choix du prochain noeud se porte sur le premier enfant du noeud courant :

Parcours en profondeur

  • le parcours en largeur (Breadth First), oĂą le prochain noeud est celui adjacent au noeud courant, c’est-Ă -dire celui situĂ© au mĂŞme niveau de profondeur :

Parcours en largeur

Parcours en profondeur - Depth first

Au niveau algorithmique, l’utilisation de la rĂ©cursivitĂ© reste le choix le plus pertinent. Ainsi, nous utiliserons la rĂ©cursivitĂ© pour notre parcours mais nous y ajouterons toutefois une Ă©tape supplĂ©mentaire pour les besoins de l’article, en stockant les noeuds dans une structure de donnĂ©es.

En effet, dès l’entrĂ©e dans un noeud et après le traitement de sa valeur, nous stockerons ses enfants dans une structure de donnĂ©es (contenant une mĂ©thode saveNode), rĂ©cupĂ©rerons ensuite le prochain noeud (mĂ©thode getNextNode) et passerons Ă  la prochaine Ă©tape de notre parcours. En pseudo-code, nous obtenons ainsi :

    // Structure de données encore inconnue
    class Structure {
      saveNode(node) {}
      getNextNode() {}
    }

    // Fonction de traitement d'un noeud
    var nodeStructure = new Structure()
    function computeNode(node) {
      if(!node) return;

      computeNodeValue(node.value)

      if(node.left) nodeStructure.saveNode(node.left)
      if(node.right) nodeStructure.saveNode(node.right)

      computeNode(nodeStructure.getNextNode())
    }

    // Création de l'arbre et début du parcours
    var tree = new Tree(...)
    computeNode(tree.root)

Maintenant, si nous dĂ©roulons un exemple plus concret avec l’arbre prĂ©sentĂ© en dĂ©but d’article et le pseudo-code ci-dessus :

1ère étape :

  • Nous traitons la valeur du noeud : 1, nous sommes Ă  la racine
  • Nous ajoutons ses deux enfants dans la structure de donnĂ©es : [2,5]
  • Nous rĂ©cupĂ©rons le prochain Ă©lĂ©ment : [2,5] => 2, [5]

2ème étape :

  • Nous traitons la valeur du noeud : 2
  • Nous ajoutons ses deux enfants dans la structure de donnĂ©es : [3, 4, 5]
  • Nous rĂ©cupĂ©rons le prochain Ă©lĂ©ment : [3, 4, 5] => 3, [4,5]

3ème étape :

  • Nous traitons la valeur du noeud : 3
  • Pas d’enfant, la structure est inchangĂ©e : [4, 5]
  • Nous rĂ©cupĂ©rons le prochain Ă©lĂ©ment : [4, 5] => 4, [5]

4ème étape :

  • Nous traitons la valeur du noeud : 4
  • Pas d’enfant, la structure est inchangĂ©e : [5]
  • Nous rĂ©cupĂ©rons le prochain Ă©lĂ©ment : [5] => 5, []

5ème étape :

  • Nous traitons la valeur du noeud : 5
  • Nous ajoutons ses deux enfants dans la structure de donnĂ©es : [6,7]
  • Nous rĂ©cupĂ©rons le prochain Ă©lĂ©ment : [6,7] => 6, [7]

6ème étape :

  • Nous traitons la valeur du noeud : 6
  • Pas d’enfant, la structure est inchangĂ©e : [7]
  • Nous rĂ©cupĂ©rons le prochain Ă©lĂ©ment : [7] => 7, []

7ème étape :

  • Nous traitons la valeur du noeud : 7
  • Pas d’enfant, la structure est inchangĂ©e : []
  • Nous rĂ©cupĂ©rons le prochain Ă©lĂ©ment : [] => x, []

8ème étape :

  • Pas de valeur et la structure est vide : le parcours est terminĂ©

Après ce long dĂ©roulĂ©, nous constatons que l’ordre d’ajout dans la liste Ă  son importance : nous ne prenons que le noeud insĂ©rĂ© le plus rĂ©cemment. En algorithmique, ce concept est appelĂ© Last In First Out, abrĂ©gĂ© LIFO, et notre structure de donnĂ©es doit respecter cette règle pour devenir compatible avec le parcours en profondeur; comme par exemple une stack.

Un ajustement s’avère nĂ©anmoins nĂ©cessaire vis-Ă -vis du pseudo-algorithme prĂ©cĂ©dent : le noeud gauche y est insĂ©rĂ© avant le noeud de droite. On parcourt ainsi l’arbre par la droite (1 -5 - 7 - 6 - 2 - 4 - 3) et on doit ainsi permuter les deux lignes saveNode :

// Noeud droite avant le gauche
if (node.right) nodeStructure.saveNode(node.right);
if (node.left) nodeStructure.saveNode(node.left);

Nous en avons ainsi fini avec le parcours en profondeur. L’ajout d’une structure de donnĂ©es peut sembler au premier abord superflu mais, rassurez-vous, ce socle nous sera très utile pour le parcours en largeur.

Parcours en largeur - Breadth First

Pour le parcours en largeur, nous utiliserons une base identique à celle du parcours en profondeur, à savoir le pseudo-algorithme associé à une structure de données (encore inconnue à ce stade).

Si, comme précédent, nous effectuons un déroulé des différentes étapes du parcours, nous obtenons :

1ère étape :

  • Nous traitons la valeur du noeud : 1, nous sommes Ă  la racine
  • Nous ajoutons ses deux enfants dans la structure de donnĂ©es : [2,5]
  • Nous rĂ©cupĂ©rons le prochain Ă©lĂ©ment : [2,5] => 2, [5]

2ème étape :

  • Nous traitons la valeur du noeud : 2
  • Nous ajoutons ses deux enfants dans la structure de donnĂ©es : [3, 4, 5]
  • Nous rĂ©cupĂ©rons le prochain Ă©lĂ©ment : [3, 4, 5] => 5, [3, 4]

3ème étape :

  • Nous traitons la valeur du noeud : 5
  • Nous ajoutons ses deux enfants dans la structure de donnĂ©es : [3, 4, 6, 7]
  • Nous rĂ©cupĂ©rons le prochain Ă©lĂ©ment : [3, 4, 6, 7] => 3, [4, 6, 7]

4ème étape :

  • Nous traitons la valeur du noeud : 3
  • Pas d’enfant, la structure est inchangĂ©e : [4, 6, 7]
  • Nous rĂ©cupĂ©rons le prochain Ă©lĂ©ment : [4, 6, 7] => 4, [6, 7]

5ème étape :

  • Nous traitons la valeur du noeud : 4
  • Pas d’enfant, la structure est inchangĂ©e : [6,7]
  • Nous rĂ©cupĂ©rons le prochain Ă©lĂ©ment : [6,7] => 6, [7]

6ème étape :

  • Nous traitons la valeur du noeud : 6
  • Pas d’enfant, la structure est inchangĂ©e : [7]
  • Nous rĂ©cupĂ©rons le prochain Ă©lĂ©ment : [7] => 7, []

7ème étape :

  • Nous traitons la valeur du noeud : 7
  • Pas d’enfant, la structure est inchangĂ©e : []
  • Nous rĂ©cupĂ©rons le prochain Ă©lĂ©ment : [] => x, []

8ème étape :

  • Pas de valeur et la structure est vide : le parcours est terminĂ©

Ici, et contrairement au parcours en profondeur, ce n’est pas le dernier noeud insĂ©rĂ© qui nous intĂ©resse mais le plus ancien. Ce concept algorithmique, appelĂ© First In First Out et abrĂ©gĂ© FIFO, se doit donc d’ĂŞtre respectĂ© par notre structure de donnĂ©es et comme exemple, nous pouvons citer une queue, cette dernière Ă©tant similaire Ă  une file d’attente.

Conclusion

Plusieurs domaines de l’informatique exploitent la puissance des arbres : index de base de donnĂ©es, arbres de dĂ©cision, analyse de code source (Abstract syntax tree)… et cela explique ainsi leur prĂ©sence en entretien technique.

Toutefois comme nous venons de le voir, le parcours d’arbre en profondeur ou en largeur n’est pas si complexe et peut se rĂ©sumer Ă  une simple association avec une structure de donnĂ©es :

  • une structure respectant la logique LIFO pour un parcours en profondeur
  • et une structure respectant la logique FIFO pour un parcours en largeur

Deux concepts finalement faciles et rapides Ă  retenir et qui, j’espère, ne vous fera plus craindre de futurs entretiens avec des parcours d’arbres !