Algorithmique & Python avancé
Créez un compte gratuit pour suivre votre avancement et reprendre où vous avez laissé.
Créer un compteAlgorithmique & Python avancé
Cette fiche est délibérément hors programme lycée. On y aborde des notions que tu retrouveras en spécialité NSI, en classe préparatoire ou en première année post-bac : complexité algorithmique, structures de données efficaces, idiomes Pythoniques, récursivité et bonnes pratiques d'ingénieur logiciel.
À la fin, tu sauras estimer le coût d'un algorithme avec la notation $O(n)$, choisir entre une liste et un dictionnaire selon le besoin, écrire des compréhensions de listes et de dictionnaires, accélérer une fonction récursive avec la mémoïsation, et structurer du code propre avec des dataclass et des assert.
Remarque
Comment lire cette fiche. Le rythme est dense : chaque section introduit un concept d'algorithmique générale, puis montre comment Python l'exprime de manière idiomatique. Tu rencontreras :
- Complète le programme — 1 ou 2 mots à compléter, juste après chaque nouveau concept.
- À toi de jouer — un petit programme à finir, en fin de section.
Comme partout, chaque cellule s'exécute en cliquant sur Exécuter. Lis attentivement les messages d'erreur : à ce niveau, ils sont tes meilleurs alliés.
Complexité algorithmique : $O(1)$, $O(n)$, $O(\log n)$
La complexité d'un algorithme décrit comment son temps d'exécution évolue quand la taille des données augmente. On la note avec un grand O. Trois ordres de grandeur reviennent sans cesse :
- $O(1)$ — temps constant : accéder à
liste[i], lire une variable. - $O(n)$ — temps linéaire : parcourir une liste de
néléments. - $O(\log n)$ — temps logarithmique : recherche dichotomique dans une liste triée.
Comparons une recherche linéaire et une recherche dichotomique :
Les deux trouvent la cible. Mais sur une liste d'un million d'éléments, la dichotomique fait au plus une vingtaine d'itérations contre presque un million pour la linéaire ($\log_2(10^6) \approx 20$).
On peut mesurer concrètement avec time.perf_counter :
Complète le programme
Complète la condition : pour que la dichotomie fonctionne, la liste doit être triée. Affiche True si une liste est triée par ordre croissant.
Complète le programme
Quel est l'ordre de grandeur du nombre d'itérations de la boucle while dans une recherche dichotomique sur une liste de taille n = 1024 ? Stocke ton estimation dans reponse.
Voir la solution
import math
n = 1024
reponse = int(math.log2(n))
print(reponse)À toi de jouer
Écris une fonction somme_paires(L) qui renvoie la somme des éléments pairs d'une liste. Quelle est sa complexité ?
Voir la solution
def somme_paires(L):
total = 0
for x in L:
if x % 2 == 0:
total += x
return total
print(somme_paires([1, 2, 3, 4, 5, 6]))Remarque
Spécificité Python — la mesure trompeuse. Le for Python est lent par rapport à un for C. Une boucle native de 10 millions d'itérations prend plusieurs secondes en Python pur, alors qu'un appel à L.index(...) (codé en C) sur la même taille prend des millisecondes. Conséquence pratique : avant d'optimiser, mesure — et privilégie les fonctions natives (sum, max, min, any, all, méthodes de list) qui descendent au C sous le capot.
Dictionnaires et tables de hachage
Le dictionnaire (dict) est la structure phare de Python : il associe une clé à une valeur et permet l'accès en $O(1)$ moyen, grâce à une table de hachage. C'est radicalement plus rapide qu'une recherche dans une liste.
Cas d'usage emblématique : compter les occurrences. Avec une liste, il faudrait L.count(x) pour chaque x distinct, soit $O(n^2)$. Avec un dict, on fait un seul passage en $O(n)$ :
Encore plus idiomatique : collections.Counter fait exactement la même chose, en plus court :
Complète le programme
Complète l'accès au dictionnaire : récupère la note d'Inès, ou None si elle n'y figure pas (utilise la méthode get).
Voir la solution
notes = {'Léa': 14, 'Tom': 11, 'Inès': 17}
print(notes.get('Inès'))
print(notes.get('Zoé'))Complète le programme
Complète l'opérateur d'appartenance : teste si la clé 'Tom' est présente dans notes. Cette opération est en $O(1)$ sur un dict.
Voir la solution
notes = {'Léa': 14, 'Tom': 11}
print('Tom' in notes)À toi de jouer
Écris une fonction anagramme(a, b) qui renvoie True si a et b sont des anagrammes (même multiset de lettres). Utilise Counter : c'est une seule ligne.
Voir la solution
from collections import Counter
def anagramme(a, b):
return Counter(a) == Counter(b)
print(anagramme('chien', 'niche'))
print(anagramme('python', 'java'))Remarque
Spécificité Python — clés hashables. Une clé de dict doit être hashable : son hash ne doit pas changer pendant la durée de vie de l'objet. C'est le cas de tout objet immuable (int, str, tuple de hashables, frozenset). Une list ne peut pas être clé : elle est mutable. Essaie {[1, 2]: 'oups'} dans une cellule, tu verras l'erreur TypeError: unhashable type: 'list'.
Compréhensions et idiomes Pythoniques
Une compréhension construit une liste, un dict ou un set en une expression. C'est la forme la plus idiomatique d'écrire une boucle qui filtre/transforme. Trois familles :
On peut filtrer (if) et transformer (expr avant le for) en même temps. C'est plus lisible — et souvent plus rapide — qu'une boucle for explicite avec append.
zip et enumerate sont des compagnons indispensables :
Complète le programme
Construis une compréhension qui renvoie les cubes des entiers de 1 à 5 inclus.
Voir la solution
cubes = [n ** 3 for n in range(1, 6)]
print(cubes)Complète le programme
Construis un dictionnaire qui associe chaque mot à sa longueur, en une compréhension.
Voir la solution
mots = ['python', 'algorithme', 'liste', 'dict']
longueurs = {m: len(m) for m in mots}
print(longueurs)À toi de jouer
Écris en une seule ligne une expression qui renvoie la liste des nombres de 1 à 30 multiples de 3 ou de 5 (clin d'œil au Project Euler n°1).
Voir la solution
multiples = [n for n in range(1, 31) if n % 3 == 0 or n % 5 == 0]
print(multiples)
print(sum(multiples))Remarque
Spécificité Python — l'expression conditionnelle. Python a un opérateur ternaire qui se lit en français : a if condition else b. Il s'utilise dans les compréhensions pour transformer plutôt que filtrer : [n if n > 0 else 0 for n in L] renvoie chaque élément ou 0 (pas de filtrage, transformation conditionnelle). Ne le confonds pas avec le if de filtrage en fin de compréhension — la position et l'usage diffèrent.
Récursivité et mémoïsation
Une fonction récursive s'appelle elle-même avec un argument plus petit, jusqu'à atteindre un cas de base. Exemple classique : la suite de Fibonacci définie par $F_0 = 0$, $F_1 = 1$, $F_n = F_{n-1} + F_{n-2}$.
Le code est élégant, mais exponentiel : fib(40) fait des milliards d'appels redondants. Solution : mémoriser les résultats déjà calculés. C'est la mémoïsation, offerte par functools.lru_cache en un décorateur :
Avec le cache, la complexité passe de $O(\varphi^n)$ à $O(n)$ — fib_memo(100) est instantané et donne un entier de 21 chiffres (rappel : Python gère les grands entiers nativement).
Attention au plafond de récursion : Python l'arrête vers 1000 appels imbriqués pour éviter de faire planter la pile. On peut lire ou changer la limite :
Complète le programme
Écris la fonction factorielle récursive. Cas de base : $0! = 1$ et $1! = 1$.
Complète le programme
Complète la version itérative, beaucoup plus économe en pile pour les grands n.
Voir la solution
def factorielle_iter(n):
resultat = 1
for k in range(2, n + 1):
resultat *= k
return resultat
print(factorielle_iter(20))À toi de jouer
La suite de Padovan est définie par $P_0 = P_1 = P_2 = 1$ et $P_n = P_{n-2} + P_{n-3}$. Écris une version mémoïsée et affiche P(20).
Remarque
Spécificité Python — pas de TCO. Beaucoup de langages fonctionnels (Scheme, Haskell, OCaml) optimisent la récursion terminale en boucle. Pas Python : Guido van Rossum a explicitement refusé cette optimisation, considérant qu'elle nuit à la lisibilité des traces d'erreur. Conséquence : pour tout algorithme récursif profond ($n > 1000$), il faut soit augmenter la limite, soit ré-écrire en boucle, soit dérécursiver avec une pile explicite. C'est un choix de design assumé.
Code propre : dataclasses, type hints, tests
Quand un programme grossit, on manipule des objets structurés plutôt que des tuples ou des dicts. Le décorateur @dataclass génère automatiquement __init__, __repr__ et __eq__ :
Les annotations x: float sont des type hints : Python ne les vérifie pas à l'exécution, mais elles documentent l'intention et sont exploitées par des outils comme mypy ou les IDE.
Pour vérifier qu'une fonction se comporte comme attendu, on écrit des tests. La forme la plus simple : assert, qui lève une erreur si la condition est fausse.
Si l'un des assert échoue, Python affiche une AssertionError avec la ligne fautive. C'est le b.a.-ba du TDD (Test-Driven Development).
Complète le programme
Crée une @dataclass Vecteur avec deux flottants x et y, puis instancie v = Vecteur(2, -1) et affiche-le.
Voir la solution
from dataclasses import dataclass
@dataclass
class Vecteur:
x: float
y: float
v = Vecteur(2, -1)
print(v)Complète le programme
Complète l'assertion : Vecteur(1, 2) doit être égal à un autre Vecteur(1, 2) grâce à l'égalité générée automatiquement.
Voir la solution
from dataclasses import dataclass
@dataclass
class Vecteur:
x: float
y: float
assert Vecteur(1, 2) == Vecteur(1, 2)
print('OK')À toi de jouer
Écris une @dataclass Élève avec nom: str et notes: list. Ajoute une méthode moyenne(self). Vérifie son comportement avec deux assert.
Voir la solution
from dataclasses import dataclass
@dataclass
class Élève:
nom: str
notes: list
def moyenne(self):
return sum(self.notes) / len(self.notes)
e = Élève('Léa', [12, 16, 14])
assert e.moyenne() == 14
assert Élève('Tom', [10, 10]).moyenne() == 10
print('Tests OK')Remarque
Spécificité Python — duck typing. Les type hints ne sont pas vérifiés à l'exécution : si tu passes Vecteur('a', 'b'), Python ne dit rien tant que l'opération ne casse pas. C'est le duck typing : « si ça nage comme un canard et cancane comme un canard, c'est un canard ». Cette philosophie favorise la souplesse — au prix d'erreurs qui surgissent tard. Les outils statiques (mypy, pyright) comblent ce manque sans imposer la rigueur d'un Java ou d'un Rust.
Pour aller plus loin
Générateurs et lazy evaluation. Un yield transforme une fonction en générateur : il produit ses valeurs à la demande, sans tout stocker en mémoire. C'est l'idiome qui permet à Python de manipuler des flux infinis ou très grands.
itertools — le couteau suisse des itérables. Le module standard itertools fournit des combinatoires et des compositions de flux extraordinairement puissantes. À retenir : product, combinations, permutations, chain, accumulate, groupby.
Walrus operator := (Python 3.8+). Cet opérateur affecte et renvoie une valeur dans la même expression. Pratique dans une boucle while ou une compréhension :
Comparaison avec C/Java/JS. Là où C te demande de gérer la mémoire à la main et Java t'impose une déclaration de type pour chaque variable, Python combine gestion automatique de la mémoire (garbage collector), typage dynamique et itérables paresseux de première classe. Un Pythoniste expérimenté écrit sum(n*n for n in range(10**7)) sans hésiter — calcul correct, mémoire constante. L'équivalent en C demande une boucle explicite ; en Java, des Stream verbeux ; en JS, du chaînage .reduce() sur Array complet.
Un dernier idiome : l'unpacking étoilé. Le * capture le « reste » dans une affectation ou un appel. Lis ce code :
À toi de jouer
Écris un générateur premiers(n) qui produit les nombres premiers jusqu'à n exclu. Utilise all et une compréhension pour la primalité. Affiche la liste des premiers jusqu'à 50.
Voir la solution
def premiers(n):
for k in range(2, n):
if all(k % d != 0 for d in range(2, int(k ** 0.5) + 1)):
yield k
print(list(premiers(50)))Ce que tu as appris
- La complexité $O(n)$, $O(\log n)$, $O(1)$ décrit l'évolution du temps avec la taille des données ; mesurer reste indispensable avant d'optimiser.
- Les
dictreposent sur une table de hachage : accès et appartenance en $O(1)$ moyen ; les clés doivent être hashables (immuables). - Les compréhensions (
[... for ... if ...]) sont l'idiome canonique pour transformer/filtrer un itérable en une expression. - La récursivité s'accélère avec la mémoïsation (
@lru_cache) ; Python ne fait pas de TCO, attention au plafond de pile. - Les
@dataclassstructurent les données ; les type hints documentent ;assertvérifie. - Générateurs (
yield),itertools, walrus:=et unpacking étoilé (*args) constituent le vocabulaire d'un Pythoniste avancé.
Et après…
Tu as parcouru les 10 chapitres du parcours Python du site. Pour aller plus loin, explore les ressources dédiées : la documentation officielle docs.python.org, le module itertools dans le détail, la bibliothèque numpy (calcul scientifique vectorisé) et les tests unitaires avec unittest ou pytest. Bonne route !