← Retour au sommaire BTS

Chapitre 25 – Algorithmique appliquée

BTS  |  Mathématiques  |  Groupement D2

Dernière mise à jour : 21 octobre 2026, 13:40

Objectifs du chapitre

Situation professionnelle — Développeur en informatique industrielle

Contexte : Karim est technicien informatique dans une unité de production automobile. Sa mission : développer un programme de traitement des données de mesure collectées par les capteurs de contrôle qualité sur la ligne d'assemblage.

Chaque heure, 500 mesures de cotes de pièces (en mm) sont enregistrées dans un fichier CSV. Karim doit :

Ce chapitre fournit les algorithmes et leur implémentation Python pour résoudre ces problèmes.

1. Rappels algorithmiques

Ce chapitre s'appuie sur les notions du chapitre 17 (variables, structures de contrôle, fonctions). Rappels essentiels en Python :

# Variables et types de base
n = 10           # entier
x = 3.14         # flottant
nom = "Alice"   # chaîne
tableau = [5, 3, 8, 1, 9, 2]  # liste (mutable, indexée à 0)

# Boucle for
for i in range(5):
    print(i)          # affiche 0, 1, 2, 3, 4

# Boucle while
i = 0
while i < 5:
    i += 1

# Fonction avec docstring
def maxi(tableau):
    """Retourne le maximum d'une liste."""
    m = tableau[0]
    for x in tableau:
        if x > m:
            m = x
    return m

2. Algorithmes de recherche

2.1 Recherche séquentielle

Algorithme — Recherche séquentielle Parcourt le tableau de gauche à droite et compare chaque élément avec la valeur cible. Fonctionne sur tout tableau (trié ou non).
def recherche_sequentielle(tableau, cible):
    """Retourne l'indice de cible, ou -1 si absent."""
    for i in range(len(tableau)):
        if tableau[i] == cible:
            return i
    return -1

# Exemple d'utilisation
t = [5, 3, 8, 1, 9, 2]
print(recherche_sequentielle(t, 8))  # → 2
print(recherche_sequentielle(t, 7))  # → -1
Complexité : \(O(n)\) dans le pire cas (élément absent ou en fin de tableau).

2.2 Recherche dichotomique

Algorithme — Recherche dichotomique (binaire) Prérequis : le tableau doit être trié.
Principe : à chaque étape, on compare la cible avec l'élément médian et on restreint la recherche à la moitié restante pertinente.
def recherche_dichotomique(tableau, cible):
    """Retourne l'indice de cible dans un tableau trié, ou -1."""
    gauche, droite = 0, len(tableau) - 1
    while gauche <= droite:
        milieu = (gauche + droite) // 2
        if tableau[milieu] == cible:
            return milieu
        elif tableau[milieu] < cible:
            gauche = milieu + 1   # chercher à droite
        else:
            droite = milieu - 1  # chercher à gauche
    return -1

# Exemple : tableau trié
t = [1, 2, 3, 5, 8, 9]
print(recherche_dichotomique(t, 8))  # → 4
print(recherche_dichotomique(t, 7))  # → -1
Complexité : \(O(\log_2 n)\). À chaque comparaison, la taille du problème est divisée par 2.
Comparaison chiffrée :
Pour un tableau de \(n = 1\,000\,000\) éléments :
• Recherche séquentielle : jusqu'à 1 000 000 comparaisons
• Recherche dichotomique : \(\lceil\log_2(1\,000\,000)\rceil = 20\) comparaisons au maximum
Rechercher 8 dans [1, 2, 3, 5, 8, 9] g m d 123 589 012 345 milieu = t[2] = 3 < 8 → on élimine la moitié gauche g m d 123 589 milieu = t[4] = 8 = cible → trouvé à l'indice 4

À chaque étape, on ne conserve que la moitié du tableau contenant la cible : la zone de recherche est divisée par 2.

Application — Dérouler une recherche dichotomique

On exécute recherche_dichotomique(t, 18) sur le tableau trié t = [2, 5, 7, 11, 14, 18, 21].

a. Compléter le suivi des variables gauche, droite, milieu jusqu'à l'arrêt, et donner la valeur retournée.
b. Combien de comparaisons recherche_sequentielle(t, 18) aurait-elle effectuées ?

