Français

Découvrez les Arbres Binaires de Recherche (ABR) et leur implémentation efficace en JavaScript. Ce guide aborde la structure, les opérations et des exemples.

Arbres Binaires de Recherche : Un Guide Complet d'Implémentation en JavaScript

Les Arbres Binaires de Recherche (ABR) sont une structure de données fondamentale en informatique, largement utilisée pour la recherche, le tri et la récupération efficaces de données. Leur structure hiérarchique permet une complexité temporelle logarithmique pour de nombreuses opérations, ce qui en fait un outil puissant pour gérer de grands ensembles de données. Ce guide fournit un aperçu complet des ABR et démontre leur implémentation en JavaScript, s'adressant aux développeurs du monde entier.

Comprendre les Arbres Binaires de Recherche

Qu'est-ce qu'un Arbre Binaire de Recherche ?

Un Arbre Binaire de Recherche est une structure de données arborescente où chaque nœud a au plus deux enfants, appelés l'enfant gauche et l'enfant droit. La propriété clé d'un ABR est que pour tout nœud donné :

Cette propriété garantit que les éléments d'un ABR sont toujours ordonnés, ce qui permet une recherche et une récupération efficaces.

Concepts Clés

Implémenter un Arbre Binaire de Recherche en JavaScript

Définir la classe Node

D'abord, nous définissons une classe `Node` pour représenter chaque nœud dans l'ABR. Chaque nœud contiendra une `key` pour stocker la donnée et des pointeurs `left` et `right` vers ses enfants.


class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

Définir la classe BinarySearchTree

Ensuite, nous définissons la classe `BinarySearchTree`. Cette classe contiendra le nœud racine et les méthodes pour insérer, rechercher, supprimer et parcourir l'arbre.


class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  // Les méthodes seront ajoutées ici
}

Insertion

La méthode `insert` ajoute un nouveau nœud avec la clé donnée à l'ABR. Le processus d'insertion maintient la propriété de l'ABR en plaçant le nouveau nœud à la position appropriée par rapport aux nœuds existants.


insert(key) {
  const newNode = new Node(key);

  if (this.root === null) {
    this.root = newNode;
  } else {
    this.insertNode(this.root, newNode);
  }
}

insertNode(node, newNode) {
  if (newNode.key < node.key) {
    if (node.left === null) {
      node.left = newNode;
    } else {
      this.insertNode(node.left, newNode);
    }
  } else {
    if (node.right === null) {
      node.right = newNode;
    } else {
      this.insertNode(node.right, newNode);
    }
  }
}

Exemple : Insertion de valeurs dans l'ABR


const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);

Recherche

La méthode `search` vérifie si un nœud avec la clé donnée existe dans l'ABR. Elle parcourt l'arbre, comparant la clé avec la clé du nœud actuel et se déplaçant vers le sous-arbre gauche ou droit en conséquence.


search(key) {
  return this.searchNode(this.root, key);
}

searchNode(node, key) {
  if (node === null) {
    return false;
  }

  if (key < node.key) {
    return this.searchNode(node.left, key);
  } else if (key > node.key) {
    return this.searchNode(node.right, key);
  } else {
    return true;
  }
}

Exemple : Recherche d'une valeur dans l'ABR


console.log(bst.search(9));  // Sortie : true
console.log(bst.search(2));  // Sortie : false

Suppression

La méthode `remove` supprime un nœud avec la clé donnée de l'ABR. C'est l'opération la plus complexe car elle doit maintenir la propriété de l'ABR tout en supprimant le nœud. Il y a trois cas à considérer :


remove(key) {
  this.root = this.removeNode(this.root, key);
}

removeNode(node, key) {
  if (node === null) {
    return null;
  }

  if (key < node.key) {
    node.left = this.removeNode(node.left, key);
    return node;
  } else if (key > node.key) {
    node.right = this.removeNode(node.right, key);
    return node;
  } else {
    // la clé est égale à la clé du nœud

    // cas 1 - le nœud est une feuille
    if (node.left === null && node.right === null) {
      node = null;
      return node;
    }

    // cas 2 - le nœud a un seul enfant
    if (node.left === null) {
      node = node.right;
      return node;
    } else if (node.right === null) {
      node = node.left;
      return node;
    }

    // cas 3 - le nœud a deux enfants
    const aux = this.findMinNode(node.right);
    node.key = aux.key;
    node.right = this.removeNode(node.right, aux.key);
    return node;
  }
}

findMinNode(node) {
  let current = node;
  while (current != null && current.left != null) {
    current = current.left;
  }
  return current;
}

