Prérequis : La récursivité, Tris naïfs.
L[:m] et L[m+1:] : l'élément d'indice m disparaît.| Code erroné | Code correct | Explication |
|---|---|---|
tri_fusion(L[:m]) et tri_fusion(L[m+1:]) | L[:m] et L[m:] | Les tranches L[:m] et L[m:] recouvrent toute la liste ; avec m+1, l'élément L[m] n'est dans aucune moitié et disparaît du résultat. |
if len(L) == 1: return L comme seul cas de base | if len(L) <= 1: return L | Découper [x] donne [] et [x] : sans cas de base pour la liste vide, tri_fusion([]) se rappelle indéfiniment (RecursionError). |
| Fusion terminée dès qu'une liste est épuisée | extend(L1[i:]) puis extend(L2[j:]) | Quand une liste est vide, la fin de l'autre est déjà triée : il faut la recopier, sinon des éléments manquent. |
return L1 + L2 dans la fusion | Parcours avec deux indices i et j | La concaténation juxtapose les deux listes sans les mêler : [1, 4] + [2, 3] donne [1, 4, 2, 3], qui n'est pas trié. |
def fusion(L1, L2):
"""Fusionne deux listes triées en une seule liste triée."""
resultat = []
i, j = 0, 0
while i < len(L1) and j < len(L2):
if L1[i] <= L2[j]:
resultat.append(L1[i])
i += 1
else:
resultat.append(L2[j])
j += 1
resultat.extend(L1[i:]) # reste de L1 (éventuellement vide)
resultat.extend(L2[j:]) # reste de L2
return resultat
print(fusion([1, 4, 9], [2, 3, 10])) # [1, 2, 3, 4, 9, 10]
Trace de fusion([1, 4, 9], [2, 3, 10]) :
| Étape | Comparaison | Élément pris | resultat |
|---|---|---|---|
| 1 | $1 \leqslant 2$ | 1 (de L1) | [1] |
| 2 | $4 > 2$ | 2 (de L2) | [1, 2] |
| 3 | $4 > 3$ | 3 (de L2) | [1, 2, 3] |
| 4 | $4 \leqslant 10$ | 4 (de L1) | [1, 2, 3, 4] |
| 5 | $9 \leqslant 10$ | 9 (de L1) | [1, 2, 3, 4, 9] |
| fin | L1 épuisée | reste de L2 recopié | [1, 2, 3, 4, 9, 10] |
Cinq comparaisons pour six éléments : la fusion est linéaire.
def tri_fusion(L):
"""Renvoie une nouvelle liste contenant les éléments de L triés."""
if len(L) <= 1: # cas de base : 0 ou 1 élément
return L
m = len(L) // 2
gauche = tri_fusion(L[:m]) # régner sur la moitié gauche
droite = tri_fusion(L[m:]) # régner sur la moitié droite
return fusion(gauche, droite) # combiner
print(tri_fusion([8, 3, 5, 1, 9, 2])) # [1, 2, 3, 5, 8, 9]
Déroulement sur [8, 3, 5, 1, 9, 2] :
| Phase | Listes |
|---|---|
| découpage, niveau 1 | [8, 3, 5] [1, 9, 2] |
| découpage, niveau 2 | [8] [3, 5] [1] [9, 2] |
| découpage, niveau 3 | [3] [5] [9] [2] |
| fusions, niveau 3 | [3, 5] [2, 9] |
| fusions, niveau 2 | [3, 5, 8] [1, 2, 9] |
| fusion, niveau 1 | [1, 2, 3, 5, 8, 9] |
Trois niveaux de découpage pour six éléments ($\lceil \log_2 6 \rceil = 3$) ; à chaque niveau, les fusions parcourent au total six éléments.
Ordres de grandeur : nombre d'opérations pour trier $n$ éléments.
| $n$ | $\log_2 n$ | tri fusion, $n \log_2 n$ | tri par sélection, $n^2 / 2$ |
|---|---|---|---|
| $8$ | $3$ | $24$ | $32$ |
| $1\,024$ | $10$ | $10\,240$ | $524\,288$ |
| $10^6$ | $20$ | $2 \times 10^7$ | $5 \times 10^{11}$ |
def puissance_rapide(x, n):
if n == 0:
return 1
demi = puissance_rapide(x, n // 2)
if n % 2 == 0:
return demi * demi
return x * demi * demi
print(puissance_rapide(2, 10)) # 1024, en 4 appels récursifs au lieu de 10
def maximum_dpr(L):
"""Maximum d'une liste non vide, par diviser pour régner."""
if len(L) == 1:
return L[0]
m = len(L) // 2
return max(maximum_dpr(L[:m]), maximum_dpr(L[m:]))
print(maximum_dpr([3, 9, 1, 7, 4])) # 9
On applique tri_fusion à la liste [6, 2, 7, 4, 1, 5, 3].
tri_fusion sont effectués au total (appel initial compris) ?[6, 2, 7] et [4, 1, 5, 3] ; niveau 2, [6], [2, 7], [4, 1], [5, 3] ; niveau 3, [2], [7], [4], [1], [5], [3].[2, 7] ; [2, 6, 7] ; [1, 4] ; [3, 5] ; [1, 3, 4, 5] ; enfin [1, 2, 3, 4, 5, 6, 7].[6], [2, 7], [4, 1], [5, 3]) et six au niveau 3 : $1 + 2 + 4 + 6 = 13$ appels. Sept d'entre eux portent sur une liste à un élément (cas de base).fusion_compteur(L1, L2) qui renvoie le couple (liste fusionnée, nombre de comparaisons).def fusion_compteur(L1, L2):
resultat = []
i, j, nb = 0, 0, 0
while i < len(L1) and j < len(L2):
nb += 1
if L1[i] <= L2[j]:
resultat.append(L1[i])
i += 1
else:
resultat.append(L2[j])
j += 1
resultat.extend(L1[i:])
resultat.extend(L2[j:])
return resultat, nb
print(fusion_compteur([1, 2, 3, 4], [5, 6, 7, 8])) # ([1, ..., 8], 4)
print(fusion_compteur([1, 3, 5, 7], [2, 4, 6, 8])) # ([1, ..., 8], 7)
Meilleur cas : tous les éléments de L1 sont plus petits que ceux de L2 : quatre comparaisons, puis L2 est recopiée sans comparaison. Pire cas : les éléments alternent : $4 + 4 - 1 = 7$ comparaisons, la dernière liste restante n'ayant plus qu'un élément.
maximum_dpr(L) qui renvoie le maximum d'une liste non vide en la coupant en deux moitiés.maximum_dpr sont effectués pour une liste de $8$ éléments ? Pour une liste de $n$ éléments ?def maximum_dpr(L):
if len(L) == 1:
return L[0]
m = len(L) // 2
return max(maximum_dpr(L[:m]), maximum_dpr(L[m:]))
print(maximum_dpr([3, 9, 1, 7, 4, 8, 2, 6])) # 9
Pour $8$ éléments : $1 + 2 + 4 + 8 = 15$ appels (un par nœud de l'arbre des appels). Pour $n$ éléments, $2n - 1$ appels : $n$ feuilles et $n - 1$ combinaisons.
Complexité : chaque appel fait au plus une comparaison (max), donc $O(n)$, comme le parcours simple. Ici, diviser pour régner n'apporte rien : la combinaison coûte autant que le problème lui-même, et la récursion consomme de la mémoire en plus. Le schéma n'est gagnant que lorsque la combinaison est peu coûteuse au regard du travail évité, comme dans le tri fusion.
Une machine effectue $10^7$ opérations élémentaires par seconde.
| $n$ | $\log_2 n$ | $n \log_2 n$ | $n^2 / 2$ |
|---|---|---|---|
| $16$ | $4$ | $64$ | $128$ |
| $10^4$ | $\approx 13{,}3$ | $\approx 1{,}3 \times 10^5$ | $5 \times 10^7$ |
| $10^6$ | $\approx 20$ | $\approx 2 \times 10^7$ | $5 \times 10^{11}$ |