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.

  1. Parcourir les clés du nœud jusqu'à trouver la première qui soit supérieure ou égale à la clé cherchée.
  2. Si cette clé est exactement celle qu'on cherche, c'est fini : on renvoie le nœud et la position.
  3. Sinon, si le nœud est une feuille, la clé n'existe pas dans l'arbre.
  4. 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

B-arbre de degré minimal 3 utilisé pour l'exemple de recherche, avec la racine G M P X.

Cherchons la clé S.

NiveauNœudComparaisonsDécision
0G M P XS > G, S > M, S > P, S < X i = 3, descendre dans le 4e fils
1R S T U VS > 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ûtPourquoi
Accès disqueO(h) = O(logt n) un seul par niveau traversé
Temps processeurO(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

Exercice 1
Dans le B-arbre de degré 3 du chapitre (racine G M P X ; feuilles A C D E, J K, N O, R S T U V, Y Z), déroulez la recherche de la clé D : nœuds visités, comparaisons, accès disque.
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és2
Comparaisons1 + 3 = 4
Accès disque1 (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.

Exercice 2
Pourquoi remplacer la boucle linéaire par une recherche dichotomique n'améliore-t-il pas la complexité en accès disque ?
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.

Exercice 3
Un index B-arbre de degré t = 128 contient 50 millions de clés. Une requête demande toutes les clés comprises entre deux bornes, et 400 clés y répondent. Estimez le nombre d'accès disque.
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

Aucune question. Soyez le premier à poser une question.
★ ★ ★ ★ ★
2 votes. Moyenne 5 sur 5.

Ajouter un commentaire

Anti-spam