- Accueil
- TUTORIELS
- LES B-ARBRES
- Rechercher une clé
CHAPITRE 5 : RECHERCHER UNE CLÉ
La recherche est l'opération la plus simple des trois, et celle qui justifie toute la structure. Elle généralise la descente dans un arbre binaire : au lieu de choisir entre deux directions, on en choisit une parmi n + 1.
Le principe
À chaque nœud, on cherche dans quel intervalle tombe la clé recherchée, puis on descend dans le fils correspondant.
- Parcourir les clés du nœud jusqu'à trouver la première qui soit supérieure ou égale à la clé cherchée.
- Si cette clé est exactement celle qu'on cherche, c'est fini : on renvoie le nœud et la position.
- Sinon, si le nœud est une feuille, la clé n'existe pas dans l'arbre.
- Sinon, descendre dans le fils correspondant et recommencer.
L'algorithme
fonction RECHERCHE-BARBRE(x, k)
// x a deja ete charge depuis le disque
i <- 1
tant que i <= x.n et k > x.cle[i] faire
i <- i + 1
fin tant que
si i <= x.n et k = x.cle[i] alors
retourner (x, i) // trouvee
sinon si x.feuille alors
retourner NIL // absente de l'arbre
sinon
LIRE-DISQUE(x.c[i]) // le seul cout reel
retourner RECHERCHE-BARBRE(x.c[i], k)
fin si
fin fonction
Remarquer la ligne LIRE-DISQUE : c'est la seule opération
coûteuse de tout l'algorithme, et elle n'apparaît qu'une fois par niveau.
En Python
def rechercher(noeud, k):
# Renvoie (noeud, position) si k est presente, sinon None
i = 0
while i < len(noeud.cles) and k > noeud.cles[i]:
i += 1
if i < len(noeud.cles) and k == noeud.cles[i]:
return (noeud, i)
if noeud.feuille:
return None
return rechercher(noeud.fils[i], k) # LIRE-DISQUE implicite
Pourquoi i sert aussi d'indice de fils. À la
sortie de la boucle, i est le nombre de clés strictement
inférieures à k. C'est exactement l'indice du sous-arbre où
k doit se trouver, puisque fils[i] contient les clés
comprises entre cles[i-1] et cles[i]. La même variable
sert donc aux deux usages, sans conversion.
Dérouler un exemple
Cherchons la clé S.
| Niveau | Nœud | Comparaisons | Décision |
|---|---|---|---|
| 0 | G M P X | S > G, S > M, S > P, S < X | i = 3, descendre dans le 4e fils |
| 1 | R S T U V | S > R, puis S = S | trouvée en position 2 |
Deux nœuds visités, donc un seul accès disque : la racine étant déjà en mémoire. Sept comparaisons de clés, qui ne coûtent rien.
Cherchons maintenant Q, qui n'existe pas : on descend
de la même façon jusqu'à la feuille R S T U V, où la boucle
s'arrête sur R sans égalité. Le nœud étant une feuille, on renvoie NIL.
Les deux complexités
Il faut distinguer soigneusement deux coûts que l'on confond souvent.
| Coût | Pourquoi | |
|---|---|---|
| Accès disque | O(h) = O(logt n) | un seul par niveau traversé |
| Temps processeur | O(t · logt n) | la boucle parcourt jusqu'à 2t − 1 clés par niveau |
C'est le coût en accès disque qui compte. Le coût processeur est t fois plus grand, mais chaque opération y est un million de fois moins chère.
Chiffrons sur un milliard de clés avec t = 128 :
acces disque : 5 x 10 ms = 50 ms
comparaisons : 5 x 255 = 1275 operations
1275 x 10 ns = 0,0127 ms
Le processeur represente 0,025 % du temps total.
Une optimisation naturelle
La boucle qui cherche l'intervalle est linéaire : elle examine les clés une par une. Or elles sont triées. On peut donc la remplacer par une recherche dichotomique :
import bisect
def rechercher(noeud, k):
i = bisect.bisect_left(noeud.cles, k) # O(log t) au lieu de O(t)
if i < len(noeud.cles) and noeud.cles[i] == k:
return (noeud, i)
if noeud.feuille:
return None
return rechercher(noeud.fils[i], k)
Le temps processeur tombe de O(t · logt n) à O(log t · logt n), c'est-à-dire O(log₂ n).
Est-ce que ça vaut le coup ? En accès disque, rien ne change, et c'est le seul coût qui compte vraiment. Le gain est donc invisible sur un index sur disque.
Il devient réel dans deux situations : quand l'index tient entièrement en mémoire vive, et quand t est très grand (plusieurs milliers de clés par nœud). C'est pourquoi les vraies implémentations font la dichotomie : elle ne coûte rien à écrire, et elle sert dans les cas où elle sert.
La recherche d'intervalle
Au-delà de la recherche d'une clé précise, on demande souvent « toutes les clés entre a et b ». On descend jusqu'à a, puis on parcourt l'arbre dans l'ordre croissant jusqu'à dépasser b.
def parcours_trie(noeud, sortie):
# Parcours infixe : sort les cles dans l'ordre croissant
for i, cle in enumerate(noeud.cles):
if not noeud.feuille:
parcours_trie(noeud.fils[i], sortie)
sortie.append(cle)
if not noeud.feuille:
parcours_trie(noeud.fils[-1], sortie) # le dernier fils
Ce parcours visite chaque nœud une fois, donc O(n) en accès disque pour tout l'arbre. Sur un B-arbre ordinaire, un parcours d'intervalle oblige à remonter et redescendre sans cesse : c'est maladroit. Le chapitre 8 montrera comment les B+ arbres règlent ce problème en chaînant les feuilles entre elles.
À retenir
- À chaque nœud, on choisit un intervalle parmi n + 1 et on descend.
- Un seul accès disque par niveau : O(logt n) au total.
- Le temps processeur, O(t · logt n), est négligeable devant le disque.
- La dichotomie dans le nœud n'améliore pas les accès disque, seulement le processeur.
Exercices d'application
Voir le corrigeMasquer le corrige
Niveau 0 : racine G M P X. On compare D à G : D < G, la boucle s'arrête immédiatement avec i = 0. Il n'y a pas égalité, et la racine n'est pas une feuille : on descend dans le fils d'indice 0.
Niveau 1 : feuille A C D E. On compare D à A (D > A, on avance), puis à C (D > C, on avance), puis à D : égalité. On renvoie (nœud, 2).
| Compte | |
|---|---|
| Nœuds visités | 2 |
| Comparaisons | 1 + 3 = 4 |
| Accès disque | 1 (la racine est en mémoire) |
La clé D est trouvée en un seul aller-retour avec le disque, bien que l'arbre contienne 18 clés.
Voir le corrigeMasquer le corrige
Parce que les deux recherches travaillent à l'intérieur d'un nœud déjà chargé en mémoire.
Le nœud a été amené du disque par un unique LIRE-DISQUE, qui transfère la page entière. Qu'on examine ensuite ses 255 clés une par une ou seulement 8 par dichotomie, aucune lecture supplémentaire n'est déclenchée.
Le nombre d'accès disque est fixé par le nombre de niveaux traversés, c'est-à-dire par la hauteur : il reste O(logt n) dans les deux cas.
C'est une illustration directe du chapitre 1 : optimiser ce qui se passe en mémoire ne sert à rien tant que le goulot d'étranglement est le disque. Il faut d'abord savoir ce qu'on mesure.
Voir le corrigeMasquer le corrige
Descente jusqu'à la première clé. La hauteur vaut au plus log₁₂₈(25 000 000) ≈ 3,5, donc 4 niveaux. Avec la racine en mémoire, cela fait 3 accès.
Parcours des 400 clés. Chaque feuille contient entre 127 et 255 clés. Il faut donc traverser environ 400 / 190 ≈ 2 à 4 feuilles.
Sur un B-arbre ordinaire, passer d'une feuille à la suivante oblige à remonter chez le père puis à redescendre : comptons généreusement 2 accès par transition, soit 6 de plus.
environ 3 + 4 + 6 ≈ 13 accès disque
Avec un B+ arbre, dont les feuilles sont chaînées entre elles, le parcours ne coûte qu'un accès par feuille : on tombe à 3 + 4 = 7. C'est précisément pour ce type de requête que les bases de données utilisent des B+ arbres plutôt que des B-arbres.
Cherchez avant d'ouvrir le corrige : c'est en butant sur les difficultes que l'on progresse.
Questions / Réponses
Ajouter un commentaire