16 - TD Algorithmique

Exercice 1 : QCM – vérification des prérequis

Pour chaque question, une seule réponse est correcte.

1. Quelle est la complexité d’un parcours séquentiel d’une liste de $n$ éléments ?

  • A. $O(1)$
  • B. $O(\log n)$
  • C. $O(n)$
  • D. $O(n^2)$
Correction

Réponse : C. Un parcours séquentiel visite chaque élément une seule fois, soit $n$ opérations. La complexité est linéaire.

  • A est faux : $O(1)$ signifie un temps constant (indépendant de $n$), ce qui n’est possible que si on connaît directement la position.
  • B est faux : $O(\log n)$ est la complexité de la recherche dichotomique (dans un tableau trié).
  • D est faux : $O(n^2)$ correspond à des algorithmes avec deux boucles imbriquées (comme le tri par sélection).

2. Que signifie « prouver la terminaison » d’un algorithme ?

  • A. Vérifier qu’il donne le bon résultat
  • B. Montrer qu’il finit toujours en un nombre fini d’étapes
  • C. Calculer sa complexité
  • D. Écrire ses tests
Correction

Réponse : B. La terminaison garantit que l’algorithme ne boucle pas indéfiniment.

  • A est faux : c’est la correction (pas la terminaison).
  • C est faux : la complexité mesure le temps d’exécution, pas la terminaison.
  • D est faux : les tests vérifient empiriquement, mais ne prouvent rien.

3. Pour prouver la terminaison d’une boucle while, on utilise :

  • A. Un invariant de boucle
  • B. Un variant de boucle
  • C. Une assertion
  • D. Un test unitaire
Correction

Réponse : B. Un variant est une quantité entière positive qui décroît strictement à chaque itération. Quand elle atteint 0, la boucle s’arrête.

  • A est faux : l’invariant sert à prouver la correction, pas la terminaison.
  • C est faux : une assertion vérifie une condition à un instant donné, elle ne prouve pas la terminaison.
  • D est faux : un test unitaire vérifie un cas particulier.

4. La recherche dichotomique nécessite que le tableau soit :

  • A. De taille paire
  • B. Constitué d’entiers
  • C. Trié
  • D. Sans doublons
Correction

Réponse : C. La dichotomie compare l’élément cherché au milieu du tableau pour éliminer une moitié. Cela ne fonctionne que si le tableau est trié.

  • A est faux : la taille n’a pas d’importance.
  • B est faux : la dichotomie fonctionne avec tout type comparable (flottants, chaînes, etc.).
  • D est faux : les doublons ne gênent pas la recherche.

Exercice 2 : exemple travaillé – recherche séquentielle avec trace

Problème : chercher la valeur 7 dans la liste [3, 8, 1, 7, 5].

Algorithme :

def chercher(L, x):
    for i in range(len(L)):
        if L[i] == x:
            return True
    return False

Trace d’exécution :

ItérationiL[i]L[i] == 7 ?Action
103NonContinuer
218NonContinuer
321NonContinuer
437Ouireturn True

Terminaison : le variant est $n - i$ (avec $n = \text{len}(L)$). Il décroît de 1 à chaque itération et la boucle s’arrête quand $i = n$ (ou plus tôt si on trouve l’élément).

Correction : si $x$ est dans la liste, il sera trouvé puisqu’on visite tous les indices. Si $x$ n’y est pas, on sort de la boucle et on renvoie False.

Complexité : au pire (élément absent), on parcourt les $n$ éléments : complexité $O(n)$.


Exercice 3 : parcours séquentiel guidé

3.1 Recherche d’un élément

Compléter la fonction suivante, puis répondre aux questions :

def est_element(a, L):
    """Renvoie True si a est dans L, False sinon."""
    for ___ in ___:
        if ___ == ___:
            return ___
    return ___
  1. La boucle se termine-t-elle toujours ? Quel est le variant ?
  2. L’algorithme est-il correct ? Justifier.
  3. Quelle est la complexité au pire cas ?
