Cours Algorithmes Complet 🧠

Les 12 chapitres essentiels — complexité, tri, recherche, récursion, graphes et programmation dynamique

12
Chapitres
60+
Algorithmes
Python
Langage
A1→C2
Niveaux

CHAPITRE 01

Introduction et complexité

🧠 Qu'est-ce qu'un algorithme ?

Un algorithme est une suite finie d'instructions qui transforme une entrée en sortie pour résoudre un problème. Une recette de cuisine est un algorithme : des ingrédients (entrée), des étapes (instructions), un plat (sortie). En informatique, on mesure la qualité d'un algorithme par deux critères : sa vitesse (temps) et sa consommation mémoire (espace). Deux algorithmes peuvent résoudre le même problème — l'un en 1 seconde, l'autre en 1 heure. Savoir choisir le bon algorithme est la compétence fondamentale du développeur.

⏱️ Pourquoi la complexité compte
# Problème : trouver si un nombre existe dans une liste

# Approche 1 : Recherche linéaire — O(n)
# On parcourt chaque élément un par un
def recherche_lineaire(liste, cible):
for element in liste:
if element == cible:
return True
return False

# Approche 2 : Recherche binaire — O(log n)
# On divise la liste en deux à chaque étape (liste triée)
def recherche_binaire(liste, cible):
gauche, droite = 0, len(liste) 1
while gauche <= droite:
milieu = (gauche + droite) // 2
if liste[milieu] == cible:
return True
elif liste[milieu] < cible:
gauche = milieu + 1
else:
droite = milieu 1
return False

# Sur 1 milliard d'éléments :
# Recherche linéaire : jusqu'à 1 000 000 000 comparaisons
# Recherche binaire : maximum 30 comparaisons (log₂ de 10⁹)

CHAPITRE 02

Notation Big O

📊 Les complexités à connaître
Big O Nom n = 1M Exemple Verdict
O(1) Constant 1 op Accès array par index, hashmap get 🟢 Idéal
O(log n) Logarithmique 20 ops Recherche binaire, arbre BST 🟢 Excellent
O(n) Linéaire 1M ops Parcours de liste, recherche linéaire 🟢 Bon
O(n log n) Linéarithmique 20M ops Merge sort, quick sort, Tim sort 🟡 Correct
O(n²) Quadratique 10¹² ops Bubble sort, 2 boucles imbriquées 🔴 Lent
O(2ⁿ) Exponentiel Fibonacci récursif naïf, sous-ensembles 🔴 Inutilisable
O(n!) Factoriel Permutations, voyageur de commerce brut 🔴 Impossible
📐 Règles de calcul
# Règle 1 : On garde le terme dominant
# O(n² + n + 1000) → O(n²) — n² domine quand n est grand

# Règle 2 : On ignore les constantes
# O(3n) → O(n) — la constante 3 disparaît

# Règle 3 : Boucles imbriquées = multiplication
for i in range(n): # O(n)
for j in range(n): # × O(n)
print(i, j) # = O(n²)

# Règle 4 : Boucles séquentielles = addition
for i in range(n): # O(n)
print(i)
for j in range(m): # + O(m)
print(j) # = O(n + m)

# Règle 5 : Diviser par 2 à chaque itération = O(log n)
while n > 1:
n = n // 2 # O(log n)

# Complexité spatiale (mémoire)
# O(1) = pas de mémoire supplémentaire (in-place)
# O(n) = copie de la liste (ex: merge sort)
# O(n²) = matrice n×n

CHAPITRE 03

Recherche

🔍 Recherche linéaire vs binaire
# RECHERCHE LINÉAIRE — O(n) — liste non triée
def recherche_lineaire(arr, cible):
for i, val in enumerate(arr):
if val == cible:
return i
return 1

# RECHERCHE BINAIRE — O(log n) — ⚠️ liste TRIÉE obligatoire
def recherche_binaire(arr, cible):
g, d = 0, len(arr) 1
while g <= d:
m = (g + d) // 2
if arr[m] == cible:
return m
elif arr[m] < cible:
g = m + 1
else:
d = m 1
return 1

# Variante : bisect (Python standard library)
import bisect
i = bisect.bisect_left(arr, cible) # Position d'insertion
found = i < len(arr) and arr[i] == cible

