Skip to main content Informatique en prépa - Lycée Malherbe 

8- Exponentiation rapide et tri fusion

On peut appliquer le principe de la dichotomie, "couper en deux", sur des problèmes où c'est moins évident : soit on arrive à ne traiter qu'une seule des deux parties (exponentiation rapide), soit on traite les deux parties indépendamment, avant de combiner les résultats (tri fusion, tri rapide).

I. Exponentiation rapide

Ici, en prenant un flottant (c'est-à-dire un nombre à virgule) et un entier positif, on veut calculer de manière efficace.

Méthode naïve : on peut faire pour un total de multiplications.

Mais on peut faire mieux ! Par exemple :

Python

Loading editor...

def exp_rapide(x, n):
    """Calcule x^n en O(log(n)), avec x un flottant et n un entier positif"""
    reste = 1
    while n != 0:
        if n % 2 == 0:
            n //= 2
            x *= x
        else:
            reste *= x
            n -= 1
    return reste
Python

Loading editor...

print(exp_rapide(5, 7), 5 ** 7)
print(exp_rapide(2, 100))
print(exp_rapide(1.5, 10))

Voici les valeurs successives des variables lors du calcul de :

xnreste
571
565
2535
252125
6251125
6250125 × 625

On peut obtenir ce tableau automatiquement avec tracer_variables :

Python
code masqué : pas besoin de le lire pour suivre, exécutez-le simplement

II. Tri fusion

On va réutiliser l'idée de couper en deux, mais cette fois pour trier une liste. Le tri fusion (ou "partition-fusion") est en 3 étapes :

  1. Couper la liste à trier en deux parts égales.
  2. Trier chaque partie séparément.
  3. Fusionner les deux parties triées.

On va s'intéresser à ces étapes une par une.

a) Couper la liste en deux

Python

Loading editor...

def partition(L):
    """Prend une liste en argument, et renvoie deux nouvelles listes contenant chacune la moitié des éléments (avec un de plus dans la deuxième si nécessaire)"""
    n = len(L)
    milieu = n//2

    moitie_1 = L[0:milieu]
    moitie_2 = L[milieu:n]

    return (moitie_1, moitie_2)
Python

Loading editor...

partition([4, 1, 2, 8, 3, 1, 9])

b) Trier chaque partie séparément

On va trier les parties en utilisant... le tri fusion lui-même !

c) Fusionner deux listes triées

Idée : on aimerait placer tous les éléments contenus dans les deux listes moitie_1 et moitie_2 dans une nouvelle liste resultat. On va prendre les éléments un par un !

Python

Loading editor...

def fusion(L1, L2):
    """fusionne deux listes triées en ordre croissant en une seule liste triée également."""
    resultat = []
    i1 = 0
    i2 = 0
    #On transfère les éléments tant qu'on n'a terminé aucune des deux listes
    while i1 < len(L1) and i2 < len(L2):
        if L1[i1] < L2[i2]:
            resultat.append(L1[i1])
            i1 += 1
        else:
            resultat.append(L2[i2])
            i2 += 1

    #Une fois qu'une liste est terminée, on peut transférer tout ce qu'il reste dans l'autre !
    reste1 = L1[i1:]
    reste2 = L2[i2:]

    resultat = resultat + reste1 + reste2

    #Un seul des deux restes sera non vide (grâce à la condition du "while")

    return resultat
Python

Loading editor...

fusion([1, 4, 8, 9], [1, 2, 3, 10])

Pour visualiser la fusion, voici la même fonction, qui note chaque étape pour l'animer avec animer_liste. On affiche L1, puis L2, puis le résultat en cours de construction ; les deux éléments comparés sont en rouge :

Python
code masqué : pas besoin de le lire pour suivre, exécutez-le simplement

d) En combinant les trois

Python

Loading editor...

def tri_fusion(L):
    if len(L) <= 1:
        return L[:]
    #partition
    moitie_1, moitie_2 = partition(L)

    #tri récursif
    moitie_1 = tri_fusion(moitie_1)
    moitie_2 = tri_fusion(moitie_2)

    #fusion
    return fusion(moitie_1, moitie_2)
Python
code masqué : pas besoin de le lire pour suivre, exécutez-le simplement

III. Tri rapide

Le tri rapide découpe lui aussi la liste en deux, mais différemment : on choisit un élément, le pivot (par exemple le premier), et on sépare les autres éléments en deux listes, ceux qui sont plus petits que le pivot et ceux qui sont plus grands. Il reste à trier ces deux listes (récursivement), et à les mettre bout à bout, avec le pivot au milieu. Il n'y a plus besoin de fusion !

