Fiches d'algorithmes pour débutants — version fusionnée et corrigée
Bases : Grokking Algorithms (1re éd., 2016) et Introduction to Algorithms (CLRS, 3e éd., 2009), complétées par les mises à jour connues au 10 novembre 2026.
Sommaire
Ce fichier : mises à jour, corrections Python 2 → 3, plan de révision, sources
Grand O
Recherche dichotomique
Mémoire, tableaux et listes chaînées
Récursivité et pile d'appels
Diviser pour régner et tris
Tables de hachage
Graphes et parcours en largeur (BFS)
Plus courts chemins : Dijkstra et Bellman-Ford
Algorithmes gloutons et problèmes NP
Programmation dynamique
k plus proches voisins et machine learning
Arbres
Pour aller plus loin (Grokking ch. 11 et CLRS)
Aide-mémoire final
Chaque fiche suit le même plan : idée en une phrase, explication, code Python 3 testé, complexité, pièges, exercices corrigés, renvoi aux livres.
Conventions : n = nombre d'éléments, V = sommets, E = arêtes. Les complexités sont écrites en texte simple (O(n log n)) pour s'afficher partout, sans LaTeX. Les numéros de chapitres CLRS suivent la 3e édition, sauf mention contraire.
Ce qui a changé depuis tes deux livres
Grokking Algorithms, 2e édition (mars 2024)
Code passé en Python 3 (ton PDF est en Python 2.7).
Nouveaux chapitres sur les arbres : arbres binaires de recherche, arbres équilibrés, B-arbres.
Couverture des problèmes NP-complets étoffée.
Discussion des performances qui tient compte des processeurs modernes.
Plan : 1 Introduction, 2 Tri par sélection, 3 Récursivité, 4 Diviser pour régner (quicksort), 5 Tables de hachage, 6 Parcours en largeur, 7 Arbres, 8 Arbres équilibrés, 9 Dijkstra, 10 Glouton, 11 Programmation dynamique, 12 k plus proches voisins, 13 Pour aller plus loin.
CLRS, 4e édition (avril 2022)
Nouveaux chapitres : appariements dans les graphes bipartis, algorithmes en ligne, algorithmes de machine learning.
Le chapitre de machine learning (ch. 33 de la 4e éd.) traite le regroupement k-means, les pondérations multiplicatives et la descente de gradient.
Nouveau contenu sur : résolution des récurrences, tables de hachage, fonctions de potentiel (analyse amortie), tableaux de suffixes.
Retirés de la 4e édition : tas de Fibonacci, arbres de van Emde Boas, géométrie algorithmique (toujours dans la 3e).
Implémentations en Python de tous les algorithmes, sur le site de MIT Press ; illustrations en couleur ; 140 nouveaux exercices et 22 nouveaux problèmes.
Recherche récente
Plus courts chemins (2025) : Duan, Mao, Mao, Shu et Yin (prix du meilleur article, STOC 2025) ont présenté un algorithme déterministe en O(m log^(2/3) n) pour les graphes orientés à poids positifs ou nuls. Il bat le O(m + n log n) de Dijkstra sur les graphes peu denses, dans le modèle « comparaison-addition ». C'est un résultat théorique : Dijkstra reste l'algorithme à apprendre et à utiliser.
Ce qui n'a pas changé
Grand O, dichotomie, tris, hachage, BFS, Dijkstra, programmation dynamique : les bases restent valables.
Corriger le code de ton PDF Grokking (Python 2 → 3)
print x devient print(x).
mid = (low + high) / 2 devient mid = (low + high) // 2 (en Python 3, / donne un décimal : erreur d'indice).
raw_input() devient input().
Depuis Python 3.7, un dict conserve l'ordre d'insertion.
Plan de révision en 4 semaines
Semaine 1 : fiches 1 à 4. Recoder dichotomie, liste chaînée, récursivité.
Semaine 2 : fiches 5 à 7. Recoder tri fusion, quicksort, BFS avec chemin.
Semaine 3 : fiches 8 à 10. Remplir à la main la grille du sac à dos.
Semaine 4 : fiches 11 à 14, puis refaire tous les exercices sans regarder.
Les points non sourcés ci-dessus (Python 3, Timsort, SHA-1, ECDH, lru_cache, heapq) relèvent de connaissances générales : à vérifier dans la documentation officielle si besoin.
Corrections par rapport aux versions précédentes
Cette version corrige les erreurs repérées lors de la revue : exemple de planification de la fiche gloutons, réponses « nombre d'étapes » de la dichotomie (formule exacte : floor(log2 n) + 1), mémoire du tri rapide, distinction NP-difficile / NP-complet, BFS qui reconstruit le chemin, KeyError sur les graphes, récursivité sur liste vide, titre « plus longue sous-chaîne ».
Revue du 10 novembre 2026 : tout le code a été réexécuté (Python 3.15) avec des tests ; corrections sur la hauteur d'un arbre (niveaux contre arêtes), le nombre de comparaisons du tri par sélection, l'usage de bisect_left, la définition du tas, un exemple de graphe pour Dijkstra, la limite de pile de lru_cache, et le corrigé de l'exercice compter (sans copie de liste).
Le Grand O décrit comment le nombre d'opérations (ou la mémoire) d'un algorithme grandit quand la taille des données grandit. Ce n'est pas un temps en secondes.
Explication
Pour chercher un nom dans une liste de n éléments :
Recherche simple : on regarde les éléments un par un, jusqu'à n essais → O(n).
Recherche dichotomique (liste triée) : on élimine la moitié à chaque étape → O(log n).
Sur 100 éléments la différence est minime ; sur 1 milliard, c'est 1 milliard d'étapes contre une trentaine.
Définition (un peu plus formelle)
T(n) = O(f(n)) signifie qu'à partir d'une certaine taille, T(n) ne dépasse pas un multiple constant de f(n).
On ignore les constantes : 3n, 100n et n sont tous O(n).
On ignore les termes secondaires : n² + 100n + 7 est O(n²).
Exemple : 2n² + 500n + 10 opérations → O(n²).
Les complexités courantes (du plus rapide au plus lent)
Notation
Nom
Exemple
O(1)
constante
lire liste[3]
O(log n)
logarithmique
recherche dichotomique
O(n)
linéaire
parcourir une liste
O(n log n)
quasi-linéaire
tri fusion, quicksort (moyenne)
O(n²)
quadratique
double boucle imbriquée, tri par sélection
O(2^n)
exponentielle
essayer tous les sous-ensembles
O(n!)
factorielle
tous les ordres possibles (10 villes : 3 628 800)
Les logarithmes simplement
log2 n répond à : « combien de fois diviser par 2 pour arriver à 1 ? ». Ex. 8 → 4 → 2 → 1 : log2 8 = 3.
n
log2 n
8
3
128
7
1 024
10
1 000 000
environ 20
1 000 000 000
environ 30
Précision utile : le nombre maximal d'étapes de la dichotomie sur n éléments est floor(log2 n) + 1. Ex. : 100 → 7 ; 128 → 8 ; 1 024 → 11. Dans le Grand O, cette différence de 1 disparaît.
Pire cas, meilleur cas, cas moyen
Pire cas : la situation la plus défavorable (c'est celle que le Grand O décrit par défaut).
Meilleur cas : la plus favorable (trouver tout de suite).
Cas moyen : le comportement typique.
Trois notations (CLRS ch. 3)
O : borne supérieure (« au plus »).
Ω (oméga) : borne inférieure (« au moins »).
Θ (thêta) : borne serrée (les deux à la fois).
Code : repérer la complexité
Python
defafficher(liste): # O(n) : une bouclefor element in liste:
print(element)
defcomparer_paires(liste): # O(n²) : deux boucles imbriquéesfor a in liste:
for b in liste:
print(a, b)
premier = [10, 20, 30][0] # O(1) : accès direct
Méthode
Repérer la taille de l'entrée. 2. Compter les boucles. 3. Repérer les boucles imbriquées. 4. Garder le terme dominant. 5. Ignorer les constantes. 6. Préciser le cas étudié.
Pièges
Croire que O(n) veut dire « n secondes ».
Croire que O(1) veut dire « rapide » : c'est « indépendant de n ».
Confondre log n (très lent à grandir) et n log n.
Oublier la mémoire, qui a aussi un Grand O.
Oublier que la constante compte parfois (c'est pourquoi quicksort bat souvent le tri fusion).
Exercices
Afficher le premier élément d'une liste.
Afficher tous les éléments.
Trouver le plus grand élément d'une liste non triée.
Le Grand O mesure la croissance, pas des secondes ; O(log n) est excellent, O(n) raisonnable, O(n²) vite pénible, O(n!) inutilisable dès 15 éléments environ.
Grokking ch. 1 · CLRS ch. 2 (exercices sur la recherche binaire)
L'idée en une phrase
Dans une liste triée, on regarde l'élément du milieu et on élimine la moitié qui ne peut pas contenir la cible, puis on recommence.
Principe
Regarder le milieu. 2. Si c'est la cible, renvoyer son indice. 3. Si la cible est plus petite, chercher à gauche. 4. Si elle est plus grande, chercher à droite. 5. Répéter jusqu'à trouver ou jusqu'à ce que la zone soit vide.
Exemple : chercher 7 dans [1, 3, 5, 7, 9] : milieu = 5 (< 7) → zone [7, 9] → milieu = 7 : trouvé à l'indice 3.
Code Python 3
Python
defrecherche_dichotomique(liste, cible):
bas, haut = 0, len(liste) - 1while bas <= haut:
milieu = (bas + haut) // 2# // et non / en Python 3
valeur = liste[milieu]
if valeur == cible:
return milieu
elif valeur < cible:
bas = milieu + 1else:
haut = milieu - 1returnNone
En pratique, le module standard bisect fait ce travail. Attention : bisect_left renvoie une position d'insertion, pas « trouvé / pas trouvé » ; il faut vérifier :
Python
from bisect import bisect_left
defchercher(liste, cible):
i = bisect_left(liste, cible)
return i if i < len(liste) and liste[i] == cible elseNone
Complexité
Cas
Complexité
Meilleur
O(1)
Pire / moyen
O(log n)
Mémoire
O(1)
Nombre maximal d'étapes : floor(log2 n) + 1.
Pièges
Liste non triée : le résultat est faux.
Oublier de mettre à jour bas ou haut (boucle infinie).
La façon de ranger les données en mémoire décide de ce qui est rapide ou lent.
Explication
La mémoire est une armoire à tiroirs numérotés.
Tableau : éléments côte à côte. L'adresse du i-ème se calcule directement : lecture instantanée. Insérer au milieu oblige à décaler les suivants. Tous les éléments ont le même type.
Liste chaînée : éléments dispersés, chacun contient l'adresse du suivant. Insérer ou supprimer = changer un lien. Pour atteindre le 100e, il faut passer par les 99 premiers.
Image de Grokking : un tableau, c'est réserver des places côte à côte au cinéma (impossible d'ajouter un ami si la rangée est pleine) ; une liste chaînée, c'est être dispersés dans la salle, chacun sachant où est le suivant.
Terminologie
Accès aléatoire : aller directement à l'élément i (tableaux).
Accès séquentiel : lire dans l'ordre (listes chaînées).
Code : liste chaînée
Python
classNoeud:
def__init__(self, valeur):
self.valeur = valeur
self.suivant = NoneclassListeChainee:
def__init__(self):
self.tete = Nonedefajouter_debut(self, valeur): # O(1)
nouveau = Noeud(valeur)
nouveau.suivant = self.tete
self.tete = nouveau
defrechercher(self, valeur): # O(n)
courant = self.tete
while courant isnotNone:
if courant.valeur == valeur:
return courant
courant = courant.suivant
returnNonedefsupprimer(self, valeur): # O(n) pour trouver, O(1) pour délier
precedent, courant = None, self.tete
while courant isnotNone:
if courant.valeur == valeur:
if precedent isNone:
self.tete = courant.suivant
else:
precedent.suivant = courant.suivant
returnTrue
precedent, courant = courant, courant.suivant
returnFalsedefvers_liste(self):
resultat, courant = [], self.tete
while courant isnotNone:
resultat.append(courant.valeur)
courant = courant.suivant
return resultat
Comparaison
Opération
Tableau
Liste chaînée
Accès à l'élément i
O(1)
O(n)
Recherche d'une valeur
O(n)
O(n)
Insertion au début
O(n)
O(1)
Suppression au début
O(n)
O(1)
Insertion / suppression au milieu
O(n)
O(1) si le nœud précédent est déjà connu
Quand choisir quoi ?
Accès fréquent par position, ou dichotomie → tableau.
Beaucoup d'insertions / suppressions en tête → liste chaînée.
Une fonction s'appelle elle-même sur un problème plus petit, jusqu'à atteindre un cas trivial.
Explication
Pour savoir si une boîte contient une clé : on l'ouvre ; si elle contient d'autres boîtes, on applique la même méthode à chacune.
Toute fonction récursive a deux parties :
Cas de base : cas si simple qu'on répond directement (la récursion s'arrête).
Cas récursif : on se ramène à un problème plus petit.
Python
deffactorielle(n):
if n <= 1: # cas de basereturn1return n * factorielle(n - 1) # cas récursifdefsomme(liste):
ifnot liste: # cas de base : liste videreturn0return liste[0] + somme(liste[1:])
Une pile est une structure LIFO (dernier entré, premier sorti) avec deux opérations : push (empiler) et pop (dépiler).
Chaque appel de fonction empile un cadre (variables locales, point de retour). Quand l'appel se termine, son cadre est dépilé.
Avec la récursivité, la pile peut devenir très haute : coût mémoire, et erreur RecursionError si on dépasse la limite (1 000 par défaut dans CPython).
Python n'optimise pas la récursion terminale.
Version sans copies (meilleure pratique)
liste[1:]copie la liste à chaque appel : somme devient O(n²). Avec des indices, on reste en O(n) :
Python
defsomme_indices(liste, i=0):
if i == len(liste):
return0return liste[i] + somme_indices(liste, i + 1)
defmaximum(liste, i=0):
ifnot liste:
raiseValueError("liste vide")
if i == len(liste) - 1:
return liste[i]
reste = maximum(liste, i + 1)
return liste[i] if liste[i] > reste else reste
Récursivité ou boucle ?
Situation
Souvent préférable
Parcourir une liste simple
boucle
Parcourir un arbre ou des dossiers imbriqués
récursivité
Sous-problèmes identiques plus petits
récursivité
Très grande profondeur
boucle
Pour aller plus loin (CLRS ch. 4)
Pour analyser un algorithme récursif, on écrit une récurrence : T(n) = 2T(n/2) + n (tri fusion) donne Θ(n log n). Le théorème maître résout T(n) = aT(n/b) + f(n). La 4e édition a retravaillé cette partie.
Pièges
Oublier le cas de base (récursion infinie).
Un cas récursif qui ne se rapproche jamais du cas de base.
Copier des listes à chaque appel sans y penser.
Ignorer la mémoire de la pile.
Exercices
Que vaut factorielle(4) et combien de cadres au maximum sur la pile ?
Écrire une fonction récursive qui compte les éléments d'une liste.
Pourquoi une fonction sans cas de base échoue-t-elle ?
Réponses : 1. 24 ; 4 cadres · 2. def compter(l, i=0): return 0 if i == len(l) else 1 + compter(l, i + 1) (avec des indices, sans copier la liste) · 3. elle s'appelle sans fin jusqu'au débordement de la pile (RecursionError).
Diviser pour régner : on découpe un problème en morceaux plus petits du même type, on les résout, puis on recombine. Trier, c'est réorganiser des éléments selon un ordre.
Diviser pour régner (Grokking ch. 4)
Trouver le cas de base (très simple : liste vide ou à un élément).
Réduire le problème jusqu'à l'atteindre.
Exemples : partager un terrain en carrés égaux les plus grands possibles (algorithme d'Euclide) ; additionner une liste = premier élément + somme du reste.
Comparaison des tris
Tri
Meilleur
Moyen
Pire
Mémoire en plus
Sélection
O(n²)
O(n²)
O(n²)
O(1)
Insertion
O(n)
O(n²)
O(n²)
O(1)
Fusion
O(n log n)
O(n log n)
O(n log n)
O(n)
Quicksort (en place, pivot aléatoire)
O(n log n)
O(n log n)
O(n²)
O(log n)
Quicksort (version « listes » ci-dessous)
O(n log n)
O(n log n)
O(n²)
O(n)
Heapsort (CLRS ch. 6)
O(n log n)
O(n log n)
O(n log n)
O(1)
Tri par sélection
On cherche le plus petit élément restant et on le place en tête.
Python
deftri_selection(liste):
for i inrange(len(liste) - 1):
indice_min = i
for j inrange(i + 1, len(liste)):
if liste[j] < liste[indice_min]:
indice_min = j
liste[i], liste[indice_min] = liste[indice_min], liste[i]
return liste
On insère chaque élément à sa place dans la partie déjà triée. Très rapide sur de petites listes ou des listes presque triées.
Python
deftri_insertion(liste):
for i inrange(1, len(liste)):
cle = liste[i]
j = i - 1while j >= 0and liste[j] > cle:
liste[j + 1] = liste[j]
j -= 1
liste[j + 1] = cle
return liste
Tri fusion
On coupe en deux, on trie chaque moitié, on fusionne. Toujours O(n log n).
Python
deftri_fusion(liste):
iflen(liste) <= 1:
return liste
milieu = len(liste) // 2return fusionner(tri_fusion(liste[:milieu]), tri_fusion(liste[milieu:]))
deffusionner(gauche, droite):
resultat, i, j = [], 0, 0while i < len(gauche) and j < len(droite):
if gauche[i] <= droite[j]:
resultat.append(gauche[i]); i += 1else:
resultat.append(droite[j]); j += 1
resultat.extend(gauche[i:])
resultat.extend(droite[j:])
return resultat
Quicksort (tri rapide)
Choisir un pivot. 2. Partitionner : plus petits à gauche, plus grands à droite. 3. Trier les deux côtés récursivement.
Version « listes » (lisible, mais O(n) de mémoire) :
Python
import random
deftri_rapide(liste):
iflen(liste) <= 1:
return liste
pivot = random.choice(liste) # pivot aléatoire
petits = [x for x in liste if x < pivot]
egaux = [x for x in liste if x == pivot]
grands = [x for x in liste if x > pivot]
return tri_rapide(petits) + egaux + tri_rapide(grands)
Version en place (O(log n) de pile) : on récurse sur le plus petit côté et on boucle sur l'autre.
Python
import random
deftri_rapide_en_place(a, bas=0, haut=None):
if haut isNone:
haut = len(a) - 1while bas < haut:
p = random.randint(bas, haut)
a[p], a[haut] = a[haut], a[p]
pivot, i = a[haut], bas
for j inrange(bas, haut):
if a[j] < pivot:
a[i], a[j] = a[j], a[i]
i += 1
a[i], a[haut] = a[haut], a[i]
if i - bas < haut - i:
tri_rapide_en_place(a, bas, i - 1)
bas = i + 1else:
tri_rapide_en_place(a, i + 1, haut)
haut = i - 1return a
Pourquoi un pivot aléatoire ?
Avec toujours le premier élément comme pivot, une liste déjà triée donne des partitions déséquilibrées (0 et n-1 éléments) : O(n²). Un pivot aléatoire rend ce cas très improbable : CLRS ch. 7 montre un temps moyen espéré de O(n log n) quelle que soit l'entrée. Attention : la version en place ci-dessus dégrade en O(n²) si tous les éléments sont égaux ; la version « listes » gère bien les doublons grâce à egaux.
Pourquoi quicksort bat souvent le tri fusion
Les deux sont O(n log n) en moyenne, mais la constante du quicksort est plus petite. Le Grand O ignore les constantes ; elles comptent pourtant.
Pour aller plus loin (CLRS)
Borne inférieure (ch. 8) : tout tri par comparaisons demande Ω(n log n).
On fait mieux (O(n)) seulement avec d'autres hypothèses : tri par dénombrement, par base (radix), par paquets.
Mise à jour pratique : sorted() et list.sort() de Python utilisent Timsort (hybride fusion + insertion, stable), pas quicksort. Dans un vrai projet, utilise-les.
Stable = conserve l'ordre des éléments égaux ; en place = peu de mémoire supplémentaire. Ce sont deux notions différentes.
Pièges
Pivot mal choisi (liste triée + pivot fixe).
Confondre « en place » et « stable ».
Croire que le tri par sélection est meilleur au meilleur cas : il reste O(n²).
Exercices
Pourquoi choisir le pivot au hasard ?
Quel tri garantit O(n log n) au pire ?
Quel est le meilleur cas du tri par insertion ?
Trier [5, 3, 6, 2, 10] avec chaque fonction ci-dessus.
Réponses : 1. éviter le pire cas sur des entrées particulières · 2. fusion et heapsort · 3. O(n), liste déjà triée · 4. [2, 3, 5, 6, 10].
Grokking ch. 5 · CLRS ch. 11 (nouveautés dans la 4e éd.)
L'idée en une phrase
Une table de hachage associe une clé à une valeur et retrouve la valeur en temps constant en moyenne.
Explication
Une fonction de hachage transforme une clé (mot, nom…) en un indice de tableau, comme un annuaire où le nom donne directement la page.
Une bonne fonction de hachage :
est déterministe (même clé, même indice, toujours) ;
répartit les clés uniformément dans le tableau ;
associe, autant que possible, des clés différentes à des cases différentes.
Cas d'usage (Grokking)
Associer une clé à une valeur (annuaire).
Éviter les doublons (« ai-je déjà vu cette valeur ? »).
Cache : mémoriser un résultat déjà calculé.
Python
annuaire = {"alice": "alice@exemple.fr", "bob": "bob@exemple.fr"}
annuaire["carole"] = "carole@exemple.fr"defcompter_mots(texte):
compteur = {}
for mot in texte.split():
compteur[mot] = compteur.get(mot, 0) + 1return compteur
defsans_doublons(liste):
vus, resultat = set(), []
for x in liste:
if x notin vus:
vus.add(x)
resultat.append(x)
return resultat
cache = {}
defcarre_lent(n): # exemple de cacheif n notin cache:
cache[n] = n * n # imagine un calcul coûteuxreturn cache[n]
Collisions et performance
Collision : deux clés tombent dans la même case.
Chaînage (Grokking) : chaque case contient une liste de paires clé-valeur.
Adressage ouvert : si la case est occupée, on cherche une autre case libre.
Facteur de charge = nombre d'éléments / nombre de cases. Grokking conseille de redimensionner quand il dépasse environ 0,7.
Opération
Moyen
Pire cas
Recherche
O(1)
O(n)
Insertion
O(1)
O(n)
Suppression
O(1)
O(n)
Le pire cas arrive quand tout s'entasse dans la même case (mauvaise fonction de hachage).
Pour aller plus loin (CLRS ch. 11)
Hachage universel : choisir la fonction au hasard dans une famille pour éviter les entrées « piégées ».
Hachage parfait : O(1) garanti au pire cas pour un ensemble de clés fixe.
Mise à jour : en Python, dict utilise l'adressage ouvert, pas le chaînage de Grokking. Depuis Python 3.7, il garde l'ordre d'insertion.
Pièges
Utiliser une clé modifiable : une list n'est pas hachable (utiliser un tuple).
Croire que O(1) veut dire « instantané ».
Chercher un ordre trié dans un dict : il garde l'ordre d'insertion, pas un ordre de tri.
Exercices
Pourquoi une table de hachage n'est-elle pas triée par clé ?
Que fait-on quand le facteur de charge est trop grand ?
Compter les mots de "a b a c b a".
Réponses : 1. la fonction de hachage disperse les clés · 2. on redimensionne · 3. {'a': 3, 'b': 2, 'c': 1}.
Mettre le départ dans une file. 2. Retirer le premier sommet. 3. Examiner ses voisins. 4. Ajouter à la file ceux pas encore vus. 5. Répéter jusqu'à trouver la cible ou vider la file.
La file garantit qu'on traite d'abord les voisins directs, puis les voisins des voisins : le premier chemin trouvé est donc le plus court en nombre d'arêtes.
Code : BFS qui renvoie le chemin
Python
from collections import deque
defplus_court_chemin(graphe, depart, cible):
parents = {depart: None}
file = deque([depart])
while file:
sommet = file.popleft()
if sommet == cible:
chemin = []
while sommet isnotNone:
chemin.append(sommet)
sommet = parents[sommet]
return chemin[::-1]
for voisin in graphe.get(sommet, []): # .get : pas de KeyErrorif voisin notin parents:
parents[voisin] = sommet
file.append(voisin)
returnNone
parents joue aussi le rôle de l'ensemble des sommets déjà vus.
Complexité
O(V + E) : chaque sommet et chaque arête sont examinés une fois.
Pour aller plus loin (CLRS ch. 22)
CLRS colore les sommets (blanc, gris, noir) et calcule les distances et prédécesseurs.
Parcours en profondeur (DFS) : explore à fond avant de revenir (pile ou récursivité). Il sert au tri topologique (ordonner des tâches avec dépendances) et aux composantes fortement connexes.
Liste d'adjacence (graphes peu denses) ou matrice d'adjacence (graphes denses).
Pièges
Oublier de marquer les sommets déjà vus : boucle infinie si le graphe a un cycle.
Utiliser une pile à la place de la file : on obtient un DFS, pas le plus court chemin.
Utiliser BFS avec des poids : il faut Dijkstra (fiche 8).
Exercices
Pile ou file pour BFS ?
Coût de BFS sur 1 000 sommets et 5 000 arêtes ?
Chemin de "vous" à "peggy" dans le graphe ci-dessus ?
Réponses : 1. file · 2. environ 6 000 opérations (V + E) · 3. ['vous', 'alice', 'peggy'].
Grokking ch. 7 (9 dans la 2e éd.) · CLRS ch. 24 et 25
L'idée en une phrase
Dans un graphe dont les arêtes ont un poids (temps, prix, distance), Dijkstra trouve le chemin de coût total minimal si tous les poids sont positifs ou nuls.
Exemple
Schéma
A -> B : 5 minutes
A -> C : 2 minutes
C -> B : 1 minute
A -> C -> B coûte 3 minutes, A -> B coûte 5 : Dijkstra choisit A -> C -> B.
Principe
Coût du départ = 0, tous les autres = infini.
Prendre le sommet non traité de coût minimal.
Mettre à jour ses voisins si passer par lui est moins cher (et noter le parent).
Répéter jusqu'à avoir traité tous les sommets accessibles.
Reconstruire le chemin avec les parents.
Grokking utilise trois tables de hachage : le graphe, les coûts et les parents, plus une liste des sommets traités.
Avec un poids négatif, Dijkstra peut se tromper : un sommet déjà « fini » pourrait encore être amélioré.
Dans un graphe non orienté, chaque arête est un cycle : sans importance tant que les poids sont positifs.
Bellman-Ford (poids négatifs)
Python
defbellman_ford(sommets, aretes, depart):
d = {s: float("inf") for s in sommets}
d[depart] = 0for _ inrange(len(sommets) - 1):
for u, v, w in aretes:
if d[u] + w < d[v]:
d[v] = d[u] + w
for u, v, w in aretes: # détection de cycle négatifif d[u] + w < d[v]:
raiseValueError("cycle de poids négatif")
return d
Complexité
Algorithme
Complexité
Dijkstra, tableau simple
O(V²)
Dijkstra, tas binaire (code ci-dessus)
O((V + E) log V)
Dijkstra, tas de Fibonacci (CLRS 3e éd. ch. 19, retiré de la 4e)
O(E + V log V)
Bellman-Ford
O(V · E)
Graphe orienté acyclique, ordre topologique
O(V + E) (même avec poids négatifs)
Floyd-Warshall, tous les couples (CLRS ch. 25)
O(V³)
Mise à jour 2025
Un algorithme déterministe en O(m log^(2/3) n) bat Dijkstra sur les graphes orientés peu denses (Duan et al., STOC 2025). Il n'a pas rendu Dijkstra obsolète : à connaître comme culture générale.
Pièges
Poids négatifs avec Dijkstra.
Oublier de mémoriser les parents : on a le coût mais pas le chemin.
Un sommet inaccessible reste absent de couts (ou à l'infini).
Exercices
BFS ou Dijkstra pour un métro où chaque trajet dure un temps différent ?
Grokking ch. 8 (10 dans la 2e éd.) · CLRS ch. 16, 34 et 35
L'idée en une phrase
Un algorithme glouton choisit à chaque étape l'option qui paraît la meilleure sur le moment, sans jamais revenir en arrière. Simple et rapide, il n'est pas toujours optimal.
Exemple qui marche : planning de réunions
On veut placer le plus de réunions sans chevauchement. Stratégie : prendre celle qui finit le plus tôt, puis la suivante compatible.
Réunion
Début
Fin
Art
9 h 00
10 h 00
Anglais
9 h 30
10 h 30
Maths
10 h 00
11 h 00
Info
10 h 30
11 h 30
Musique
11 h 00
12 h 00
Résultat : Art, Maths, Musique = 3 réunions, ce qui est le maximum ici.
Python
defplanning(reunions):
"""reunions : liste de (nom, debut, fin) ; heures en décimal (9.5 = 9 h 30)."""
choisies, fin_derniere = [], float("-inf")
for nom, debut, fin insorted(reunions, key=lambda r: r[2]):
if debut >= fin_derniere:
choisies.append(nom)
fin_derniere = fin
return choisies
Exemple qui échoue : sac à dos (0/1)
Objet
Poids
Valeur
Chaîne hi-fi
30 kg
3 000 €
Ordinateur
20 kg
2 000 €
Guitare
15 kg
1 500 €
Sac de 35 kg. Glouton « le plus cher d'abord » : hi-fi (30 kg), il reste 5 kg, rien d'autre ne rentre : 3 000 €. Mieux : ordinateur + guitare = 35 kg, 3 500 €. Le glouton n'est donc ici qu'une approximation. Pour le sac à dos 0/1, utilise la programmation dynamique (fiche 10). Pour le sac à dos fractionnable, trier par valeur/poids est optimal.
Couverture d'ensemble (Grokking)
Choisir le moins de stations de radio possible pour couvrir tous les États voulus : à chaque tour, prendre la station qui couvre le plus d'États pas encore couverts.
La solution est proche de l'optimum, pas garantie optimale.
Problèmes NP : vocabulaire précis
P : problèmes résolubles en temps polynomial (rapide).
NP : problèmes dont une solution proposée se vérifie rapidement.
NP-difficile : au moins aussi dur que tous les problèmes de NP (n'est pas forcément dans NP).
NP-complet : NP-difficile et dans NP. Aucun algorithme rapide connu ; savoir si P = NP reste une question ouverte (l'un des problèmes du prix du millénaire).
Exemples classiques : voyageur de commerce, couverture d'ensemble, couverture par sommets, SAT, sac à dos (version exacte).
Comment les repérer (Grokking)
Rapide avec peu d'éléments, très lent dès qu'on en ajoute ; on te demande « toutes les combinaisons de X » ; ça ressemble au voyageur de commerce ou à la couverture d'ensemble.
Que faire ?
Un algorithme d'approximation : rapide, proche de l'optimum. Un glouton est un bon candidat. Autres options : heuristiques, programmation dynamique sur des cas restreints.
Pour aller plus loin (CLRS)
Quand le glouton est-il correct ? Il faut la propriété de choix glouton et la sous-structure optimale (ch. 16). Exemples : sélection d'activités, codage de Huffman, arbres couvrants de poids minimal (Kruskal, Prim, ch. 23).
NP-complétude (ch. 34) : réductions polynomiales. Approximation (ch. 35) : par exemple, facteur 2 pour la couverture par sommets.
Le chapitre sur les gloutons a été remanié dans la 4e édition.
Pièges
Supposer qu'un glouton est optimal sans le démontrer (rendu de monnaie avec des pièces atypiques).
Confondre NP-difficile et NP-complet.
Chercher un algorithme exact rapide pour un problème NP-complet de grande taille.
Exercices
Le glouton est-il toujours optimal ?
Combien de réunions dans l'exemple ci-dessus ?
Que faire d'un problème NP-complet de grande taille ?
Réponses : 1. non · 2. 3 (Art, Maths, Musique) · 3. approximation ou heuristique.
On résout un problème difficile en le découpant en sous-problèmes dont on mémorise les réponses (dans une grille ou un cache) pour ne jamais les recalculer.
Quand l'utiliser ?
Sous-structure optimale : la solution optimale contient des solutions optimales de sous-problèmes.
Sous-problèmes qui se chevauchent : on les rencontre plusieurs fois.
Les sous-problèmes sont indépendants et discrets : on ne peut pas voler une fraction d'objet (dans ce cas, un glouton suffit).
Grille : chaque ligne ajoute un objet, chaque colonne est une capacité (1 kg, 2 kg…). Chaque case = meilleure valeur possible. case = max( case_du_dessus , valeur_objet + case_du_dessus_avec_capacité_restante )
Python
defsac_a_dos(poids, valeurs, capacite):
n = len(poids)
table = [[0] * (capacite + 1) for _ inrange(n + 1)]
for i inrange(1, n + 1):
for w inrange(capacite + 1):
table[i][w] = table[i - 1][w]
if poids[i - 1] <= w:
table[i][w] = max(table[i][w],
valeurs[i - 1] + table[i - 1][w - poids[i - 1]])
return table[n][capacite]
Complexité : O(n · W) (n objets, capacité W). C'est dit « pseudo-polynomial » : W est une valeur numérique, pas une taille de données.
Plus longue sous-chaîne commune (lettres consécutives)
Si les lettres correspondent : case = diagonale haut-gauche + 1 ; sinon 0. La réponse est le maximum de la grille.
Plus longue sous-séquence commune (lettres dans l'ordre, pas forcément consécutives)
Si correspondance : diagonale + 1 ; sinon max(case du haut, case de gauche). Utilisée par diff et en génétique.
Python
defplus_longue_sous_sequence(a, b):
t = [[0] * (len(b) + 1) for _ inrange(len(a) + 1)]
for i inrange(1, len(a) + 1):
for j inrange(1, len(b) + 1):
if a[i - 1] == b[j - 1]:
t[i][j] = t[i - 1][j - 1] + 1else:
t[i][j] = max(t[i - 1][j], t[i][j - 1])
return t[len(a)][len(b)]
Exemple : "fish" et "fosh" : sous-chaîne 2 (sh), sous-séquence 3 (fsh).
Deux styles (CLRS ch. 15)
Descendant avec mémoïsation : récursif + cache.
Ascendant : remplir la grille (comme ci-dessus).
Python
from functools import lru_cache
@lru_cache(maxsize=None)
deffib(n):
return n if n < 2else fib(n - 1) + fib(n - 2) # O(n) au lieu de O(2^n)
Limite : la version récursive reste soumise à la limite de pile de Python (fib(5000) lève RecursionError au premier appel) ; la version ascendante n'a pas ce problème.
Problèmes classiques de CLRS : découpe de barres, produit de matrices en chaîne, plus longue sous-séquence commune, arbres de recherche optimaux.
Pièges
Mal définir ce que représente une case.
Mauvais ordre de remplissage (une case dépend de cases déjà calculées).
Appliquer la programmation dynamique à un problème dont les sous-problèmes dépendent les uns des autres.
Exercices
Pourquoi Fibonacci naïf est-il lent ?
Glouton ou programmation dynamique pour un sac à dos fractionnable ?
Grokking ch. 10 (12 dans la 2e éd.) · CLRS 4e éd. (nouveau chapitre ML)
L'idée en une phrase
Pour classer ou prédire un nouvel élément, on regarde les k exemples connus les plus proches et on s'aligne sur eux.
Principe
Représenter chaque objet par des nombres : les caractéristiques (features). C'est l'extraction de caractéristiques.
Calculer la distance entre le nouvel objet et les exemples connus.
Prendre les k plus proches.
Classification : classe majoritaire. Régression : moyenne des valeurs des voisins.
Distance euclidienne : racine carrée de la somme des carrés des écarts. Grokking mentionne aussi la similarité cosinus, qui compare des directions plutôt que des tailles (utile quand les utilisateurs notent sur des échelles différentes).
Code Python 3
Python
import math
from collections import Counter
defdistance(a, b):
return math.sqrt(sum((x - y) ** 2for x, y inzip(a, b)))
defknn_classe(donnees, cible, k=3):
voisins = sorted(donnees, key=lambda e: distance(e["point"], cible))[:k]
return Counter(e["classe"] for e in voisins).most_common(1)[0][0]
defknn_regression(donnees, cible, k=3):
voisins = sorted(donnees, key=lambda e: distance(e["point"], cible))[:k]
returnsum(e["valeur"] for e in voisins) / len(voisins)
defnormaliser(points):
"""Ramène chaque caractéristique entre 0 et 1 (min-max)."""
colonnes = list(zip(*points))
mins = [min(c) for c in colonnes]
maxs = [max(c) for c in colonnes]
return [[(x - lo) / (hi - lo) if hi > lo else0.0for x, lo, hi inzip(p, mins, maxs)] for p in points]
donnees = [
{"point": [3, 104], "classe": "pamplemousse"},
{"point": [2, 100], "classe": "pamplemousse"},
{"point": [1, 81], "classe": "orange"},
{"point": [1, 75], "classe": "orange"},
]
print(knn_classe(donnees, [2, 95], k=3)) # pamplemousse
Choisir de bonnes caractéristiques
Elles doivent être liées à ce qu'on veut prédire et sans biais.
Normaliser : sinon une unité en kilomètres écrase une unité en grammes.
k : trop petit = sensible à un voisin atypique ; trop grand = on lisse trop. On le choisit en testant. Un k impair évite les égalités en classification à deux classes.
Applications citées dans Grokking
OCR (reconnaissance de caractères), filtre anti-spam (méthode bayésienne naïve), prédiction boursière (très difficile : le passé ne garantit pas l'avenir).
Mises à jour
Depuis 2016, le machine learning s'est surtout développé avec l'apprentissage profond. L'idée de chercher les plus proches voisins reste très présente sous forme d'embeddings et de recherche de voisins approchés dans les bases de données vectorielles.
CLRS 4e éd., chapitre 33 : k-means (regroupement non supervisé), pondérations multiplicatives (décisions en ligne) et descente de gradient (optimisation).
Limites
Lent avec beaucoup de données (il faut comparer à tous les exemples).
Sensible aux échelles et aux caractéristiques inutiles.
Le choix de k est important.
Exercices
Classification ou régression pour prédire un prix ?
Pourquoi normaliser les caractéristiques ?
Quelle classe pour [2, 95] avec les données ci-dessus et k = 3 ?
Réponses : 1. régression · 2. pour qu'aucune ne domine la distance · 3. pamplemousse (2 voisins pamplemousse sur 3).
Grokking 2e éd. ch. 7 et 8 (1re éd. : ch. 11) · CLRS ch. 6, 12, 13, 14, 18
Ton PDF ne traite les arbres que brièvement (chapitre « Where to go next »). Cette fiche comble ce manque, comme le fait la 2e édition.
L'idée en une phrase
Un arbre est une structure hiérarchique de nœuds reliés par des relations parent-enfant ; l'arbre binaire de recherche (ABR) garde ses données ordonnées pour chercher vite.
Vocabulaire
Racine (tout en haut), nœud, enfant, feuille (sans enfant), hauteur (nombre de niveaux du plus long chemin racine → feuille ; c'est ce que compte la fonction hauteur ci-dessous). Attention : CLRS compte plutôt les arêtes, soit un de moins (l'arbre ci-dessous a 3 niveaux, donc une hauteur de 2 au sens de CLRS).
Arbre binaire de recherche
Chaque nœud a au plus deux enfants. Règle : tout ce qui est à gauche est plus petit, tout ce qui est à droite est plus grand.
Si on insère des valeurs déjà triées, l'arbre dégénère en liste. Avec ce code récursif, un arbre très profond peut aussi provoquer une RecursionError.
Arbres équilibrés
Ils se réorganisent (rotations) pour garder h = O(log n). Les plus connus : AVL et rouge-noir (CLRS ch. 13 : hauteur au plus 2 log(n+1)).
Arbres augmentés (ch. 14) : on ajoute de l'information dans les nœuds (taille du sous-arbre) pour répondre à « quel est le k-ième plus petit ? ».
La bibliothèque standard de Python n'a pas d'arbre de recherche équilibré ; on utilise bisect sur une liste triée ou une bibliothèque tierce.
B-arbres (CLRS ch. 18)
Nœuds larges contenant beaucoup de clés : très peu d'accès disque. Utilisés dans les bases de données et systèmes de fichiers. Sur les machines actuelles, réduire le nombre d'accès lents compte autant que le nombre d'opérations.
Tas (heap, CLRS ch. 6)
Arbre binaire presque complet où chaque parent est inférieur ou égal (tas-min) ou supérieur ou égal (tas-max) à ses enfants. Trouver l'extrême : O(1) ; insérer / retirer : O(log n). Sert à la file de priorité (Dijkstra) et à heapsort.
Python
import heapq
tas = []
for x in (5, 1, 3):
heapq.heappush(tas, x)
print(heapq.heappop(tas)) # 1deftri_par_tas(liste):
tas = list(liste)
heapq.heapify(tas) # O(n)return [heapq.heappop(tas) for _ inrange(len(tas))]
Hachage ou arbre ?
Table de hachage : recherche O(1) en moyenne, pas d'ordre de tri.
Idées citées dans Grokking (ch. 11 de la 1re éd., 13 dans la 2e)
Index inversé : pour chaque mot, la liste des pages qui le contiennent (base des moteurs de recherche).
Transformée de Fourier : décompose un signal en fréquences (compression audio, JPEG). CLRS ch. 30 : FFT en O(n log n).
Algorithmes parallèles et MapReduce : map applique une fonction à chaque élément, reduce les combine ; utile pour répartir le travail sur plusieurs machines.
Filtres de Bloom et HyperLogLog : structures approximatives très économes en mémoire (appartenance, comptage d'éléments distincts).
Algorithmes SHA : empreintes de fichiers ou de mots de passe. Mise à jour sécurité : SHA-1 est considéré comme cassé (collisions publiques depuis 2017) ; préférer SHA-256 ou SHA-3. Pour stocker des mots de passe, utiliser un algorithme volontairement lent (bcrypt, scrypt, Argon2), pas un simple SHA.
Hachage sensible à la localité : les objets similaires obtiennent des empreintes proches.
Diffie-Hellman : accord sur une clé secrète via un canal public. Aujourd'hui, on utilise surtout ses variantes sur courbes elliptiques (ECDH).
Programmation linéaire : optimiser un objectif sous contraintes (CLRS ch. 29, simplexe).
Chapitres de CLRS absents de Grokking
Analyse amortie (ch. 17) : coût moyen sur une suite d'opérations (ex. ajout à un tableau dynamique).