# Variante : trouver le premier élément ≥ cible
def lower_bound(arr, cible):
g, d = 0, len(arr)
while g < d:
m = (g + d) // 2
if arr[m] < cible:
g = m + 1
else:
d = m
return g

La recherche binaire est l'algorithme le plus important à maîtriser. Elle divise l'espace de recherche en deux à chaque itération. Applicable partout : tableaux triés, recherche de seuil, minimiser/maximiser une valeur, trouver une racine. Piège classique : (g + d) // 2 peut overflow en Java/C++ — utiliser g + (d – g) // 2.

CHAPITRE 04

Tri basique

🔄 Tri par sélection, insertion et à bulles
# TRI PAR SÉLECTION — O(n²) — simple mais lent
# Idée : trouver le minimum, le placer au début, répéter
def tri_selection(arr):
for i in range(len(arr)):
min_idx = i
for j in range(i + 1, len(arr)):
if arr[j] < arr[min_idx]:
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i]

# TRI PAR INSERTION — O(n²) pire cas, O(n) meilleur cas
# Idée : insérer chaque élément à sa bonne place
# ✅ Très rapide sur des données presque triées
def tri_insertion(arr):
for i in range(1, len(arr)):
cle = arr[i] j = i 1
while j >= 0 and arr[j] > cle:
arr[j + 1] = arr[j] j -= 1
arr[j + 1] = cle

# TRI À BULLES — O(n²) — le plus simple mais le plus lent
# Idée : comparer les éléments adjacents, échanger si mal placés
def tri_bulle(arr):
n = len(arr)
for i in range(n):
swapped = False
for j in range(0, n i 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j] swapped = True
if not swapped: # Optimisation : arrêter si déjà trié
break

Ces algorithmes O(n²) sont pédagogiques mais inutilisables en production sur de grandes données. Utilisez-les seulement pour comprendre les bases du tri. En pratique, utilisez les algorithmes O(n log n) du chapitre suivant. Seul le tri par insertion reste utile : il est optimal sur les petites listes (< 50 éléments) et les données presque triées — c'est pour ça que Tim sort (Python) l'utilise en interne.

CHAPITRE 05

Tri avancé

⚡ Merge sort — O(n log n) garanti
# MERGE SORT — Diviser pour régner
# 1. Diviser la liste en deux moitiés
# 2. Trier chaque moitié récursivement
# 3. Fusionner les deux moitiés triées

def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
gauche = merge_sort(arr[:mid])
droite = merge_sort(arr[mid:])
return fusionner(gauche, droite)

def fusionner(g, d):
resultat = [] i = j = 0
while i < len(g) and j < len(d):
if g[i] <= d[j]: # <= pour la stabilité
resultat.append(g[i]); i += 1
else:
resultat.append(d[j]); j += 1
resultat.extend(g[i:])
resultat.extend(d[j:])
return resultat

⚡ Quick sort — O(n log n) moyen, O(n²) pire cas
# QUICK SORT — Diviser autour d'un pivot
# 1. Choisir un pivot
# 2. Placer les éléments < pivot à gauche, > pivot à droite
# 3. Récursion sur chaque côté

def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2] # Pivot au milieu (évite O(n²) trié)
gauche = [x for x in arr if x < pivot] milieu = [x for x in arr if x == pivot] droite = [x for x in arr if x > pivot] return quick_sort(gauche) + milieu + quick_sort(droite)

Algorithme Temps moyen Pire cas Espace Stable Usage
Merge sort O(n log n) O(n log n) O(n) ✅ Oui Données sur disque, listes chaînées
Quick sort O(n log n) O(n²) O(log n) ❌ Non Arrays en mémoire (le plus rapide en pratique)
Tim sort O(n log n) O(n log n) O(n) ✅ Oui Python sorted(), Java Arrays.sort()
Heap sort O(n log n) O(n log n) O(1) ❌ Non Mémoire limitée (in-place garanti)
Counting sort O(n + k) O(n + k) O(k) ✅ Oui Petites valeurs entières (0 à k)

En pratique, utilisez simplement sorted() en Python ou .sort() en JavaScript. Ces fonctions utilisent Tim sort (hybride merge sort + insertion sort), optimal dans 99% des cas. Comprenez merge sort et quick sort pour les entretiens techniques et pour savoir pourquoi les tris naïfs O(n²) sont mauvais.

CHAPITRE 06

Récursion

