cours1 min de lecture
Chercher dans un tableau trié en divisant l'espace de recherche par deux à chaque étape — coût $\mathcal{O}(\log n)$.
programme

Introduction

La recherche linéaire du cours 1 examine toutes les cases d'un tableau au pire. Pour 1 million d'éléments, c'est un million d'opérations. Si le tableau est trié, on peut faire spectaculairement mieux : c'est l'objet de la recherche dichotomique. L'idée est simple — comparer la cible à l'élément du milieu, et selon le résultat, jeter la moitié inutile. À chaque étape on divise par deux l'espace de recherche.

L'intuition « plus chaud, plus froid »

Vous connaissez le jeu : « j'ai un nombre entre 1 et 100, devine-le, je te dis plus haut ou plus bas ». La stratégie optimale est de toujours proposer la moitié de l'intervalle restant.

  • Au départ : 1 à 100. Vous proposez 50.
  • Réponse « plus haut » : 51 à 100. Vous proposez 75.
  • Réponse « plus bas » : 51 à 74. Vous proposez 62.

En 7 questions au maximum, vous trouvez. Pourquoi 7 ? Parce que 27=1281002^7 = 128 \geq 100. Sept divisions par deux suffisent à isoler un nombre parmi 100. C'est exactement l'idée de la dichotomie.

Analogie de l'annuaire papier : pour trouver « Dupont », vous n'ouvrez pas le livre page 1. Vous l'ouvrez au milieu, regardez la lettre, et vous allez à gauche ou à droite. Puis encore au milieu de la moitié restante. En quelques étapes, vous y êtes.

L'algorithme

Prérequis : le tableau t doit être trié dans l'ordre croissant. Sinon l'algorithme ne fonctionne pas.

On maintient deux indices gauche et droite qui délimitent la zone où la cible pourrait encore se trouver. Tant que cette zone n'est pas vide :

  1. on calcule l'indice du milieu, m = (gauche + droite) // 2 ;
  2. si t[m] == cible, on a trouvé, on retourne m ;
  3. si t[m] < cible, la cible est forcément à droite : on pose gauche = m + 1 ;
  4. sinon, la cible est forcément à gauche : on pose droite = m - 1.

Si la zone devient vide (gauche > droite), la cible n'est pas dans le tableau.

def dichotomie(t, cible):
    """Retourne l'indice de cible dans t (trié), ou -1 si absente."""
    gauche, droite = 0, len(t) - 1
    while gauche <= droite:
        m = (gauche + droite) // 2
        if t[m] == cible:
            return m
        elif t[m] < cible:
            gauche = m + 1
        else:
            droite = m - 1
    return -1

trie = [1, 4, 7, 12, 18, 25, 33, 47, 58, 66, 80]
print(dichotomie(trie, 25))   # 5
print(dichotomie(trie, 10))   # -1
⏵ Ctrl+↵ pour exécuter
Aucune exécution pour l'instant.

Le variant de boucle

Pour la dichotomie, le bon outil de raisonnement est le variant : une quantité qui décroît strictement à chaque tour et qui ne peut pas descendre indéfiniment.

Posons v=v = droite - gauche. C'est la taille de la zone de recherche moins 1. À chaque tour :

  • soit on trouve et on s'arrête (le while se termine par return),
  • soit on fait gauche = m + 1, ce qui supprime au moins la moitié de l'intervalle ;
  • soit on fait droite = m - 1, idem.

Dans tous les cas, vv diminue strictement. Comme vv est un entier, il finira par devenir négatif (v<0v < 0, donc gauche > droite), ce qui arrête la boucle. L'algorithme termine donc toujours.

Variant vs invariant :
  • Un invariant est une propriété constante vraie d'un tour à l'autre (utile à la correction).
  • Un variant est une grandeur qui décroît (utile à la terminaison).

Pourquoi O(logn)\mathcal{O}(\log n) ?

À chaque tour, la taille de la zone de recherche est divisée par deux. En partant d'une zone de taille nn :

  • après 1 tour : taille n/2n / 2 ;
  • après 2 tours : taille n/4n / 4 ;
  • après kk tours : taille n/2kn / 2^k.

On s'arrête quand cette taille atteint 0 ou 1, c'est-à-dire quand 2kn2^k \geq n. Le plus petit kk qui convient est k=log2nk = \lceil \log_2 n \rceil. La complexité est donc logarithmique en nn :

O(logn)\mathcal{O}(\log n)
Pour vous donner une idée concrète : pour n=109n = 10^9 (un milliard !), la recherche dichotomique trouve en 30 étapes au maximum. La recherche linéaire en exigerait un milliard.

Pour un tableau trié de 1 048 576 éléments, combien d'étapes au pire pour la dichotomie ?

Pièges courants

  • Oublier que le tableau doit être trié : la dichotomie ne marche pas sur un tableau quelconque.
  • m = (gauche + droite) / 2 au lieu de // 2 : la division / retourne un flottant en Python, qui ne peut pas servir d'indice.
  • Bornes mal mises à jour : gauche = m au lieu de gauche = m + 1 cause des boucles infinies quand gauche == droite. La case m vient d'être testée — il faut l'exclure.
  • Tableau vide : len(t) - 1 = -1, donc gauche = 0 > droite = -1, donc on n'entre pas dans la boucle, et on retourne -1. Cas géré sans cas particulier.
Si le tableau n'est pas trié, ne convoquez pas la dichotomie. Soit vous triez d'abord (mais alors le coût total est dominé par le tri), soit vous restez sur une recherche linéaire.

Le prérequis indispensable pour appliquer la dichotomie est :

Pour aller plus loin

La dichotomie est l'archétype de la stratégie diviser pour régner, que vous reverrez en Terminale (tri fusion, tri rapide). À chaque étape, on réduit le problème à un sous-problème deux fois plus petit, jusqu'à un cas de base. Cette stratégie produit naturellement des complexités en O(logn)\mathcal{O}(\log n) ou O(nlogn)\mathcal{O}(n \log n).