Python

Loading editor...

def tri_rapide(L):
    if len(L) <= 1:
        return L[:]
    pivot = L[0]
    plus_petits = []
    plus_grands = []
    for x in L[1:]:
        if x < pivot:
            plus_petits.append(x)
        else:
            plus_grands.append(x)
    return tri_rapide(plus_petits) + [pivot] + tri_rapide(plus_grands)

print(tri_rapide([4, 1, 2, 8, 3, 1, 9]))

from affichage import animer_tri
animer_tri(tri_rapide, [5, 9, 2, 7, 1, 8, 3, 6, 4, 10, 12, 11])
Complexité du tri rapide

  • Si le pivot coupe toujours la liste en deux parties de même taille, on a niveaux d'appels, chacun en : la complexité est en , comme le tri fusion.
  • Dans le pire cas, le pivot est toujours le plus petit (ou le plus grand) élément, par exemple si la liste est déjà triée : une des deux parties est vide, et la complexité est en .
Python
code masqué : pas besoin de le lire pour suivre, exécutez-le simplement

En pratique, le tri rapide est souvent très rapide (d'où son nom), surtout si on choisit le pivot au hasard et pas comme le premier élément.

Exercices

Exercice 1

Combien de multiplications fait exp_rapide(x, 16) ? Et exp_rapide(x, 15) ? Comparer avec la méthode naïve.

Pour : est divisé par 2 quatre fois (4 multiplications x *= x), puis une multiplication reste *= x quand : 5 multiplications, contre 15 pour la méthode naïve. Pour : 7 multiplications (on peut le vérifier avec tracer_variables).

Exercice 2

Écrire une fonction récursive fusion_rec(L1, L2) qui fusionne deux listes triées, en comparant leurs premiers éléments.

Python

Loading editor...

def fusion_rec(L1, L2):
    if len(L1) == 0:
        return L2[:]
    if len(L2) == 0:
        return L1[:]
    if L1[0] <= L2[0]:
        return [L1[0]] + fusion_rec(L1[1:], L2)
    return [L2[0]] + fusion_rec(L1, L2[1:])
Exercice 3

Modifier tri_rapide pour choisir un pivot au hasard (from random import randint). Vérifier avec animer_tri que la liste déjà triée n'est plus un pire cas.

Python

Loading editor...

On tire un indice au hasard, on prend pivot = L[k], et on sépare les éléments de L[:k] + L[k+1:] :

from random import randint

def tri_rapide_hasard(L):
    if len(L) <= 1:
        return L[:]
    k = randint(0, len(L) - 1)
    pivot = L[k]
    autres = L[:k] + L[k+1:]
    plus_petits = [x for x in autres if x < pivot]
    plus_grands = [x for x in autres if x >= pivot]
    return tri_rapide_hasard(plus_petits) + [pivot] + tri_rapide_hasard(plus_grands)
Exercice 4

Écrire une version récursive de exp_rapide, en utilisant et .

Python

Loading editor...

def exp_rapide_rec(x, n):
    if n == 0:
        return 1
    y = exp_rapide_rec(x, n // 2)
    if n % 2 == 0:
        return y * y
    return x * y * y
Exercice 5

Une inversion d'une liste L est un couple d'indices tels que L[i] > L[j]. Adapter fusion et tri_fusion pour compter le nombre d'inversions en .

Python

Loading editor...

Lors de la fusion, quand on prend un élément de L2, il forme une inversion avec tous les éléments restants de L1 :

def tri_fusion_inversions(L):
    """Renvoie la liste triée et le nombre d'inversions."""
    if len(L) <= 1:
        return L[:], 0
    milieu = len(L) // 2
    L1, n1 = tri_fusion_inversions(L[:milieu])
    L2, n2 = tri_fusion_inversions(L[milieu:])
    resultat, inversions = [], n1 + n2
    i1 = i2 = 0
    while i1 < len(L1) or i2 < len(L2):
        if i1 < len(L1) and (i2 == len(L2) or L1[i1] <= L2[i2]):
            resultat.append(L1[i1])
            i1 += 1
        else:
            resultat.append(L2[i2])
            inversions += len(L1) - i1
            i2 += 1
    return resultat, inversions

tri_fusion_inversions([3, 1, 2, 5, 4])   # 3 inversions : (3, 1), (3, 2), (5, 4)
Exercice 6

Combien de tours de boucle fait recherche_zero(f, 0, 1, 1e-9) (la fonction du chapitre sur la dichotomie) ?

Il faut , soit : 30 tours.