🔁 Penser récursivement
# Toute récursion a 2 parties :
# 1. CAS DE BASE — quand s'arrêter (sinon boucle infinie)
# 2. CAS RÉCURSIF — appeler la fonction sur un sous-problème

# Factorielle : n! = n × (n-1)!
def factorielle(n):
if n <= 1: # Cas de base
return 1
return n * factorielle(n 1) # Cas récursif

# Fibonacci NAÏF — O(2ⁿ) ❌ TERRIBLE
def fib_naif(n):
if n <= 1: return n
return fib_naif(n1) + fib_naif(n2) # Recalcule tout !

# Fibonacci avec MÉMOÏSATION — O(n) ✅
from functools import lru_cache

@lru_cache(maxsize=None)
def fib_memo(n):
if n <= 1: return n
return fib_memo(n1) + fib_memo(n2)

# Fibonacci ITÉRATIF — O(n) temps, O(1) espace ✅✅
def fib_iter(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a

Fibonacci illustre le piège de la récursion naïve : fib(50) naïf prend des millions d'années car il recalcule les mêmes valeurs des milliards de fois. fib_memo(50) prend des microsecondes. Si vous voyez une récursion avec des sous-problèmes qui se répètent → c'est de la programmation dynamique (chapitre 8).

CHAPITRE 07

Algorithmes de graphes

🕸️ Représenter un graphe
# Un graphe = des nœuds (sommets) + des arêtes (connexions)
# Ex : réseau social, carte routière, dépendances de packages

# Représentation : liste d'adjacence (la plus courante)
graphe =

# BFS — Parcours en largeur — O(V + E)
# Explore niveau par niveau (file/queue)
# ✅ Chemin le plus court (graphe non pondéré)
from collections import deque

def bfs(graphe, depart):
visite = set()
queue = deque([depart])
visite.add(depart)
while queue:
noeud = queue.popleft()
print(noeud, end= » « )
for voisin in graphe[noeud]:
if voisin not in visite:
visite.add(voisin)
queue.append(voisin)

# DFS — Parcours en profondeur — O(V + E)
# Explore le plus loin possible (pile/stack)
# ✅ Détection de cycles, tri topologique
def dfs(graphe, noeud, visite=None):
if visite is None:
visite = set()
visite.add(noeud)
print(noeud, end= » « )
for voisin in graphe[noeud]:
if voisin not in visite:
dfs(graphe, voisin, visite)

🗺️ Dijkstra — Chemin le plus court pondéré
# Dijkstra — O((V + E) log V) avec heap
# ✅ Chemin le plus court dans un graphe PONDÉRÉ (poids ≥ 0)
import heapq

def dijkstra(graphe, depart):
distances =
distances[depart] = 0
heap = [(0, depart)] # (distance, noeud)

while heap:
dist, noeud = heapq.heappop(heap)
if dist > distances[noeud]:
continue
for voisin, poids in graphe[noeud]:
nouvelle_dist = dist + poids
if nouvelle_dist < distances[voisin]:
distances[voisin] = nouvelle_dist
heapq.heappush(heap, (nouvelle_dist, voisin))
return distances

# Graphe pondéré (noeud → [(voisin, poids)])
graphe_p =
print(dijkstra(graphe_p, « A »))
# →

CHAPITRE 08

Programmation dynamique

🧩 Le principe

La programmation dynamique (DP) résout un problème complexe en le décomposant en sous-problèmes qui se chevauchent. Au lieu de recalculer les mêmes résultats (comme Fibonacci naïf), on les stocke dans un tableau. Deux approches : top-down (récursion + mémoïsation) ou bottom-up (itératif, remplir un tableau du plus petit au plus grand). La DP transforme des problèmes exponentiels O(2ⁿ) en polynomiaux O(n²) ou O(n).

🎒 Problème du sac à dos (Knapsack)
# Sac à dos 0/1 : maximiser la valeur sans dépasser le poids
# Objets : [(poids, valeur)]
# Capacité du sac : W

def knapsack(objets, W):
n = len(objets)
# dp[i][w] = valeur max avec les i premiers objets et capacité w
dp = [[0] * (W + 1) for _ in range(n + 1)]

for i in range(1, n + 1):
poids, valeur = objets[i 1] for w in range(W + 1):
if poids > w:
dp[i][w] = dp[i1][w] # Ne prend pas l'objet
else:
dp[i][w] = max(
dp[i1][w], # Ne prend pas
dp[i1][w poids] + valeur # Prend l'objet
)
return dp[n][W]

objets = [(2, 3), (3, 4), (4, 5), (5, 6)] print(knapsack(objets, 8)) # → 10

📏 Plus longue sous-séquence commune (LCS)
# LCS : trouver la plus longue sous-séquence commune à 2 strings
# « ABCDE » et « ACE » → « ACE » (longueur 3)
# Utilisé dans : diff (git), bioinformatique, correcteurs

def lcs(s1, s2):
m, n = len(s1), len(s2)
dp = [[0] * (n + 1) for _ in range(m + 1)]

for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i1] == s2[j1]:
dp[i][j] = dp[i1][j1] + 1
else:
dp[i][j] = max(dp[i1][j], dp[i][j1])
return dp[m][n]

