les Skip Lists
Introduction
Les skip lists (ou listes d'intervalles) sont des structures de données probabilistes utilisées pour stocker un ensemble ordonné de nœuds, permettant des opérations efficaces de recherche, d'insertion et de suppression. Inventées par William Pugh en 1989, les skip lists sont une alternative aux arbres équilibrés comme les arbres AVL ou les arbres rouges-noirs, offrant des performances similaires avec une implémentation souvent plus simple.
Notions Fondamentales
1. Définition
Une skip list est composée de plusieurs niveaux de listes chaînées, où chaque niveau est une sous-liste de l'ensemble des éléments. Les niveaux supérieurs contiennent des liens qui "sautent" plusieurs éléments, ce qui permet de rechercher rapidement un élément en réduisant le nombre d'étapes nécessaires pour traverser la liste.
2. Propriétés
- Complexité : Les opérations de recherche, d'insertion et de suppression ont une complexité moyenne de .
- Probabiliste : Utilise des niveaux aléatoires pour équilibrer la liste, ce qui la rend plus simple à implémenter que les arbres équilibrés.
- Efficacité : Comparable aux arbres équilibrés, mais souvent plus facile à mettre en œuvre.
Structure d'une Skip List
Une skip list est composée de plusieurs niveaux, chaque niveau étant une liste chaînée contenant des sous-ensembles d'éléments. Le niveau inférieur (niveau 0) contient tous les éléments, tandis que les niveaux supérieurs contiennent des liens qui permettent de "sauter" plusieurs éléments.
Schéma :
Niveau 3: [ ] -> [ ] -> [ ]
Niveau 2: [ ] -> [ ] -> [ ] -> [ ]
Niveau 1: [ ] -> [ ] -> [ ] -> [ ] -> [ ]
Niveau 0: [ ] -> [ ] -> [ ] -> [ ] -> [ ] -> [ ]
Fonctionnement
3. Recherche d'un Élement
Pour rechercher un élément dans une skip list :
- On commence par le niveau supérieur.
- On avance dans la liste jusqu'à ce que l'élément suivant soit plus grand que l'élément recherché ou que l'on atteigne la fin de la liste.
- Si l'élément suivant est plus grand ou si on atteint la fin, on descend d'un niveau et on continue la recherche.
- On répète jusqu'à atteindre le niveau inférieur.
4. Insertion d'un Élement
Pour insérer un élément :
- On effectue une recherche pour déterminer l'emplacement de l'élément.
- On insère l'élément dans le niveau inférieur.
- Avec une certaine probabilité, on insère également l'élément dans les niveaux supérieurs.
5. Suppression d'un Élement
Pour supprimer un élément :
- On effectue une recherche pour trouver l'élément.
- On supprime l'élément de tous les niveaux où il apparaît.
Exemple de Code en JavaScript
Code :
// Classe représentant un nœud de la skip list
class Node {
constructor(value, level) {
this.value = value; // Valeur du nœud
this.forward = new Array(level + 1).fill(null); // Tableau de pointeurs vers les nœuds suivants pour chaque niveau
}
}
// Classe représentant la skip list
class SkipList {
constructor(maxLevel, p) {
this.maxLevel = maxLevel; // Niveau maximum de la skip list
this.p = p; // Probabilité utilisée pour déterminer les niveaux aléatoires
this.header = new Node(null, maxLevel); // Nœud tête avec un niveau maximum
this.level = 0; // Niveau actuel de la skip list
}
// Génère un niveau aléatoire pour le nouvel élément
randomLevel() {
let level = 0;
while (Math.random() < this.p && level < this.maxLevel) {
level++; // Incrémente le niveau tant que la condition est remplie
}
return level; // Retourne le niveau généré
}
// Recherche un élément dans la skip list
search(value) {
let current = this.header; // Commence à partir du nœud tête
for (let i = this.level; i >= 0; i--) {
// Descend à travers les niveaux
while (current.forward[i] !== null && current.forward[i].value < value) {
current = current.forward[i]; // Avance au nœud suivant au niveau actuel
}
}
current = current.forward[0]; // Descend au niveau 0
return current !== null && current.value === value ? current : null; // Retourne le nœud si trouvé
}
// Insertion d'un nouvel élément dans la skip list
insert(value) {
let update = new Array(this.maxLevel + 1).fill(null); // Tableau pour suivre les nœuds à mettre à jour
let current = this.header; // Commence à partir du nœud tête
for (let i = this.level; i >= 0; i--) {
// Descend à travers les niveaux
while (current.forward[i] !== null && current.forward[i].value < value) {
current = current.forward[i]; // Avance au nœud suivant au niveau actuel
}
update[i] = current; // Enregistre le nœud à mettre à jour
}
current = current.forward[0]; // Descend au niveau 0
if (current === null || current.value !== value) {
// Si l'élément n'existe pas déjà
let newLevel = this.randomLevel(); // Génère un niveau aléatoire pour le nouvel élément
if (newLevel > this.level) {
// Met à jour les nœuds tête si le nouveau niveau est plus grand
for (let i = this.level + 1; i <= newLevel; i++) {
update[i] = this.header;
}
this.level = newLevel; // Met à jour le niveau actuel de la skip list
}
let newNode = new Node(value, newLevel); // Crée un nouveau nœud
for (let i = 0; i <= newLevel; i++) {
newNode.forward[i] = update[i].forward[i]; // Pointe le nouveau nœud vers les nœuds suivants
update[i].forward[i] = newNode; // Met à jour les nœuds précédents pour pointer vers le nouveau nœud
}
}
}
// Suppression d'un élément de la skip list
delete(value) {
let update = new Array(this.maxLevel + 1).fill(null); // Tableau pour suivre les nœuds à mettre à jour
let current = this.header; // Commence à partir du nœud tête
for (let i = this.level; i >= 0; i--) {
// Descend à travers les niveaux
while (current.forward[i] !== null && current.forward[i].value < value) {
current = current.forward[i]; // Avance au nœud suivant au niveau actuel
}
update[i] = current; // Enregistre le nœud à mettre à jour
}
current = current.forward[0]; // Descend au niveau 0
if (current !== null && current.value === value) {
// Si l'élément est trouvé
for (let i = 0; i <= this.level; i++) {
if (update[i].forward[i] !== current) break; // Arrête si le lien est rompu
update[i].forward[i] = current.forward[i]; // Met à jour les nœuds pour sauter le nœud supprimé
}
while (this.level > 0 && this.header.forward[this.level] === null) {
this.level--; // Ajuste le niveau actuel si nécessaire
}
}
}
}
// Création d'une skip list avec un niveau maximum de 3 et une probabilité de 0.5
const skipList = new SkipList(3, 0.5);
// Insertion d'éléments
skipList.insert(1);
skipList.insert(3);
skipList.insert(7);
skipList.insert(8);
skipList.insert(9);
skipList.insert(12);
skipList.insert(19);
// Recherche d'éléments
console.log(skipList.search(3) !== null); // Output: true
console.log(skipList.search(10) !== null); // Output: false
// Suppression d'un élément
skipList.delete(3);
console.log(skipList.search(3) !== null); // Output: false
Explication du Code
-
Classe Node :
- Représente un nœud dans la skip list.
- Contient une valeur et un tableau de pointeurs (
forward) vers les nœuds suivants à différents niveaux.
-
Classe SkipList :
- Initialise la skip list avec un niveau maximum (
maxLevel) et une probabilité (p). - Contient un nœud tête (
header) et le niveau actuel de la liste (level).
- Initialise la skip list avec un niveau maximum (
-
Méthode
randomLevel:- Génère un niveau aléatoire pour un nouvel élément basé sur la probabilité
p.
- Génère un niveau aléatoire pour un nouvel élément basé sur la probabilité
-
Méthode
search:- Recherche un élément dans la skip list en traversant les niveaux de haut en bas.
-
Méthode
insert:- Insère un nouvel élément dans la skip list en mettant à jour les pointeurs (
forward) des nœuds affectés.
- Insère un nouvel élément dans la skip list en mettant à jour les pointeurs (
-
Méthode
delete:- Supprime un élément de la skip list en ajustant les pointeurs (
forward) des nœuds affectés.
- Supprime un élément de la skip list en ajustant les pointeurs (
-
Exemple d'utilisation :
- Création d'une skip list avec un niveau maximum de 3 et une probabilité de 0.5.
- Insertion, recherche et suppression d'éléments dans la skip list.
Ce code montre comment les skip lists permettent des opérations efficaces de recherche, d'insertion et de suppression avec une complexité moyenne de . Les commentaires ligne par ligne facilitent la compréhension du fonctionnement interne de la skip list.
Vulgarisation avec des Exemples Amusants
Histoire Amusante : L'Ascenseur Magique
Histoire :
Imagine que tu es le gardien d'un immeuble avec un ascenseur magique. Cet ascenseur a plusieurs niveaux de boutons, et chaque niveau te permet de sauter plusieurs étages à la fois. Pour retrouver un ami, tu commences par le niveau supérieur (celui avec les plus grands sauts) et tu descends jusqu'au niveau de base (celui où tu montes un étage à la fois).
Explication Pas à Pas :
-
Recherche :
- Tu commences par le bouton le plus haut de l'ascenseur.
- Tu avances étage par étage jusqu'à ce que tu dépasses l'étage où ton ami pourrait se trouver.
- Tu descends d'un niveau et recommences jusqu'à ce que tu trouves ton ami.
-
Insertion :
- Tu utilises l'ascenseur pour trouver l'emplacement où ton ami doit être.
- Tu l'ajoutes à cet étage.
- Avec une certaine probabilité, tu ajoutes également ton ami à des niveaux plus élevés pour qu'il puisse être trouvé plus rapidement la prochaine fois.
-
Suppression :
- Tu utilises l'ascenseur pour trouver l'ami que tu veux enlever.
- Tu le retires de tous les niveaux où il apparaît.
Conclusion
Les skip lists offrent une structure de données élégante et efficace pour les opérations de recherche, d'insertion et de suppression, avec des performances comparables à celles des arbres équilibrés, mais avec une implémentation plus simple. Grâce à leur nature probabiliste et leur capacité à gérer des niveaux de liens, les skip lists permettent une navigation rapide et une gestion flexible des ensembles ordonnés.