Correction
def est_element(a, L):
    """Renvoie True si a est dans L, False sinon."""
    for element in L:
        if element == a:
            return True
    return False

assert est_element(3, [1, 2, 3]) == True
assert est_element(5, [1, 2, 3]) == False
assert est_element(1, []) == False
  1. Terminaison : la boucle for parcourt une liste de longueur finie, elle se termine toujours. Le variant est le nombre d’éléments restant à parcourir.
  2. Correction : si a est dans L, il sera comparé à un moment et la fonction renverra True. Si a n’est pas dans L, toutes les comparaisons échoueront et la fonction renverra False.
  3. Complexité : au pire (élément absent ou en dernière position), on effectue $n$ comparaisons : $O(n)$.

3.2 Calcul de moyenne (météo)

Écrire une fonction moyenne(L) qui calcule la moyenne d’une liste de flottants (par exemple des températures). Déterminer sa complexité.

Correction
def moyenne(L):
    """Calcule la moyenne d'une liste non vide."""
    assert len(L) > 0, "La liste ne doit pas être vide"
    total = 0
    for x in L:
        total += x
    return total / len(L)

# Test avec des températures (en °C)
temps = [12.5, 14.0, 11.3, 15.8, 13.2]
assert abs(moyenne(temps) - 13.36) < 0.01

Terminaison : la boucle for parcourt une liste finie. Correction : à la fin, total contient la somme de tous les éléments, et on divise par le nombre d’éléments. Complexité : $O(n)$ (un seul parcours).


Exercice 4 : recherche d’un extremum (sport)

Le tableau suivant donne les temps (en secondes) de cinq coureurs sur un 100 m : [11.2, 10.8, 12.1, 10.5, 11.7]

  1. Écrire une fonction indice_min(L) qui renvoie l’indice du minimum d’une liste (sans utiliser min() ni index()).
  2. En déduire le numéro du coureur le plus rapide.
  3. Prouver la terminaison et la correction. Déterminer la complexité.
Correction
def indice_min(L):
    """Renvoie l'indice du plus petit élément de L."""
    assert len(L) > 0
    i_min = 0
    for i in range(1, len(L)):
        if L[i] < L[i_min]:
            i_min = i
    return i_min

temps = [11.2, 10.8, 12.1, 10.5, 11.7]
i = indice_min(temps)
print(f"Coureur le plus rapide : n°{i+1} avec {temps[i]} s")
# Coureur le plus rapide : n°4 avec 10.5 s

Terminaison : variant $n - i$, décroît de 1 à chaque itération.

Correction (invariant) : à l’entrée de la boucle pour l’indice $i$, i_min contient l’indice du minimum de L[0:i]. Cet invariant est maintenu car on met à jour i_min si L[i] < L[i_min]. À la fin ($i = n$), i_min est l’indice du minimum de toute la liste.

Complexité : $O(n)$ (un seul parcours, une comparaison par élément).


Exercice 5 : invariant et correction – factorielle

On considère le programme suivant :

def factorielle(n):
    i = 0
    f = 1
    while i < n:
        i = i + 1
        f = f * i
    return f
  1. Justifier que la boucle n’est pas infinie (donner le variant).
  2. Calculer factorielle(4) et vérifier que le résultat est $4! = 24$.
  3. Montrer que $f = i!$ est un invariant de la boucle.
  4. En déduire que l’algorithme est correct (c’est-à-dire que factorielle(n) renvoie $n!$).
Correction

1. Terminaison : le variant est $n - i$. À chaque itération, $i$ augmente de 1 donc $n - i$ décroît de 1. Comme $n - i$ est un entier initialement positif (si $n \geq 0$), il atteint 0 en au plus $n$ itérations.

2. Trace de factorielle(4) :

Tour$i$ (avant)$i$ (après)$f$ (après)
101$1 \times 1 = 1$
212$1 \times 2 = 2$
323$2 \times 3 = 6$
434$6 \times 4 = 24$

Résultat : $f = 24 = 4!$ ✓