CHAPITRE 09

Algorithmes gloutons

🤑 Le principe glouton (greedy)
# Glouton = à chaque étape, faire le choix localement optimal
# Rapide et simple, mais ne donne PAS toujours la solution optimale

# RENDU DE MONNAIE — Glouton fonctionne avec les pièces standard
def rendu_monnaie(montant, pieces=[200, 100, 50, 20, 10, 5, 2, 1]):
resultat = [] for piece in pieces:
while montant >= piece:
resultat.append(piece)
montant -= piece
return resultat

print(rendu_monnaie(167)) # → [100, 50, 10, 5, 2]

# PLANIFICATION D'ACTIVITÉS — trier par fin, prendre si compatible
def activites_max(activites):
# activites = [(debut, fin), …]
activites.sort(key=lambda x: x[1]) # Trier par heure de fin
resultat = [activites[0]] for debut, fin in activites[1:]:
if debut >= resultat[1][1]: # Pas de chevauchement
resultat.append((debut, fin))
return resultat

Glouton ≠ toujours optimal. Le rendu de monnaie avec les pièces [1, 3, 4] : pour 6, le glouton donne [4, 1, 1] (3 pièces) mais l'optimal est [3, 3] (2 pièces). Quand le glouton ne marche pas → programmation dynamique. Quand il marche, c'est plus rapide et plus simple que la DP.

CHAPITRE 10

Backtracking

↩️ Explorer et revenir en arrière
# Backtracking = explorer toutes les possibilités
# Si un chemin ne mène nulle part → revenir en arrière (backtrack)

# PERMUTATIONS — générer toutes les permutations
def permutations(arr):
resultat = [] def backtrack(chemin, restant):
if not restant:
resultat.append(chemin[:])
return
for i in range(len(restant)):
chemin.append(restant[i])
backtrack(chemin, restant[:i] + restant[i+1:])
chemin.pop() # ← Backtrack !
backtrack([], arr)
return resultat

# N-QUEENS — placer N reines sans conflit
def n_queens(n):
solutions = [] def backtrack(row, cols, diag1, diag2, board):
if row == n:
solutions.append([r[:] for r in board])
return
for col in range(n):
if col in cols or (rowcol) in diag1 or (row+col) in diag2:
continue
board[row][col] = « Q »
backtrack(row+1, cols|, diag1|, diag2|, board)
board[row][col] = « . » # ← Backtrack
board = [[« . »] * n for _ in range(n)] backtrack(0, set(), set(), set(), board)
return solutions

Le backtracking est la base de la résolution de puzzles : Sudoku, N-Queens, labyrinthe, mots croisés. Le pattern est toujours le même : faire un choix → explorer → annuler le choix (backtrack). Souvent O(n!) ou O(2ⁿ) — on accélère avec du pruning (élagage) en coupant les branches impossibles le plus tôt possible.

CHAPITRE 11

Entretiens techniques

💼 Patterns les plus fréquents
Pattern Quand l'utiliser Exemple
Two Pointers Array trié, paire avec somme = cible Two Sum (array trié), palindrome
Sliding Window Sous-array/substring contigu(ë) optimal(e) Max sum subarray, longest substring
HashMap/Set Lookup O(1), comptage, doublons Two Sum, anagrammes, fréquences
Binary Search Donnée triée, chercher un seuil Search in rotated array, Koko bananas
BFS / DFS Graphes, arbres, matrices Nombre d'îles, chemin le plus court
Dynamic Programming Sous-problèmes qui se chevauchent Climbing stairs, coin change, LCS
Backtracking Explorer toutes les combinaisons Permutations, N-Queens, Sudoku
Stack/Queue Parenthèses, monotone stack Valid parentheses, next greater element
🔧 Two Pointers et Sliding Window
# TWO POINTERS — Two Sum sur array trié — O(n)
def two_sum_sorted(arr, cible):
g, d = 0, len(arr) 1
while g < d:
s = arr[g] + arr[d] if s == cible:
return [g, d] elif s < cible:
g += 1
else:
d -= 1
return []