Exemple : Suppression d'une valeur de l'ABR


bst.remove(7);
console.log(bst.search(7)); // Sortie : false

Parcours d'Arbre

Le parcours d'arbre consiste à visiter chaque nœud de l'arbre dans un ordre spécifique. Il existe plusieurs méthodes de parcours courantes :


inOrderTraverse(callback) {
  this.inOrderTraverseNode(this.root, callback);
}

inOrderTraverseNode(node, callback) {
  if (node !== null) {
    this.inOrderTraverseNode(node.left, callback);
    callback(node.key);
    this.inOrderTraverseNode(node.right, callback);
  }
}

preOrderTraverse(callback) {
  this.preOrderTraverseNode(this.root, callback);
}

preOrderTraverseNode(node, callback) {
  if (node !== null) {
    callback(node.key);
    this.preOrderTraverseNode(node.left, callback);
    this.preOrderTraverseNode(node.right, callback);
  }
}

postOrderTraverse(callback) {
  this.postOrderTraverseNode(this.root, callback);
}

postOrderTraverseNode(node, callback) {
  if (node !== null) {
    this.postOrderTraverseNode(node.left, callback);
    this.postOrderTraverseNode(node.right, callback);
    callback(node.key);
  }
}

Exemple : Parcours de l'ABR


const printNode = (value) => console.log(value);

bst.inOrderTraverse(printNode);   // Sortie : 3 5 8 9 10 11 12 13 14 15 18 20 25
bst.preOrderTraverse(printNode);  // Sortie : 11 5 3 9 8 10 15 13 12 14 20 18 25
bst.postOrderTraverse(printNode); // Sortie : 3 8 10 9 12 14 13 18 25 20 15 11

Valeurs Minimale et Maximale

Trouver les valeurs minimale et maximale dans un ABR est simple, grâce à sa nature ordonnée.


min() {
  return this.minNode(this.root);
}

minNode(node) {
  let current = node;
  while (current !== null && current.left !== null) {
    current = current.left;
  }
  return current;
}

max() {
  return this.maxNode(this.root);
}

maxNode(node) {
  let current = node;
  while (current !== null && current.right !== null) {
    current = current.right;
  }
  return current;
}

Exemple : Recherche des valeurs minimale et maximale


console.log(bst.min().key); // Sortie : 3
console.log(bst.max().key); // Sortie : 25

Applications Pratiques des Arbres Binaires de Recherche

Les Arbres Binaires de Recherche sont utilisés dans diverses applications, notamment :

Considérations sur les Performances

La performance d'un ABR dépend de sa structure. Dans le meilleur des cas, un ABR équilibré permet une complexité temporelle logarithmique pour les opérations d'insertion, de recherche et de suppression. Cependant, dans le pire des cas (par exemple, un arbre déséquilibré), la complexité temporelle peut se dégrader en temps linéaire.

Arbres Équilibrés vs Déséquilibrés

Un ABR équilibré est un arbre où la hauteur des sous-arbres gauche et droit de chaque nœud diffère d'au plus un. Les algorithmes d'auto-équilibrage, tels que les arbres AVL et les arbres Rouge-Noir, garantissent que l'arbre reste équilibré, offrant des performances constantes. Différentes régions peuvent nécessiter différents niveaux d'optimisation en fonction de la charge sur le serveur ; l'équilibrage aide à maintenir les performances lors d'une utilisation mondiale élevée.

Complexité Temporelle

Concepts Avancés sur les ABR

Arbres Auto-Équilibrés

Les arbres auto-équilibrés sont des ABR qui ajustent automatiquement leur structure pour maintenir l'équilibre. Cela garantit que la hauteur de l'arbre reste logarithmique, offrant des performances constantes pour toutes les opérations. Les arbres auto-équilibrés courants incluent les arbres AVL et les arbres Rouge-Noir.

Arbres AVL

Les arbres AVL maintiennent l'équilibre en s'assurant que la différence de hauteur entre les sous-arbres gauche et droit de n'importe quel nœud est d'au plus un. Lorsque cet équilibre est rompu, des rotations sont effectuées pour le restaurer.

Arbres Rouge-Noir

Les arbres Rouge-Noir utilisent des propriétés de couleur (rouge ou noir) pour maintenir l'équilibre. Ils sont plus complexes que les arbres AVL mais offrent de meilleures performances dans certains scénarios.

Exemple de Code JavaScript : Implémentation Complète d'un Arbre Binaire de Recherche


