Algorithm e
Algorithm e
Total 24h
Page 1 / 22
Module 1 — Fondamentaux : complexité et notation Big
O
Résumé de la leçon
Avant d'étudier un algorithme de tri, il faut savoir comment mesurer son efficacité. La complexité algorithmique
décrit comment le temps d'exécution (complexité temporelle) et la mémoire utilisée (complexité spatiale) évoluent en
fonction de la taille n des données. On utilise la notation Big O pour exprimer le pire des cas.
O(1) — constant
O(log n) — logarithmique
O(n) — linéaire
On distingue aussi la stabilité d'un tri (deux éléments égaux gardent-ils leur ordre relatif ?) et le fait qu'il soit en place
(utilise-t-il peu de mémoire supplémentaire, O(1) ou O(log n) ?).
✏️Exercices
# A
for i in range(n):
print(i)
# B
for i in range(n):
for j in range(n):
print(i, j)
# C
i = 1
while i < n:
i = i * 2
✅ Réponse 1.1
✅ Réponse 1.2
import time
Page 2 / 22
resultat = fonction(*args)
fin = time.perf_counter()
print(f"Temps d'exécution : {(fin - debut) * 1000:.4f} ms")
return resultat
# Exemple d'utilisation
def somme_liste(liste):
return sum(liste)
mesurer_temps(somme_liste, list(range(1000000)))
✅ Réponse 1.3
import bisect
import time
liste = list(range(100_000))
cible = 99_999
debut = time.perf_counter()
recherche_lineaire(liste, cible)
print("Linéaire :", time.perf_counter() - debut, "s")
debut = time.perf_counter()
recherche_binaire(liste, cible)
print("Binaire :", time.perf_counter() - debut, "s")
☑ À la fin de ce module, vous devez savoir lire un algorithme et estimer sa complexité en O(1), O(log n), O(n), O(n log n)
ou O(n²), et savoir mesurer un temps d'exécution en Python.
Page 3 / 22
Module 2 — Tri à bulles (Bubble Sort)
Résumé de la leçon
Le tri à bulles parcourt la liste plusieurs fois en comparant deux éléments adjacents et en les échangeant s'ils sont dans
le mauvais ordre. À chaque passage, le plus grand élément restant "remonte" (comme une bulle) vers sa position finale.
C'est l'algorithme le plus simple à comprendre, mais aussi l'un des moins efficaces.
def bubble_sort(liste):
n = len(liste)
for i in range(n - 1):
echange = False
for j in range(n - 1 - i):
if liste[j] > liste[j + 1]:
liste[j], liste[j + 1] = liste[j + 1], liste[j]
echange = True
if not echange: # optimisation : liste déjà triée
break
return liste
✏️Exercices
✅ Réponse 2.1
def bubble_sort_simple(liste):
n = len(liste)
for i in range(n - 1):
for j in range(n - 1 - i):
if liste[j] > liste[j + 1]:
liste[j], liste[j + 1] = liste[j + 1], liste[j]
return liste
print(bubble_sort_simple([5, 1, 4, 2, 8]))
# [1, 2, 4, 5, 8]
✅ Réponse 2.2
def bubble_sort_decroissant(liste):
n = len(liste)
nb_echanges = 0
for i in range(n - 1):
for j in range(n - 1 - i):
if liste[j] < liste[j + 1]:
liste[j], liste[j + 1] = liste[j + 1], liste[j]
nb_echanges += 1
return liste, nb_echanges
Page 4 / 22
print(resultat, "-> échanges :", echanges)
# [8, 5, 4, 2, 1] -> échanges : 6
✅ Réponse 2.3
def bubble_sort_avec_comptage(liste):
n = len(liste)
passages = 0
for i in range(n - 1):
echange = False
passages += 1
for j in range(n - 1 - i):
if liste[j] > liste[j + 1]:
liste[j], liste[j + 1] = liste[j + 1], liste[j]
echange = True
if not echange:
break
return liste, passages
print(bubble_sort_avec_comptage([1, 2, 3, 4, 5]))
# ([1, 2, 3, 4, 5], 1) -> un seul passage suffit
print(bubble_sort_avec_comptage([5, 4, 3, 2, 1]))
# ([1, 2, 3, 4, 5], 4) -> il faut n-1 passages
☑ À la fin de ce module, vous devez savoir implémenter le tri à bulles, l'optimiser avec un drapeau d'arrêt, et expliquer
pourquoi sa complexité est O(n²) dans le pire cas.
Page 5 / 22
Module 3 — Tri par sélection (Selection Sort)
Résumé de la leçon
Le tri par sélection divise la liste en une partie triée (au début) et une partie non triée (à la fin). À chaque itération, il
cherche le minimum de la partie non triée et l'échange avec le premier élément non trié. Contrairement au tri à bulles,
le nombre d'échanges est minimal (au plus n-1), mais le nombre de comparaisons reste toujours O(n²), même si la liste
est déjà triée.
def selection_sort(liste):
n = len(liste)
for i in range(n - 1):
indice_min = i
for j in range(i + 1, n):
if liste[j] < liste[indice_min]:
indice_min = j
liste[i], liste[indice_min] = liste[indice_min], liste[i]
return liste
✏️Exercices
✅ Réponse 3.1
def selection_sort(liste):
n = len(liste)
for i in range(n - 1):
indice_min = i
for j in range(i + 1, n):
if liste[j] < liste[indice_min]:
indice_min = j
liste[i], liste[indice_min] = liste[indice_min], liste[i]
return liste
✅ Réponse 3.2
def selection_sort_par_max(liste):
n = len(liste)
for i in range(n - 1, 0, -1):
indice_max = i
for j in range(i):
if liste[j] > liste[indice_max]:
indice_max = j
liste[i], liste[indice_max] = liste[indice_max], liste[i]
return liste
Page 6 / 22
print(selection_sort_par_max([64, 25, 12, 22, 11]))
# [11, 12, 22, 25, 64]
✅ Réponse 3.3
def selection_sort_comptage(liste):
n = len(liste)
comparaisons = 0
for i in range(n - 1):
indice_min = i
for j in range(i + 1, n):
comparaisons += 1
if liste[j] < liste[indice_min]:
indice_min = j
liste[i], liste[indice_min] = liste[indice_min], liste[i]
return liste, comparaisons
☑ À la fin de ce module, vous devez savoir pourquoi le tri par sélection fait toujours O(n²) comparaisons, même sur une
liste déjà triée, contrairement au tri à bulles optimisé.
Page 7 / 22
Module 4 — Tri par insertion (Insertion Sort)
Résumé de la leçon
Le tri par insertion construit la liste triée un élément à la fois : il prend chaque élément de la partie non triée et l'insère
à sa bonne place dans la partie déjà triée, en décalant les éléments plus grands. C'est la méthode qu'on utilise
naturellement pour trier des cartes à jouer dans sa main. Très efficace sur des petites listes ou des listes presque triées.
def insertion_sort(liste):
for i in range(1, len(liste)):
cle = liste[i]
j = i - 1
while j >= 0 and liste[j] > cle:
liste[j + 1] = liste[j]
j -= 1
liste[j + 1] = cle
return liste
✏️Exercices
✅ Réponse 4.1
def insertion_sort(liste):
for i in range(1, len(liste)):
cle = liste[i]
j = i - 1
while j >= 0 and liste[j] > cle:
liste[j + 1] = liste[j]
j -= 1
liste[j + 1] = cle
return liste
✅ Réponse 4.2
import bisect
Page 8 / 22
Exercice 4.3 — Tri par insertion binaire
Améliorez le tri par insertion en utilisant une recherche binaire (au lieu d'une recherche linéaire) pour trouver la
position d'insertion, ce qui réduit le nombre de comparaisons (le nombre de déplacements reste O(n) cependant).
✅ Réponse 4.3
import bisect
def insertion_sort_binaire(liste):
for i in range(1, len(liste)):
cle = liste[i]
# recherche binaire de la position d'insertion dans liste[0:i]
pos = bisect.bisect_left(liste, cle, 0, i)
# décaler les éléments pour libérer la place
liste[pos + 1:i + 1] = liste[pos:i]
liste[pos] = cle
return liste
☑ À la fin de ce module, vous devez savoir implémenter le tri par insertion classique et sa variante binaire, et expliquer
pourquoi ce tri est très performant sur des données presque triées.
Page 9 / 22
Module 5 — Tri fusion (Merge Sort)
Résumé de la leçon
Le tri fusion applique le paradigme "diviser pour régner" : on divise récursivement la liste en deux moitiés jusqu'à
obtenir des sous-listes d'un seul élément (donc triées par définition), puis on fusionne ces sous-listes deux à deux en les
remettant dans l'ordre. C'est l'un des algorithmes de tri les plus fiables, avec une complexité garantie de O(n log n) dans
tous les cas, mais il nécessite de la mémoire supplémentaire (O(n)).
def merge_sort(liste):
if len(liste) <= 1:
return liste
milieu = len(liste) // 2
gauche = merge_sort(liste[:milieu])
droite = merge_sort(liste[milieu:])
return fusionner(gauche, droite)
✏️Exercices
✅ Réponse 5.1
def merge_sort(liste):
if len(liste) <= 1:
return liste
milieu = len(liste) // 2
gauche = merge_sort(liste[:milieu])
droite = merge_sort(liste[milieu:])
return fusionner(gauche, droite)
Page 10 / 22
Exercice 5.2 — Compter les inversions
Le nombre d'inversions d'une liste est le nombre de paires (i, j) telles que i < j mais liste[i] > liste[j]. En utilisant
une variante du tri fusion (l'étape de fusion compte les inversions), écrivez une fonction
compter_inversions(liste) qui retourne ce nombre en O(n log n).
✅ Réponse 5.2
def compter_inversions(liste):
_, nb = _tri_et_compte(liste)
return nb
def _tri_et_compte(liste):
if len(liste) <= 1:
return liste, 0
milieu = len(liste) // 2
gauche, inv_g = _tri_et_compte(liste[:milieu])
droite, inv_d = _tri_et_compte(liste[milieu:])
fusion, inv_f = _fusionner_compte(gauche, droite)
return fusion, inv_g + inv_d + inv_f
print(compter_inversions([2, 4, 1, 3, 5]))
# 3 (paires (2,1), (4,1), (4,3))
✅ Réponse 5.3
def fusionner_k_listes(listes):
if not listes:
return []
while len(listes) > 1:
listes_fusionnees = []
for i in range(0, len(listes), 2):
if i + 1 < len(listes):
listes_fusionnees.append(fusionner(listes[i], listes[i + 1]))
else:
listes_fusionnees.append(listes[i])
listes = listes_fusionnees
return listes[0]
Page 11 / 22
print(fusionner_k_listes([[1, 4, 7], [2, 5], [0, 3, 6, 9]]))
# [0, 1, 2, 3, 4, 5, 6, 7, 9]
☑ À la fin de ce module, vous devez maîtriser le paradigme "diviser pour régner", implémenter le tri fusion, et l'adapter
pour compter des inversions ou fusionner plusieurs listes triées.
Page 12 / 22
Module 6 — Tri rapide (Quick Sort)
Durée : 4h Complexité : O(n log n) moyen / O(n²) pire cas Instable · En place
Résumé de la leçon
Le tri rapide choisit un élément pivot, puis partitionne la liste en deux groupes : les éléments plus petits que le pivot et
ceux plus grands. Il trie ensuite récursivement chaque groupe. C'est l'un des tris les plus rapides en pratique grâce à sa
bonne localité mémoire, mais son pire cas (O(n²)) survient quand le pivot est mal choisi (ex : toujours le premier élément
sur une liste déjà triée).
def quick_sort(liste):
if len(liste) <= 1:
return liste
pivot = liste[len(liste) // 2]
plus_petits = [x for x in liste if x < pivot]
egaux = [x for x in liste if x == pivot]
plus_grands = [x for x in liste if x > pivot]
return quick_sort(plus_petits) + egaux + quick_sort(plus_grands)
✏️Exercices
✅ Réponse 6.1
def quick_sort(liste):
if len(liste) <= 1:
return liste
pivot = liste[len(liste) // 2]
plus_petits = [x for x in liste if x < pivot]
egaux = [x for x in liste if x == pivot]
plus_grands = [x for x in liste if x > pivot]
return quick_sort(plus_petits) + egaux + quick_sort(plus_grands)
print(quick_sort([10, 7, 8, 9, 1, 5]))
# [1, 5, 7, 8, 9, 10]
✅ Réponse 6.2
Page 13 / 22
if liste[j] <= pivot:
i += 1
liste[i], liste[j] = liste[j], liste[i]
liste[i + 1], liste[haut] = liste[haut], liste[i + 1]
return i + 1
print(quick_sort_en_place([10, 7, 8, 9, 1, 5]))
# [1, 5, 7, 8, 9, 10]
✅ Réponse 6.3
import random
☑ À la fin de ce module, vous devez comprendre le mécanisme de partition, savoir implémenter le tri rapide en place, et
connaître les techniques (pivot aléatoire, médiane de trois) pour éviter le pire cas.
Page 14 / 22
Module 7 — Tri par tas (Heap Sort)
Résumé de la leçon
Le tri par tas utilise une structure de données appelée tas binaire max (max-heap) : un arbre binaire presque complet
où chaque parent est plus grand que ses enfants. On construit d'abord le tas à partir de la liste, puis on extrait
répétitivement le maximum (la racine) qu'on place à la fin de la liste, en réajustant le tas à chaque extraction.
Contrairement au tri fusion, il ne nécessite pas de mémoire supplémentaire.
def heap_sort(liste):
n = len(liste)
for i in range(n // 2 - 1, -1, -1):
entasser(liste, n, i)
for i in range(n - 1, 0, -1):
liste[0], liste[i] = liste[i], liste[0]
entasser(liste, i, 0)
return liste
✏️Exercices
✅ Réponse 7.1
def heap_sort(liste):
n = len(liste)
for i in range(n // 2 - 1, -1, -1):
entasser(liste, n, i)
for i in range(n - 1, 0, -1):
liste[0], liste[i] = liste[i], liste[0]
entasser(liste, i, 0)
return liste
Page 15 / 22
Exercice 7.2 — File de priorité avec heapq
Utilisez le module standard heapq pour implémenter une file de priorité qui retourne toujours les 3 plus petits
éléments d'une liste, sans trier la liste entière.
✅ Réponse 7.2
import heapq
def trois_plus_petits(liste):
return [Link](3, liste)
print(trois_plus_petits_manuel(liste))
# [1, 5, 6]
✅ Réponse 7.3
def est_un_max_heap(liste):
n = len(liste)
for i in range(n):
gauche, droite = 2 * i + 1, 2 * i + 2
if gauche < n and liste[i] < liste[gauche]:
return False
if droite < n and liste[i] < liste[droite]:
return False
return True
☑ À la fin de ce module, vous devez comprendre la structure de tas binaire, savoir implémenter entasser et
heap_sort , et connaître le module heapq de Python.
Page 16 / 22
Module 8 — Tri par comptage et Tri par base (Counting /
Radix Sort)
Résumé de la leçon
Contrairement aux tris précédents, le tri par comptage (Counting Sort) et le tri par base (Radix Sort) ne comparent
pas les éléments entre eux : ils exploitent la structure des données (nombres entiers dans un intervalle connu). Le tri par
comptage compte les occurrences de chaque valeur puis reconstruit la liste triée. Le tri par base applique un tri par
comptage sur chaque chiffre (unités, dizaines, ...), du moins significatif au plus significatif. Ces algorithmes peuvent
battre la limite théorique O(n log n) des tris comparatifs, mais uniquement sous des conditions particulières.
Counting Sort O(n + k) O(n + k) k = étendue des valeurs, doit être raisonnable
def counting_sort(liste):
if not liste:
return liste
maxi = max(liste)
compteur = [0] * (maxi + 1)
for x in liste:
compteur[x] += 1
resultat = []
for valeur, occurrences in enumerate(compteur):
[Link]([valeur] * occurrences)
return resultat
✏️Exercices
✅ Réponse 8.1
def counting_sort(liste):
if not liste:
return liste
maxi = max(liste)
compteur = [0] * (maxi + 1)
for x in liste:
compteur[x] += 1
resultat = []
for valeur, occurrences in enumerate(compteur):
[Link]([valeur] * occurrences)
return resultat
print(counting_sort([4, 2, 2, 8, 3, 3, 1]))
# [1, 2, 2, 3, 3, 4, 8]
✅ Réponse 8.2
Page 17 / 22
def counting_sort_stable(liste):
if not liste:
return liste
maxi = max(liste)
compteur = [0] * (maxi + 1)
for x in liste:
compteur[x] += 1
# sommes cumulées : compteur[v] = nombre d'éléments <= v
for v in range(1, maxi + 1):
compteur[v] += compteur[v - 1]
sortie = [0] * len(liste)
# parcours de droite à gauche pour garantir la stabilité
for x in reversed(liste):
compteur[x] -= 1
sortie[compteur[x]] = x
return sortie
print(counting_sort_stable([4, 2, 2, 8, 3, 3, 1]))
# [1, 2, 2, 3, 3, 4, 8]
✅ Réponse 8.3
def radix_sort(liste):
if not liste:
return liste
maxi = max(liste)
exp = 1
while maxi // exp > 0:
liste = counting_sort_par_chiffre(liste, exp)
exp *= 10
return liste
☑ À la fin de ce module, vous devez comprendre pourquoi les tris non comparatifs peuvent dépasser la limite O(n log n),
et savoir dans quels cas les utiliser (petits entiers, étendue de valeurs limitée).
Page 18 / 22
Module 9 — Synthèse, comparaison et projet final
Résumé de la leçon
Ce dernier module consolide tout ce qui a été appris. Voici un tableau récapitulatif des algorithmes étudiés :
En pratique : privilégiez insertion sort pour de très petites listes, quick sort pour la vitesse générale (c'est ce qu'utilise
sorted() / Timsort combiné à l'insertion en interne côté Python — Timsort mélange fusion et insertion), et merge sort
ou heap sort quand une complexité garantie O(n log n) est nécessaire.
✏️Exercices
✅ Réponse 9.1
import random
import time
def bubble_sort(liste):
n = len(liste)
for i in range(n - 1):
for j in range(n - 1 - i):
if liste[j] > liste[j + 1]:
liste[j], liste[j + 1] = liste[j + 1], liste[j]
return liste
def merge_sort(liste):
if len(liste) <= 1:
return liste
m = len(liste) // 2
g, d = merge_sort(liste[:m]), merge_sort(liste[m:])
resultat, i, j = [], 0, 0
while i < len(g) and j < len(d):
if g[i] <= d[j]:
[Link](g[i]); i += 1
else:
[Link](d[j]); j += 1
return resultat + g[i:] + d[j:]
Page 19 / 22
def quick_sort(liste):
if len(liste) <= 1:
return liste
pivot = liste[len(liste) // 2]
return (quick_sort([x for x in liste if x < pivot])
+ [x for x in liste if x == pivot]
+ quick_sort([x for x in liste if x > pivot]))
✅ Réponse 9.2
✅ Réponse 9.3
def bubble_sort(liste):
liste = [Link]()
n = len(liste)
for i in range(n - 1):
for j in range(n - 1 - i):
if liste[j] > liste[j + 1]:
liste[j], liste[j + 1] = liste[j + 1], liste[j]
return liste
def insertion_sort(liste):
liste = [Link]()
for i in range(1, len(liste)):
cle = liste[i]
j = i - 1
while j >= 0 and liste[j] > cle:
liste[j + 1] = liste[j]
j -= 1
Page 20 / 22
liste[j + 1] = cle
return liste
def merge_sort(liste):
if len(liste) <= 1:
return liste
m = len(liste) // 2
g, d = merge_sort(liste[:m]), merge_sort(liste[m:])
resultat, i, j = [], 0, 0
while i < len(g) and j < len(d):
if g[i] <= d[j]:
[Link](g[i]); i += 1
else:
[Link](d[j]); j += 1
return resultat + g[i:] + d[j:]
def quick_sort(liste):
if len(liste) <= 1:
return liste
pivot = liste[len(liste) // 2]
return (quick_sort([x for x in liste if x < pivot])
+ [x for x in liste if x == pivot]
+ quick_sort([x for x in liste if x > pivot]))
def heap_sort(liste):
liste = [Link]()
def entasser(l, n, i):
plus_grand = i
g, d = 2 * i + 1, 2 * i + 2
if g < n and l[g] > l[plus_grand]: plus_grand = g
if d < n and l[d] > l[plus_grand]: plus_grand = d
if plus_grand != i:
l[i], l[plus_grand] = l[plus_grand], l[i]
entasser(l, n, plus_grand)
n = len(liste)
for i in range(n // 2 - 1, -1, -1):
entasser(liste, n, i)
for i in range(n - 1, 0, -1):
liste[0], liste[i] = liste[i], liste[0]
entasser(liste, i, 0)
return liste
☑ Félicitations ! Vous avez maintenant une vue d'ensemble complète des algorithmes de tri classiques, leur complexité,
leurs cas d'usage, et vous savez les implémenter en Python.
Page 21 / 22
🎓 Fin du programme — 24 heures
Ce plan couvre les algorithmes de tri fondamentaux (comparatifs et non comparatifs) attendus dans toute
formation en algorithmique ou en entretien technique. Pour aller plus loin : étudiez Timsort (l'algorithme utilisé par
Python en interne), le tri par bucket (Bucket Sort), et les tris parallèles/externes pour les très grands volumes de
données ne tenant pas en mémoire.
Page 22 / 22