3. Invariant $f = i!$ :

  • Initialisation : avant la boucle, $i = 0$ et $f = 1 = 0!$ ✓
  • Conservation : si $f = i!$ au début d’un tour, alors après le tour : $i’ = i + 1$ et $f’ = f \times i’ = i! \times (i+1) = (i+1)! = i’!$ ✓

4. Correction : à la sortie de la boucle, $i = n$ (car la condition i < n est fausse). Par l’invariant, $f = i! = n!$. Donc factorielle(n) renvoie bien $n!$.


Exercice 6 : recherche dichotomique

  1. Écrire une fonction dichotomie(L, x) qui cherche la valeur x dans une liste triée L et renvoie True si elle est présente, False sinon.

  2. Tracer l’exécution sur L = [2, 5, 8, 12, 16, 23, 38, 42] avec x = 23.

  3. Prouver la terminaison (variant : $d - g$). Quelle est la complexité ?

Correction
def dichotomie(L, x):
    """Recherche dichotomique de x dans L triée."""
    g = 0
    d = len(L) - 1
    while g <= d:
        m = (g + d) // 2
        if L[m] == x:
            return True
        elif L[m] < x:
            g = m + 1
        else:
            d = m - 1
    return False

Trace pour x = 23 :

Tour$g$$d$$m$L[m]ComparaisonAction
107312$12 < 23$$g = 4$
247523$23 = 23$return True

Trouvé en deux étapes (au lieu de six en séquentiel).

Terminaison : le variant est $d - g + 1$ (taille de l’intervalle de recherche). À chaque itération, soit $g$ augmente, soit $d$ diminue. Le variant décroît strictement et est toujours positif tant que $g \leq d$.

Complexité : à chaque itération, l’intervalle est divisé par 2. Au bout de $k$ itérations, l’intervalle contient au plus $n / 2^k$ éléments. On s’arrête quand $n / 2^k \leq 1$, soit $k \leq \log_2(n)$. La complexité est $O(\log n)$.

# Tests
L = [2, 5, 8, 12, 16, 23, 38, 42]
assert dichotomie(L, 23) == True
assert dichotomie(L, 2) == True
assert dichotomie(L, 42) == True
assert dichotomie(L, 10) == False
assert dichotomie(L, 0) == False

Exercice 7 : synthèse – annuaire téléphonique

Un annuaire est une liste de tuples (nom, numéro) triée par ordre alphabétique :

annuaire = [
    ("Dupont", "01 23 45 67 89"),
    ("Garcia", "06 12 34 56 78"),
    ("Lambert", "04 56 78 90 12"),
    ("Martin", "07 89 01 23 45"),
    ("Petit", "03 21 09 87 65"),
]
  1. Écrire une fonction recherche_seq(annuaire, nom) qui cherche un nom par parcours séquentiel et renvoie le numéro (ou None).
  2. Écrire une fonction recherche_dicho(annuaire, nom) qui fait la même chose par dichotomie (le tableau est trié alphabétiquement).
  3. Comparer le nombre de comparaisons effectuées pour trouver "Petit" avec chaque méthode.
  4. Si l’annuaire contenait un million d’entrées, combien de comparaisons faudrait-il au maximum avec chaque méthode ?
Correction
def recherche_seq(annuaire, nom):
    for n, numero in annuaire:
        if n == nom:
            return numero
    return None

def recherche_dicho(annuaire, nom):
    g, d = 0, len(annuaire) - 1
    while g <= d:
        m = (g + d) // 2
        if annuaire[m][0] == nom:
            return annuaire[m][1]
        elif annuaire[m][0] < nom:
            g = m + 1
        else:
            d = m - 1
    return None

3. Pour trouver "Petit" (dernier élément) :

  • Séquentiel : cinq comparaisons (on parcourt tout).
  • Dichotomie : trois comparaisons au maximum ($\lceil \log_2(5) \rceil = 3$).

4. Pour un million d’entrées :

  • Séquentiel : jusqu’à $1,000,000$ comparaisons.
  • Dichotomie : $\lceil \log_2(1,000,000) \rceil = 20$ comparaisons au maximum.

La dichotomie est incomparablement plus efficace sur de grands ensembles triés.