a. Indices : \(t[0]=2,\ t[1]=5,\ t[2]=7,\ t[3]=11,\ t[4]=14,\ t[5]=18,\ t[6]=21\).
Tourgauchedroitemilieut[milieu]TestAction
106(0+6)//2 = 31111 < 18gauche = 4
246(4+6)//2 = 51818 = 18return 5
La fonction retourne \(\mathbf{5}\) après 2 tours de boucle.
b. La recherche séquentielle teste \(t[0], t[1], \dots, t[5]\) : elle trouve la cible au même indice 5, mais après 6 comparaisons. Le gain de la dichotomie apparaît déjà sur 7 valeurs, et devient décisif pour \(n\) grand : \(O(\log_2 n)\) contre \(O(n)\).

3. Algorithmes de tri

3.1 Tri à bulles (Bubble Sort)

Algorithme — Tri à bulles Principe : parcourir le tableau en comparant des éléments adjacents et en les échangeant si nécessaire. Répéter jusqu'à ce qu'aucun échange ne soit effectué.
def tri_bulles(t):
    """Tri à bulles (modifie t en place)."""
    n = len(t)
    for i in range(n - 1):
        echange = False
        for j in range(n - 1 - i):
            if t[j] > t[j+1]:
                t[j], t[j+1] = t[j+1], t[j]
                echange = True
        if not echange:   # déjà trié → arrêt anticipé
            break
Complexité : \(O(n^2)\) (pire et cas moyen), \(O(n)\) (meilleur cas : déjà trié).

Trace d'exécution sur \([5, 3, 1, 4, 2]\) — passe 1 :

ÉtapeTableauComparaisonÉchange
1[5, 3, 1, 4, 2]5 > 3 ?Oui
2[3, 5, 1, 4, 2]5 > 1 ?Oui
3[3, 1, 5, 4, 2]5 > 4 ?Oui
4[3, 1, 4, 5, 2]5 > 2 ?Oui
Fin passe 1[3, 1, 4, 2, 5]—Le 5 est en place

3.2 Tri par insertion

Algorithme — Tri par insertion Pour chaque élément \(t[i]\), on l'insère à sa bonne position dans la partie déjà triée \(t[0..i-1]\).
def tri_insertion(t):
    """Tri par insertion (modifie t en place)."""
    for i in range(1, len(t)):
        cle = t[i]
        j = i - 1
        while j >= 0 and t[j] > cle:
            t[j+1] = t[j]   # décaler vers la droite
            j -= 1
        t[j+1] = cle    # insérer à la bonne position
Complexité : \(O(n^2)\) pire cas, \(O(n)\) si tableau quasi-trié. Efficace pour les petits tableaux et les insertions en temps réel.

3.3 Tri par sélection

Algorithme — Tri par sélection À chaque tour, on sélectionne le minimum de la partie non triée et on l'échange avec le premier élément non trié.
def tri_selection(t):
    """Tri par sélection (modifie t en place)."""
    n = len(t)
    for i in range(n - 1):
        i_min = i
        for j in range(i + 1, n):
            if t[j] < t[i_min]:
                i_min = j
        t[i], t[i_min] = t[i_min], t[i]  # échange
Complexité : \(O(n^2)\) dans tous les cas. Nombre d'échanges minimal : \(O(n)\) — utile si les échanges sont coûteux.
Application — Dérouler un tri par sélection

Dérouler tri_selection(t) sur t = [5, 2, 8, 1] : donner l'indice i_min retenu et l'état du tableau à la fin de chaque tour de la boucle sur i.

\(n = 4\), donc i parcourt range(3) : \(i = 0, 1, 2\) (le dernier élément se place tout seul).
\(i\)Partie non triéeRecherche du minimumi_minÉchangeTableau obtenu
0[5, 2, 8, 1]2 < 5 → i_min = 1 ; 8 < 2 ? non ; 1 < 2 → i_min = 33t[0] ↔ t[3][1, 2, 8, 5]
1[2, 8, 5]8 < 2 ? non ; 5 < 2 ? non1t[1] ↔ t[1] (sans effet)[1, 2, 8, 5]
2[8, 5]5 < 8 → i_min = 33t[2] ↔ t[3][1, 2, 5, 8]
Tableau trié : \(\mathbf{[1,\,2,\,5,\,8]}\).
Bilan : \(3 + 2 + 1 = 6\) comparaisons (toujours les mêmes, quel que soit le tableau : \(O(n^2)\)) mais seulement 3 échanges, dont un sans effet lorsque le minimum était déjà en place.

