Cours – Recherche dichotomique dans une liste triée
Table des matières
1 Présentation...1
2 Recherche séquentielle...1
2.1 Complexité...2
3 Algorithme de recherche dichotomique...3
3.1 Principe de fonctionnement...3
3.2 Algorithme...5
3.3 Une implémentation en python...5
3.4 Complexité...6
3.5 Terminaison...7
1 Présentation
Des volumes importants de données sont susceptibles d’être traitées par les ordinateurs. Des algorithmes efficaces sont alors nécessaires pour réaliser ces opérations comme, par exemple, la sélection et la
récupération des données. Les algorithmes de recherche entrent dans cette catégorie. Leur rôle est de déterminer si une donnée est présente et, le cas échéant, d’en indiquer sa position, pour effectuer des traitements annexes. La recherche d’une information dans un annuaire illustre cette idée. On cherche si telle personne est présente dans l’annuaire afin d’en déterminer l’adresse. Plus généralement, c’est l’un des mécanismes principaux des bases de données : à l’aide d’un identifiant, on souhaite retrouver les informations correspondantes.
Dans cette famille d’algorithmes, la recherche dichotomique permet de traiter efficacement des données représentées dans un tableau de façon ordonnée.
Nous allons voir que si la liste est ordonnées, la recherche par dichotomie est plus rapide que la recherche par balayage.
Exemple de valeurs
Ordre
Taille de la liste 0 1 2 4 8 16 32 64 128 n
Recherche
séquentielle 0 1 2 4 8 16 32 64 128 n
Recherche dichotomique
0 1 2 3 4 5 6 7 8 log (n)₂
On dit que :
• la recherche séquentielle est de l'ordre n ;
• la recherche dichotomique est de l'ordre de log₂ (n).
Remarque : log₂(n)=ln(n) ln(2)
2 Recherche séquentielle
Exemple d'implémentation def balayage(liste, val):
for i in range(len(liste)):
if liste[i] == val:
return i return -1
Si la valeur est présente dans la liste, la fonction renvoie un entier positif ou nul qui correspond à l'indice de la valeur.
Si la valeur n'est pas présente dans la liste, la fonction renvoie -1.
Un test de la fonction
liste1 = [5, 7, 12, 14, 23, 27, 35, 40, 41, 45]
assert balayage(liste1, 35) == 6 assert balayage(liste1, 25) == -1
2.1 Complexité
Soit une liste de taille n.
Dans le pire des cas, l'algorithme doit parcourir n valeurs dans la liste pour trouver ou pas un élément.
Pour étudier la complexité nous allons écrire la fonction en faisant apparaître des instructions simples (comparaisons et affections).
Sur la droite on note le nombre de fois que l'instruction est exécutée.
def balayage2(liste, val):
n = len(liste) # 1 : a
i = 0 # 1 : a
while i < n: # n : c if liste[i] == val: # n : c
return i # 1 : a, si val présent i = i + 1 # n : a
return -1 # 1 : a, si val non présent
On calcule le nombre d'instructions dans la cas le plus défavorable soit parcourir n éléments.
• Dans le cas où la valeur est présente : 1 + 1 +n + n + n + 1 = 3n + 3
• Dans le cas où la valeur n'est pas présente : 1 + 1 +n + n + n + 1 = 3n + 3 Pour n grand, on obtient l'égalité asymptotique suivante : 3n+3≃3n
On peut observer que le nombre d'instructions à exécuter est proportionnel à n.
On dit que la complexité est d'ordre n que l'on note : O(n).
3 Algorithme de recherche dichotomique
3.1 Principe de fonctionnement
L’idée centrale de cette approche repose sur l’idée de réduire de moitié l’espace de recherche à chaque étape : on regarde la valeur du milieu et si ce n’est pas celle recherchée, on sait qu’il faut continuer de chercher dans la première moitié ou dans la seconde.
Plus précisément, en tenant compte du caractère trié du tableau, il est possible d’améliorer l’efficacité d’une telle recherche de façon conséquente en procédant ainsi :
1. on détermine l’élément m au milieu du tableau ;
2. si c’est la valeur recherchée, on s’arrête avec un succès ; 3. sinon, deux cas sont possibles :
a) si m est plus grand que la valeur recherchée, comme la tableau est trié, cela signifie qu’il suffit de continuer à chercher dans la première moitié du tableau ;
b) sinon, il suffit de chercher dans la moitié droite.
4. on répète cela jusqu'à avoir trouvé la valeur recherchée, ou bien avoir réduit l’intervalle de recherche à un intervalle vide, ce qui signifie que la valeur recherchée n’est pas présente.
À chaque étape, on coupe l’intervalle de recherche en deux, et on en choisit une moitié. On dit que l’on procède par dichotomie, du grec dikha (en deux) et tomos (couper).
On peut trouver un exemple animé de l’exécution de cet algorithme à l’adresse : https ://professeurb.github.io/articles/dichoto/.
Illustration de l'algorithme.
Recherche de la valeur 35 dans la liste [5, 7, 12, 14, 23, 27, 35, 40, 41, 45] .
3.2 Algorithme
fonction dichotomie(liste, val):
a = 0
b = len(liste) – 1
TantQue a <= b Faire
m = (a + b) // 2 # Division euclidienne
Si liste[m] = val alors Renvoyer m Sinon
Si liste[m] > alors b = m - 1 Sinon
a = m + 1 FinSi
FinSi Renvoyer -1
3.3 Une implémentation en python
def dichotomie(liste, val):
a = 0
b = len(liste) - 1
while a <= b:
m = (a + b) // 2 # Division euclidienne
if liste[m] == val:
return m elif liste[m] > val:
b = m - 1 else:
a = m + 1
return -1
Un test de la fonction.
liste1 = [5, 7, 12, 14, 23, 27, 35, 40, 41, 45]
assert dichotomie(liste1, 35) == 6 assert dichotomie(liste1, 25) == -1
3.4 Complexité
Pour étudier la complexité, nous allons nous intéresser à la boucle while.
Au niveau de la boucle, combien doit-on effectuer d'itérations pour un tableau de taille n dans le cas le plus défavorable (l'entier x n'est pas dans le tableau liste1) ?
Sachant qu'à chaque itération de la boucle on divise le tableau en 2, cela revient donc à se demander combien de fois faut-il diviser le tableau en 2 pour obtenir, à la fin, un tableau comportant un seul entier ? Autrement dit, combien de fois faut-il diviser n par 2 pour obtenir 1 ?
Mathématiquement cela se traduit par n1 2
1 2
1 2...
⏟
a
=n
2a=1 soit l'équation : n
2a=1 avec a le nombre de fois qu'il faut diviser n par 2 pour obtenir 1.
Il faut donc trouver a !
A ce stade il est nécessaire d'introduire une nouvelle notion mathématique : le "logarithme base 2" noté log2 .
Par définition log2(2x)=x Nous avons donc : n
2a=1 => n=2a => log2(n)=log2(2a)=a , nous avons donc a=log2(n) Nous pouvons donc dire que la complexité en temps dans le pire des cas de l'algorithme de recherche dichotomique est O(log 2(n)) .
Afin de pouvoir comparer l'efficacité des différents algorithmes, voici une représentation graphique des fonctions y=x (en rouge), y=x2 (en bleu) et y=log2(x) (en vert).
3.5 Terminaison
La fonction dichotomie(liste, val) contient une boucle non bornée, une boucle while, et pour être sûr de toujours obtenir un résultat, il faut s’assurer que le programme termine, que l’on ne reste pas bloqué infiniment dans la boucle.
Pour prouver que c’est bien le cas, nous allons utiliser un variant de boucle.
Variant de boucle
Il s’agit d’une quantité entière qui :
• doit être positive ou nulle pour rester dans la boucle ;
• doit décroître strictement à chaque itération.
Si l’on arrive à trouver une telle quantité, il est évident que l’on a nécessairement sortir de la boucle au bout d’un nombre fini d’itérations, puisque un entier positif ne peut décroître infiniment.
Preuve de la terminaison
Pour le cas qui nous occupe, un variant est très facile à trouver : il s’agit de la largeur de la quantité : b - a Avec :
• a indice de gauche,
• b indice de droite.
La condition de boucle étant a <= b, cela correspond exactement à ce que notre variant soit positif ou nul : b - a>=0
Montrons maintenant que le variant décroît strictement lors de l’exécution du corps de la boucle.
On commence par définir m = (a + b) // 2.
En particulier, on a alors a <= m <= b.
Ensuite, trois cas sont possibles :
• si liste[m] == val est vrai, on sort directement de la boucle à l’aide d’un return.
La terminaison est assurée.
• si liste[m] > val est vrai, on modifie la valeur de droite. En appelant b2 = m - 1 cette nouvelle valeur, on a : b2 < m ≤ b on soustrait a à chaque membre
b2 - a < m - a ≤ b – a donc b2 - a < b - a
Ainsi, le variant b-a est strictement décroissant.
• sinon, on modifie gauche et on a de même : a2 = m + 1
a ≤ m < a2 on multiplie par (-1)
-a2 < - m ≤ - a on ajoute b à chaque membre b – a2 < b – m ≤ b - a donc
b – a2 < b - a
Ainsi, le variant b - a est strictement décroissant.
Nous avons donc :
• identifié un variant de boucle : b-a ;
• montré que le variant est positif ou nul : b-a ≥ 0 ;
• montré que le variant est strictement décroissant : b-a > b2 - a2 > b3 – a3 > … ;
• montré que l'on a nécessairement une sortie de la boucle au bout d’un nombre fini d’itérations, puisque un entier positif ne peut décroître infiniment.