Parcours d'arbre binaire : Analyse complète récursive et itérative

Introduction aux parcours d'arbre binaire

Les trois types de parcours d'arbre binaire constituent des opérations fondamentales en informatique. Chaque parcours peut être implémenté de deux manières : récursivement (plus lisible) ou itérativement (avec une pile, évitant les risques de débordement de pile). Ce guide présente des implémentations complètes avec des explications détaillées.

Définition d'un nœud d'arbre binaire

Créons d'abord une classe générique pour représenter un nœud :

class NoeudArbre {
    int valeur;
    NoeudArbre gauche;
    NoeudArbre droit;
    
    NoeudArbre() {}
    
    NoeudArbre(int val) {
        this.valeur = val;
    }
    
    NoeudArbre(int val, NoeudArbre g, NoeudArbre d) {
        this.valeur = val;
        this.gauche = g;
        this.droit = d;
    }
}

Parcours préordonné (racine → gauche → droit)

Principe : visiter d'abord le nœud courant, puis traiter récursivement le sous-arbre gauche, et enfin le sous-arbre droit.

Approche récursive

class Solution {
    public List<Integer> parcoursPreorde(NoeudArbre racine) {
        List<Integer> resultat = new ArrayList<>();
        traiterPreorde(racine, resultat);
        return resultat;
    }

    private void traiterPreorde(NoeudArbre actuel, List<Integer> resultat) {
        if (actuel == null) return;
        resultat.add(actuel.valeur);           // 1. Visiter la racine
        traiterPreorde(actuel.gauche, resultat);  // 2. Sous-arbre gauche
        traiterPreorde(actuel.droit, resultat);   // 3. Sous-arbre droit
    }
}

Approche itérative avec pile

En utilisant le principe LIFO de la pile, on empile d'abord le sous-arbre droit, puis le gauche, pour obtenir l'ordre « racine → gauche → droit » :

class Solution {
    public List<Integer> parcoursPreorde(NoeudArbre racine) {
        List<Integer> resultat = new ArrayList<>();
        if (racine == null) return resultat;
        
        Deque<NoeudArbre> pile = new LinkedList<>();
        pile.push(racine);
        
        while (!pile.isEmpty()) {
            NoeudArbre actuel = pile.pop();
            resultat.add(actuel.valeur);         // Visiter la racine
            
            // Empiler droit en premier (traitement différé), puis gauche
            if (actuel.droit != null) pile.push(actuel.droit);
            if (actuel.gauche != null) pile.push(actuel.gauche);
        }
        return resultat;
    }
}

Parcours infixé (gauche → racine → droit)

Principe : traiter d'abord le sous-arbre gauche, visiter la racine, puis traiter le sous-arbre droit.

Approche récursive

class Solution {
    public List<Integer> parcoursInfixe(NoeudArbre racine) {
        List<Integer> resultat = new ArrayList<>();
        traiterInfixe(racine, resultat);
        return resultat;
    }

    private void traiterInfixe(NoeudArbre actuel, List<Integer> resultat) {
        if (actuel == null) return;
        traiterInfixe(actuel.gauche, resultat);   // 1. Sous-arbre gauche
        resultat.add(actuel.valeur);               // 2. Visiter la racine
        traiterInfixe(actuel.droit, resultat);    // 3. Sous-arbre droit
    }
}

Approche itérative avec pile

On descend d'abord jusqu'au nœud le plus à gauche en empilant les nœuds rencontrés, puis on remontre en visitant les racines et en taritant les sous-arbres droits :

class Solution {
    public List<Integer> parcoursInfixe(NoeudArbre racine) {
        List<Integer> resultat = new ArrayList<>();
        Deque<NoeudArbre> pile = new LinkedList<>();
        NoeudArbre courant = racine;
        
        while (courant != null || !pile.isEmpty()) {
            // Descendre jusqu'au nœud extrême gauche
            while (courant != null) {
                pile.push(courant);
                courant = courant.gauche;
            }
            
            // Dépiler et visiter le nœud
            courant = pile.pop();
            resultat.add(courant.valeur);
            
            // Passer au sous-arbre droit
            courant = courant.droit;
        }
        return resultat;
    }
}

Parcours postordonné (gauche → droite → racine)

Principe : traiter d'abord le sous-arbre gauche, puis le droit, et enfin visiter la racine.

Approche récursive

class Solution {
    public List<Integer> parcoursPostorde(NoeudArbre racine) {
        List<Integer> resultat = new ArrayList<>();
        traiterPostorde(racine, resultat);
        return resultat;
    }

    private void traiterPostorde(NoeudArbre actuel, List<Integer> resultat) {
        if (actuel == null) return;
        traiterPostorde(actuel.gauche, resultat);   // 1. Sous-arbre gauche
        traiterPostorde(actuel.droit, resultat);    // 2. Sous-arbre droit
        resultat.add(actuel.valeur);                // 3. Visiter la racine
    }
}

Approche itérative - Méthode 1 : inversion du parcours préordonné

On modifie le parcours préordonné pour obtenir l'ordre « racine → droit → gauche », puis on inverse la séquence résultat :

class Solution {
    public List<Integer> parcoursPostorde(NoeudArbre racine) {
        List<Integer> resultat = new ArrayList<>();
        if (racine == null) return resultat;
        
        Deque<NoeudArbre> pile = new LinkedList<>();
        pile.push(racine);
        
        while (!pile.isEmpty()) {
            NoeudArbre actuel = pile.pop();
            resultat.add(actuel.valeur);           // Ordre : racine
            
            // Empiler gauche en premier, droit ensuite
            if (actuel.gauche != null) pile.push(actuel.gauche);
            if (actuel.droit != null) pile.push(actuel.droit);
        }
        
        Collections.reverse(resultat);            // Inverser : gauche → droit → racine
        return resultat;
    }
}

Approche itérative - Méthode 2 : technique de marquage

On utilise un marqueur null pour distinguer les nœuds déjà traités des nœuds à traiter :

class Solution {
    public List<Integer> parcoursPostorde(NoeudArbre racine) {
        List<Integer> resultat = new ArrayList<>();
        if (racine == null) return resultat;
        
        Deque<NoeudArbre> pile = new LinkedList<>();
        pile.push(racine);
        
        while (!pile.isEmpty()) {
            NoeudArbre actuel = pile.pop();
            if (actuel != null) {
                pile.push(actuel);                 // Empiler la racine
                pile.push(null);                   // Marqueur pour traitement ultérieur
                if (actuel.droit != null) pile.push(actuel.droit);
                if (actuel.gauche != null) pile.push(actuel.gauche);
            } else {
                resultat.add(pile.pop().valeur);   // Traitement de la racine
            }
        }
        return resultat;
    }
}

Tableau comparatif des parcours

Type de parcours Ordre Implémentation récursive Implémentation itérative
Préordonné Racine → Gauche → Droit Racine avant sous-arbres Pile : droit empilé en premier
Infixé Gauche → Racine → Droit Gauche, puis racine, puis droit aller jusqu'au nœud le plus à gauche, remonter et traiter
Postordonné Gauche → Droit → Racine Sous-arbres avant racine Inversion du préordonné / marquage

L'approche récursive offre une lisibilité supérieure, tandis que l'approche itérative nécessite une bonne maîtrise des opérations sur pile (particulièrement pour les parcours infixé et postordonné). En pratique, la récursion convient aux arbres de faible profondeur, tandis que l'itération est préférable pour les arbres profonds afin d'éviter les débordements de pile d'exécution.

Publié le 15 juillet à 00h58