3.4 Tri rapide (Quicksort)

Algorithme — Tri rapide Principe diviser pour régner :
1. Choisir un pivot.
2. Partitionner : éléments < pivot à gauche, éléments > pivot à droite.
3. Récursivement trier les deux partitions.
def tri_rapide(t, gauche=0, droite=None):
    """Tri rapide récursif (modifie t en place)."""
    if droite is None:
        droite = len(t) - 1
    if gauche < droite:
        pivot_idx = partition(t, gauche, droite)
        tri_rapide(t, gauche, pivot_idx - 1)
        tri_rapide(t, pivot_idx + 1, droite)

def partition(t, gauche, droite):
    """Partition autour du dernier élément (pivot)."""
    pivot = t[droite]
    i = gauche - 1
    for j in range(gauche, droite):
        if t[j] <= pivot:
            i += 1
            t[i], t[j] = t[j], t[i]
    t[i+1], t[droite] = t[droite], t[i+1]
    return i + 1
Complexité : \(O(n \log n)\) en moyenne, \(O(n^2)\) pire cas (tableau déjà trié avec mauvais pivot).

3.5 Comparaison des algorithmes de tri

AlgorithmeMeilleurMoyenPireStable ?Utilisation typique
Tri à bulles\(O(n)\)\(O(n^2)\)\(O(n^2)\)OuiPédagogie, petits tableaux
Tri par insertion\(O(n)\)\(O(n^2)\)\(O(n^2)\)OuiTableaux quasi-triés, petits \(n\)
Tri par sélection\(O(n^2)\)\(O(n^2)\)\(O(n^2)\)NonPeu d'échanges nécessaires
Tri rapide\(O(n \log n)\)\(O(n \log n)\)\(O(n^2)\)NonUsage général, très efficace
Tri fusion\(O(n \log n)\)\(O(n \log n)\)\(O(n \log n)\)OuiDonnées externes, garantie
Python sort()\(O(n)\)\(O(n \log n)\)\(O(n \log n)\)OuiUsage général (Timsort)

4. Structures de données linéaires

4.1 Pile (LIFO — Last In First Out)

Définition — Pile Une pile est une structure de données où le dernier élément inséré est le premier extrait (principe LIFO).
Opérations : push (empiler), pop (dépiler), peek (lire le sommet).
# En Python, une liste s'utilise efficacement comme pile
pile = []
pile.append(10)    # push → pile = [10]
pile.append(20)    # push → pile = [10, 20]
pile.append(30)    # push → pile = [10, 20, 30]
sommet = pile.pop()  # pop  → sommet = 30, pile = [10, 20]

# Implémentation sous forme de classe
class Pile:
    def __init__(self):
        self._data = []
    def push(self, val): self._data.append(val)
    def pop(self):
        if self._data:
            return self._data.pop()
    def peek(self):
        return self._data[-1] if self._data else None
    def est_vide(self): return len(self._data) == 0