# SLIDING WINDOW — Somme max d'un sous-array de taille k — O(n)
def max_sum_k(arr, k):
window = sum(arr[:k])
max_sum = window
for i in range(k, len(arr)):
window += arr[i] arr[i k] # Glisser la fenêtre
max_sum = max(max_sum, window)
return max_sum

CHAPITRE 12

Bonnes pratiques

🗺️ Comment choisir le bon algorithme
Problème Technique Complexité
Chercher dans une liste triée Recherche binaire O(log n)
Trier des données sorted() / Tim sort O(n log n)
Chercher un élément HashMap / Set O(1) amorti
Chemin le plus court (non pondéré) BFS O(V + E)
Chemin le plus court (pondéré) Dijkstra O((V+E) log V)
Sous-problèmes qui se chevauchent Programmation dynamique Variable
Choix localement optimal suffit Glouton O(n log n) souvent
Explorer toutes les combinaisons Backtracking O(2ⁿ) ou O(n!)
Sous-array/substring contigu Sliding window O(n)
Paire dans un array trié Two pointers O(n)
✅ Bonnes pratiques

✅ À FAIRE
• Analyser la complexité AVANT de coder
• Commencer par la solution brute force
• Identifier le pattern (two pointers, DP, BFS…)
• Tester avec des cas limites (vide, 1 élément, max)
• Utiliser les structures de données standards (dict, set, deque)
• sorted() en Python = Tim sort O(n log n)
• bisect pour la recherche binaire
• heapq pour les files de priorité
• Apprendre les 8 patterns d'entretien
• Pratiquer sur LeetCode / HackerRank

❌ À ÉVITER
• Optimiser prématurément (d'abord correct, puis rapide)
• Boucles imbriquées inutiles (O(n²) → O(n) avec hashmap)
• Récursion sans cas de base (stack overflow)
• Fibonacci naïf O(2ⁿ) (utiliser mémoïsation/itératif)
• Tri à bulles en production (sorted() est toujours mieux)
• Ignorer la complexité spatiale (O(n²) mémoire = crash)
• Copier des listes dans une boucle (O(n²) caché)
• DFS récursif sur des données très profondes (pile Python limitée)
• Supposer que l'input est valide sans vérifier
• Confondre stable/instable (order matters)

🧠 Quiz
Quelle est la complexité de rechercher un élément dans un dictionnaire Python (dict) ?
O(1) amorti. Un dictionnaire Python est une hash table. La clé est hashée pour obtenir directement l'index en mémoire — pas besoin de parcourir. C'est pour ça que if x in mon_dict est O(1) alors que if x in ma_liste est O(n). Utilisez un set ou un dict dès que vous cherchez souvent si un élément existe.
Glouton vs Programmation Dynamique — comment choisir ?
Glouton : à chaque étape, faire le meilleur choix local sans revenir en arrière. Rapide (souvent O(n log n)), mais ne donne pas toujours l'optimal. DP : explorer tous les sous-problèmes et stocker les résultats. Plus lent mais garanti optimal. Règle : si le glouton peut être prouvé optimal (par échange argument ou matroïde) → glouton. Sinon → DP.
BFS vs DFS — quand utiliser lequel ?
BFS (file) : explore niveau par niveau. Trouve le chemin le plus court dans un graphe non pondéré. Utilise plus de mémoire (stocke tout le niveau courant). DFS (pile/récursion) : explore en profondeur. Détecte les cycles, tri topologique, composantes connexes. Utilise moins de mémoire. Pour un labyrinthe « existe-t-il un chemin ? » → DFS. Pour « quel est le plus court chemin ? » → BFS.

Cours Algorithmes Complet — Complexité, tri, recherche, graphes, DP et entretiens techniques

Référence : LeetCode | NeetCode | VisuAlgo