BTS | Mathématiques | Groupement D2
Dernière mise à jour : 21 octobre 2026, 13:40
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.
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
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).
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.
À chaque étape, on ne conserve que la moitié du tableau contenant la cible : la zone de recherche est divisée par 2.
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 ?
| Tour | gauche | droite | milieu | t[milieu] | Test | Action |
|---|---|---|---|---|---|---|
| 1 | 0 | 6 | (0+6)//2 = 3 | 11 | 11 < 18 | gauche = 4 |
| 2 | 4 | 6 | (4+6)//2 = 5 | 18 | 18 = 18 | return 5 |
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 :
| Étape | Tableau | Comparaison | É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 |
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.
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.
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.
i parcourt range(3) : \(i = 0, 1, 2\) (le dernier élément se place tout seul).
| \(i\) | Partie non triée | Recherche du minimum | i_min | Échange | Tableau obtenu |
|---|---|---|---|---|---|
| 0 | [5, 2, 8, 1] | 2 < 5 → i_min = 1 ; 8 < 2 ? non ; 1 < 2 → i_min = 3 | 3 | t[0] ↔ t[3] | [1, 2, 8, 5] |
| 1 | [2, 8, 5] | 8 < 2 ? non ; 5 < 2 ? non | 1 | t[1] ↔ t[1] (sans effet) | [1, 2, 8, 5] |
| 2 | [8, 5] | 5 < 8 → i_min = 3 | 3 | t[2] ↔ t[3] | [1, 2, 5, 8] |
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).
| Algorithme | Meilleur | Moyen | Pire | Stable ? | Utilisation typique |
|---|---|---|---|---|---|
| Tri à bulles | \(O(n)\) | \(O(n^2)\) | \(O(n^2)\) | Oui | Pédagogie, petits tableaux |
| Tri par insertion | \(O(n)\) | \(O(n^2)\) | \(O(n^2)\) | Oui | Tableaux quasi-triés, petits \(n\) |
| Tri par sélection | \(O(n^2)\) | \(O(n^2)\) | \(O(n^2)\) | Non | Peu d'échanges nécessaires |
| Tri rapide | \(O(n \log n)\) | \(O(n \log n)\) | \(O(n^2)\) | Non | Usage général, très efficace |
| Tri fusion | \(O(n \log n)\) | \(O(n \log n)\) | \(O(n \log n)\) | Oui | Données externes, garantie |
Python sort() | \(O(n)\) | \(O(n \log n)\) | \(O(n \log n)\) | Oui | Usage général (Timsort) |
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.
Après push(10), push(20), push(30) :
Après pop() (retourne 30) :
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.
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ération | Pile (LIFO) | File (FIFO) |
|---|---|---|
| append(10), append(20), append(30) | [10, 20, 30] | [10, 20, 30] |
| retrait → x | x = 30 ; [10, 20] | x = 10 ; [20, 30] |
| append(40) | [10, 20, 40] | [20, 30, 40] |
| retrait → y | y = 40 ; [10, 20] | y = 20 ; [30, 40] |
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ération | Tableau (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 |
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
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]
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 ?
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.
| Tour | a (avant) | b (avant) | nouveau a = b | nouveau b = a + b |
|---|---|---|---|---|
| départ | 0 | 1 | — | — |
| 1 | 0 | 1 | 1 | 1 |
| 2 | 1 | 1 | 1 | 2 |
| 3 | 1 | 2 | 2 | 3 |
| 4 | 2 | 3 | 3 | 5 |
| 5 | 3 | 5 | 5 | 8 |
| 6 | 5 | 8 | 8 | 13 |
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.
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
| Critère | Récursion | Itération |
|---|---|---|
| Lisibilité | Souvent plus claire | Variable |
| Mémoire | Pile d'appels (\(O(n)\) minimum) | Constante si possible |
| Performance | Surcoût des appels | Généralement plus rapide |
| Risque | Stack overflow si trop profond | Boucle infinie |
| Idéal pour | Arbres, graphes, diviser-pour-régner | Boucles simples |
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))
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.
n % 2 == 1 est False :
c'est la branche else (effectif pair) qui s'exécute.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.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.
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.
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é ?
sorted()).
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()
sorted().sorted() / .sort() : Timsort \(O(n \log n)\)list.append() / list.pop()deque.append() / deque.popleft()
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.