Applications : appels de fonctions (pile d'exécution), annulation (Ctrl+Z), vérification des parenthèses, évaluation d'expressions.
Visualisation d'une pile :

Après push(10), push(20), push(30) :

30 ← sommet
20
10

Après pop() (retourne 30) :

20 ← sommet
10

4.2 File (FIFO — First In First Out)

Définition — File Une file est une structure où le premier élément inséré est le premier extrait (FIFO). Opérations : enqueue (enfiler par l'arrière), dequeue (défiler par l'avant).
from collections import deque

file = deque()            # file vide
file.append("alerte_1")  # enqueue
file.append("alerte_2")  # enqueue
file.append("alerte_3")  # enqueue
premier = file.popleft()  # dequeue → "alerte_1"

# deque est O(1) pour popleft()
# list.pop(0) serait O(n) — à éviter pour les grandes files
Applications : gestion de files d'attente (serveurs, impressions), BFS, tampon E/S.
alerte_1 alerte_2 alerte_3 Sortie · dequeue popleft() Entrée · enqueue append() avant (1ᵉʳ entré) arrière
Application — LIFO ou FIFO : prévoir l'ordre de sortie

On applique la même suite d'opérations à une pile puis à une file :

# Version pile (liste)            # Version file (deque)
p = []                          f = deque()
p.append(10)                   f.append(10)
p.append(20)                   f.append(20)
p.append(30)                   f.append(30)
x = p.pop()                     x = f.popleft()
p.append(40)                   f.append(40)
y = p.pop()                     y = f.popleft()

Donner, dans chaque cas, les valeurs de x et y ainsi que le contenu final de la structure.

OpérationPile (LIFO)File (FIFO)
append(10), append(20), append(30)[10, 20, 30][10, 20, 30]
retrait → xx = 30 ; [10, 20]x = 10 ; [20, 30]
append(40)[10, 20, 40][20, 30, 40]
retrait → yy = 40 ; [10, 20]y = 20 ; [30, 40]
Pile : \(\mathbf{x = 30}\), \(\mathbf{y = 40}\), contenu final [10, 20] — c'est le dernier entré qui sort (le 40, à peine empilé, sort aussitôt).
File : \(\mathbf{x = 10}\), \(\mathbf{y = 20}\), contenu final [30, 40] — c'est le premier entré qui sort ; le 40 attend son tour derrière le 30.
Pour un traitement d'alertes dans leur ordre d'apparition, c'est bien la file qu'il faut choisir.

4.3 Liste chaînée

Définition — Liste chaînée Chaque nœud contient une valeur et un pointeur vers le nœud suivant. Contrairement à un tableau, les éléments ne sont pas contigus en mémoire.
class Noeud:
    def __init__(self, valeur):
        self.valeur = valeur
        self.suivant = None

class ListeChainee:
    def __init__(self): self.tete = None

    def inserer_debut(self, val):
        nouveau = Noeud(val)
        nouveau.suivant = self.tete
        self.tete = nouveau

    def parcourir(self):
        courant = self.tete
        while courant:
            print(courant.valeur, end=" -> ")
            courant = courant.suivant
        print("None")
OpérationTableau (list)Liste chaînée
Accès par indice\(O(1)\)\(O(n)\)
Insertion en tête\(O(n)\)\(O(1)\)
Insertion en fin\(O(1)\)*\(O(n)\) ou \(O(1)\) avec pointeur fin
Suppression\(O(n)\)\(O(1)\) si nœud connu
*amorti
tête 12 7 5 None valeur suivant

5. Algorithmes récursifs

5.1 Principe de la récursivité

Définition — Récursivité Une fonction est récursive si elle s'appelle elle-même. Tout algorithme récursif comporte obligatoirement :
• Un ou plusieurs cas de base (condition d'arrêt) — sans appel récursif.
• Un ou plusieurs appels récursifs sur un problème de taille strictement plus petite.

Sans cas de base (ou si le cas de base n'est jamais atteint), on obtient une récursion infinie (équivalent d'une boucle infinie), qui provoque un dépassement de pile (stack overflow).

5.2 Factorielle

def factorielle(n):
    """Calcule n! récursivement. n doit être un entier >= 0."""
    if n == 0:         # cas de base : 0! = 1
        return 1
    return n * factorielle(n - 1)  # appel récursif

# Arbre d'appels pour factorielle(4) :
#  factorielle(4) = 4 × factorielle(3)
#                       = 3 × factorielle(2)
#                             = 2 × factorielle(1)
#                                   = 1 × factorielle(0) = 1

5.3 Suite de Fibonacci

def fibonacci_recursif(n):
    """F(n) récursif — complexité O(2^n), à éviter pour grands n."""
    if n <= 1: return n
    return fibonacci_recursif(n-1) + fibonacci_recursif(n-2)

def fibonacci_iteratif(n):
    """F(n) itératif — complexité O(n), recommandé."""
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

def fibonacci_memo(n, cache={}):
    """F(n) avec mémoïsation — complexité O(n)."""
    if n in cache: return cache[n]
    if n <= 1: return n
    cache[n] = fibonacci_memo(n-1) + fibonacci_memo(n-2)
    return cache[n]
Attention — Fibonacci récursif naïf La version récursive naïve recalcule les mêmes valeurs un très grand nombre de fois : \(T(n) \approx 2^n\) opérations. Pour \(n=50\), c'est plus de \(10^{15}\) appels. Toujours préférer la version itérative ou avec mémoïsation.
Application — Affectation simultanée dans Fibonacci

Dérouler fibonacci_iteratif(6) en notant les valeurs de a et b après chaque tour de boucle, puis donner la valeur retournée.
Que renverrait la fonction si l'on remplaçait a, b = b, a + b par les deux lignes successives a = b puis b = a + b ?

L'affectation a, b = b, a + b évalue d'abord le membre de droite avec les anciennes valeurs, puis affecte les deux variables en même temps.
Toura (avant)b (avant)nouveau a = bnouveau b = a + b
départ01——
10111
21112
31223
42335
53558
658813
Après les 6 tours, la fonction retourne \(a = \mathbf{8}\), ce qui est bien \(F(6)\) (suite \(0,\,1,\,1,\,2,\,3,\,5,\,8\)).
Avec deux lignes successives : a = b écrase \(a\) avant le calcul de b = a + b, qui vaut alors \(b + b = 2b\). Les valeurs de \(a\) deviennent \(1, 2, 4, 8, 16, 32\) : la fonction retournerait 32, c'est-à-dire \(2^5\) et non \(F(6)\). L'affectation simultanée est donc indispensable.

5.4 Tours de Hanoï

def hanoi(n, source, cible, intermediaire):
    """Déplace n disques de source vers cible via intermediaire."""
    if n == 1:
        print(f"Déplacer disque 1 : {source} → {cible}")
        return
    hanoi(n-1, source, intermediaire, cible)
    print(f"Déplacer disque {n} : {source} → {cible}")
    hanoi(n-1, intermediaire, cible, source)

# hanoi(3, 'A', 'C', 'B')  → 7 déplacements
# Nombre de déplacements pour n disques = 2^n - 1

5.5 Récursion vs itération

CritèreRécursionItération
LisibilitéSouvent plus claireVariable
MémoirePile d'appels (\(O(n)\) minimum)Constante si possible
PerformanceSurcoût des appelsGénéralement plus rapide
RisqueStack overflow si trop profondBoucle infinie
Idéal pourArbres, graphes, diviser-pour-régnerBoucles simples

6. Algorithmes de traitement de données

def statistiques(data):
    """Calcule moyenne, médiane, variance, min, max d'une liste."""
    n = len(data)
    if n == 0: return None

    # Minimum et maximum
    minimum = maximum = data[0]
    for x in data:
        if x < minimum: minimum = x
        if x > maximum: maximum = x

    # Moyenne
    somme = sum(data)
    moyenne = somme / n

    # Variance et écart-type
    variance = sum((x - moyenne)**2 for x in data) / n
    ecart_type = variance ** 0.5

    # Médiane (nécessite un tableau trié)
    trie = sorted(data)
    if n % 2 == 1:
        mediane = trie[n // 2]
    else:
        mediane = (trie[n//2 - 1] + trie[n//2]) / 2

    return {
        "n": n, "min": minimum, "max": maximum,
        "moyenne": moyenne, "médiane": mediane,
        "variance": variance, "écart_type": ecart_type
    }

def filtrer(data, predicate):
    """Retourne les éléments vérifiant predicate."""
    return [x for x in data if predicate(x)]

def compter(data, predicate):
    """Compte les éléments vérifiant predicate."""
    return sum(1 for x in data if predicate(x))
Application — Médiane : quelle branche de l'algorithme ?

Un poste de contrôle relève les cotes suivantes (en mm) : data = [100.2, 99.7, 100.9, 99.4, 100.1, 99.9].

En suivant la fonction statistiques, indiquer quelle branche du test if n % 2 == 1 est exécutée, puis calculer la médiane. Reprendre en supprimant la dernière mesure.

Cas 1 — 6 mesures. \(n = 6\), donc n % 2 == 1 est False : c'est la branche else (effectif pair) qui s'exécute.
Tableau trié : \([99{,}4\ ;\ 99{,}7\ ;\ 99{,}9\ ;\ 100{,}1\ ;\ 100{,}2\ ;\ 100{,}9]\). Avec n//2 = 3 : \[ \text{mediane} = \frac{\text{trie}[2] + \text{trie}[3]}{2} = \frac{99{,}9 + 100{,}1}{2} = \mathbf{100{,}0\ \text{mm}} \] Cas 2 — 5 mesures (sans la valeur 99,9). \(n = 5\) : n % 2 == 1 est True, on prend l'élément central.
Tableau trié : \([99{,}4\ ;\ 99{,}7\ ;\ 100{,}1\ ;\ 100{,}2\ ;\ 100{,}9]\), et n//2 = 2 : \[ \text{mediane} = \text{trie}[2] = \mathbf{100{,}1\ \text{mm}} \] Attention à l'indexation à partir de 0 : pour \(n\) impair, trie[n//2] est bien la \(3^{\text{e}}\) valeur sur 5, donc la valeur centrale.

7. Complexité algorithmique

7.1 Notation Big-O

Définition — Notation Big-O La notation \(O(f(n))\) (Big-O) décrit la croissance asymptotique du nombre d'opérations d'un algorithme en fonction de la taille \(n\) de l'entrée.

On dit que l'algorithme est \(O(f(n))\) s'il existe des constantes \(c > 0\) et \(n_0\) telles que le nombre d'opérations \(T(n) \leq c \cdot f(n)\) pour tout \(n \geq n_0\).

On ne garde que le terme dominant et on ignore les constantes : \(3n^2 + 5n + 2 \in O(n^2)\).
\(O(1)\)Constante
accès tableau par indice
\(O(\log n)\)Logarithmique
dichotomie
\(O(n)\)Linéaire
parcours, recherche séquentielle
\(O(n \log n)\)Linéarithmique
tri rapide, tri fusion
\(O(n^2)\)Quadratique
tri à bulles, sélection
\(O(2^n)\)Exponentielle
Fibonacci naïf, backtracking
n (taille des données) nombre d'opérations O(1) — constante O(log n) — dichotomie O(n) — parcours O(n log n) — tri rapide O(n²) — tri quadratique O(2ⁿ) — exponentielle

Pour de grandes tailles \(n\), l'écart entre les classes de complexité devient énorme : \(O(2^n)\) et \(O(n^2)\) explosent, tandis que \(O(\log n)\) reste quasi plate.

Application — Du nombre d'opérations au temps de calcul

L'analyse d'un module de traitement donne \(T(n) = 2n^2 + 30n + 100\) opérations pour \(n\) mesures.

a. Donner sa complexité en notation Big-O.
b. Le module traite \(n = 1\,000\) mesures en 0,4 s. Estimer le temps pour \(n = 5\,000\) mesures. Le cahier des charges (moins de 2 s) est-il respecté ?

a. On ne conserve que le terme dominant et on ignore les constantes : \(2n^2 + 30n + 100 \in \mathbf{O(n^2)}\) — l'algorithme est quadratique.
b. Le temps est proportionnel à \(n^2\). Ici \(n\) est multiplié par 5, donc le temps est multiplié par \[ \left(\frac{5\,000}{1\,000}\right)^2 = 5^2 = 25 \] \[ t(5\,000) \approx 0{,}4 \times 25 = \mathbf{10\ \text{s}} \] (le calcul exact avec \(T(n)\) donne \(\dfrac{50\,150\,100}{2\,030\,100} \approx 24{,}7\), soit \(\approx 9{,}9\) s : le terme dominant suffit bien à l'estimation).
Conclusion : 10 s ≫ 2 s, le cahier des charges n'est pas respecté. Il faut remplacer l'algorithme quadratique par un algorithme en \(O(n \log n)\) (tri rapide ou sorted()).

7.2 Complexité temporelle et spatiale

Deux types de complexité On distingue le pire cas (\(O\)), le cas moyen (\(\Theta\)) et le meilleur cas (\(\Omega\)). En pratique, le pire cas est le plus utile pour garantir les performances.
Analyse de la recherche dichotomique :
À chaque étape, la taille du sous-tableau est divisée par 2. Après \(k\) étapes, la taille est \(\dfrac{n}{2^k}\). On s'arrête quand \(\dfrac{n}{2^k} \leq 1\), soit \(k \geq \log_2 n\). Donc au maximum \(\lceil \log_2 n \rceil\) comparaisons. Pour \(n = 10^6\) : \(\lceil \log_2(10^6) \rceil = 20\) comparaisons. Bien plus efficace que \(O(n)\).

8. Application — Traitement de données industrielles

Programme complet de traitement des données de cotes de pièces pour Karim. Il lit les mesures, les trie, calcule les statistiques et génère un rapport d'anomalies.

#!/usr/bin/env python3
"""
Contrôle qualité automatique — Analyse des cotes de fabrication.
Utilisation : python3 controle_qualite.py
"""
from collections import deque
import math

TOLERANCE = 0.5    # mm
COTE_NOMINALE = 100.0  # mm

def charger_mesures(fichier):
    """Simule le chargement de mesures depuis un fichier CSV."""
    # En production : with open(fichier) as f: ...
    import random
    random.seed(42)
    return [100.0 + random.gauss(0, 0.3) for _ in range(500)]

def detecter_anomalies(mesures, cote_nom, tol):
    """Retourne les indices et valeurs des mesures hors tolérance."""
    return [(i, v) for i, v in enumerate(mesures)
            if abs(v - cote_nom) > tol]

def statistiques(data):
    """Calcule les statistiques descriptives."""
    n = len(data)
    moy = sum(data) / n
    var = sum((x - moy)**2 for x in data) / n
    trie = sorted(data)
    med = trie[n//2] if n%2 else (trie[n//2-1]+trie[n//2])/2
    return {"n":n, "min":min(data), "max":max(data),
             "moy":moy, "med":med, "ecart":math.sqrt(var)}

def recherche_dichotomique_trie(trie, cible, tol):
    """Vérifie rapidement si une valeur proche de cible existe."""
    g, d = 0, len(trie)-1
    while g <= d:
        m = (g+d)//2
        if abs(trie[m]-cible) <= tol: return True
        elif trie[m] < cible: g = m+1
        else: d = m-1
    return False

def main():
    # 1. Chargement
    mesures = charger_mesures("mesures.csv")
    print(f"Mesures chargées : {len(mesures)} valeurs")

    # 2. Statistiques
    stats = statistiques(mesures)
    print(f"Moyenne : {stats['moy']:.4f} mm | Écart-type : {stats['ecart']:.4f} mm")
    print(f"Médiane : {stats['med']:.4f} mm | Min : {stats['min']:.4f} | Max : {stats['max']:.4f}")

    # 3. Détection des anomalies
    anomalies = detecter_anomalies(mesures, COTE_NOMINALE, TOLERANCE)
    print(f"Pièces hors tolérance : {len(anomalies)} / {len(mesures)}")

    # 4. File d'alertes FIFO
    alertes = deque()
    for idx, val in anomalies:
        alertes.append({"piece": idx+1, "cote": val,
                         "ecart": val-COTE_NOMINALE})

    print("\n=== Rapport d'alertes (file FIFO) ===")
    while alertes:
        a = alertes.popleft()
        print(f"  Pièce #{a['piece']:4d} : {a['cote']:.4f} mm (écart {a['ecart']:+.4f} mm)")

if __name__ == "__main__":
    main()
Méthode — Analyser et écrire un algorithme
  1. Comprendre le problème : données d'entrée, résultat attendu, contraintes.
  2. Choisir la structure de données : accès rapide → tableau ; insertion/suppression fréquentes → liste ou deque ; LIFO → pile ; FIFO → file.
  3. Choisir l'algorithme : si données triées → dichotomique ; si petit tableau → tri insertion ; si grand tableau → tri rapide ou sorted().
  4. Écrire le pseudo-code avant de coder (préconditions, postconditions).
  5. Implémenter en Python avec docstring et noms de variables explicites.
  6. Tester sur des cas limites : tableau vide, un élément, éléments identiques, déjà trié, trié à l'envers.
  7. Analyser la complexité : déterminer \(O(\cdot)\) et décider si c'est acceptable pour le volume de données.

À retenir — Algorithmique appliquée

Recherche et tri
  • Séquentielle : \(O(n)\), tout tableau
  • Dichotomique : \(O(\log n)\), tableau trié
  • Tri quadratique : bulles, insertion, sélection — \(O(n^2)\)
  • Tri rapide (quicksort) : \(O(n \log n)\) moyen
  • Python sorted() / .sort() : Timsort \(O(n \log n)\)
Structures et récursivité
  • Pile (LIFO) : list.append() / list.pop()
  • File (FIFO) : deque.append() / deque.popleft()
  • Récursivité : cas de base obligatoire + réduction du problème
  • Fibonacci : naïf \(O(2^n)\), mémoïsation/itératif \(O(n)\)
  • Complexité : \(O(1) \ll O(\log n) \ll O(n) \ll O(n\log n) \ll O(n^2) \ll O(2^n)\)

Règle d'or : ne jamais utiliser list.pop(0) dans une boucle (c'est \(O(n)\) par appel, donc \(O(n^2)\) au total) — utiliser collections.deque à la place.