class Node {
  constructor(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

class BinarySearchTree {
  constructor() {
    this.root = null;
  }

  insert(key) {
    const newNode = new Node(key);

    if (this.root === null) {
      this.root = newNode;
    } else {
      this.insertNode(this.root, newNode);
    }
  }

  insertNode(node, newNode) {
    if (newNode.key < node.key) {
      if (node.left === null) {
        node.left = newNode;
      } else {
        this.insertNode(node.left, newNode);
      }
    } else {
      if (node.right === null) {
        node.right = newNode;
      } else {
        this.insertNode(node.right, newNode);
      }
    }
  }

  search(key) {
    return this.searchNode(this.root, key);
  }

  searchNode(node, key) {
    if (node === null) {
      return false;
    }

    if (key < node.key) {
      return this.searchNode(node.left, key);
    } else if (key > node.key) {
      return this.searchNode(node.right, key);
    } else {
      return true;
    }
  }

  remove(key) {
    this.root = this.removeNode(this.root, key);
  }

  removeNode(node, key) {
    if (node === null) {
      return null;
    }

    if (key < node.key) {
      node.left = this.removeNode(node.left, key);
      return node;
    } else if (key > node.key) {
      node.right = this.removeNode(node.right, key);
      return node;
    } else {
      // la clé est égale à la clé du nœud

      // cas 1 - le nœud est une feuille
      if (node.left === null && node.right === null) {
        node = null;
        return node;
      }

      // cas 2 - le nœud a un seul enfant
      if (node.left === null) {
        node = node.right;
        return node;
      } else if (node.right === null) {
        node = node.left;
        return node;
      }

      // cas 3 - le nœud a deux enfants
      const aux = this.findMinNode(node.right);
      node.key = aux.key;
      node.right = this.removeNode(node.right, aux.key);
      return node;
    }
  }

  findMinNode(node) {
    let current = node;
    while (current != null && current.left != null) {
      current = current.left;
    }
    return current;
  }

  min() {
    return this.minNode(this.root);
  }

  minNode(node) {
    let current = node;
    while (current !== null && current.left !== null) {
      current = current.left;
    }
    return current;
  }

  max() {
    return this.maxNode(this.root);
  }

  maxNode(node) {
    let current = node;
    while (current !== null && current.right !== null) {
      current = current.right;
    }
    return current;
  }

  inOrderTraverse(callback) {
    this.inOrderTraverseNode(this.root, callback);
  }

  inOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.inOrderTraverseNode(node.left, callback);
      callback(node.key);
      this.inOrderTraverseNode(node.right, callback);
    }
  }

  preOrderTraverse(callback) {
    this.preOrderTraverseNode(this.root, callback);
  }

  preOrderTraverseNode(node, callback) {
    if (node !== null) {
      callback(node.key);
      this.preOrderTraverseNode(node.left, callback);
      this.preOrderTraverseNode(node.right, callback);
    }
  }

  postOrderTraverse(callback) {
    this.postOrderTraverseNode(this.root, callback);
  }

  postOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.postOrderTraverseNode(node.left, callback);
      this.postOrderTraverseNode(node.right, callback);
      callback(node.key);
    }
  }
}

// Exemple d'utilisation
const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);

const printNode = (value) => console.log(value);

console.log("Parcours infixe :");
bst.inOrderTraverse(printNode);

console.log("Parcours préfixe :");
bst.preOrderTraverse(printNode);

console.log("Parcours postfixe :");
bst.postOrderTraverse(printNode);

console.log("Valeur minimale :", bst.min().key);
console.log("Valeur maximale :", bst.max().key);

console.log("Recherche de 9 :", bst.search(9));
console.log("Recherche de 2 :", bst.search(2));

bst.remove(7);
console.log("Recherche de 7 après suppression :", bst.search(7));

Conclusion

Les Arbres Binaires de Recherche sont une structure de données puissante et polyvalente avec de nombreuses applications. Ce guide a fourni un aperçu complet des ABR, couvrant leur structure, leurs opérations et leur implémentation en JavaScript. En comprenant les principes et les techniques abordés dans ce guide, les développeurs du monde entier peuvent utiliser efficacement les ABR pour résoudre un large éventail de problèmes en développement logiciel. De la gestion de bases de données mondiales à l'optimisation des algorithmes de recherche, la connaissance des ABR est un atout inestimable pour tout programmeur.

Alors que vous poursuivez votre parcours en informatique, l'exploration de concepts avancés comme les arbres auto-équilibrés et leurs diverses implémentations améliorera encore votre compréhension et vos capacités. Continuez à pratiquer et à expérimenter avec différents scénarios pour maîtriser l'art d'utiliser efficacement les Arbres Binaires de Recherche.