Algorithme de tri

Algorithme de tri

On souhaite réordonner une collection d’éléments suivant une relation d’ordre déterminée. On peut, sans perte de généralité, simplifier l’étude des différentes méthodes de tri en se limitant aux listes d’entiers, qu’on souhaite trier par ordre croissant.

Il s’agit donc d’élaborer une fonction tri(L) qui prend en paramètre une liste d’entier L et qui permet de trier cette liste, et de renvoyer la liste triée.

def tri(L):
    # ...
    return L

# Programme de test
L = [9, 5, 8, 1, 12, 4]
tri(L)
print(L)

Dans toute la suite, on pose $n = len(L)$.

Tri par sélection

Le tri par sélection consiste a sélectionner dans chaque itération le prochain élément a placer dans la liste triée, qui est le minimum des éléments non encore tries. C’est a dire, en faisant varier $i$ de $0$ a $n$, a chaque itération, on va remettre le $i^{\text{eme}}$ élément de la liste triée, qui est min(L[i:n]), a sa place L[i].

Implémentation

def tri_selection(L):                               # 1
    for i in range(len(L)-1):                       # 1
        indice_min = i                              # 1
        for j in range(i+1,len(L)):                 # 1
            if L[j] < L[indice_min] :               # 1
                indice_min = j                      # 1
        L[i],L[indice_min] = L[indice_min],L[i]     # 1

Complexité

On a deux boucles imbriquées, la boucle intérieure comporte des instructions de complexité $O(1)$, et se répète n-i fois, la boucle extérieure se répété pour i=0 a i=n, donc la complexité est $O\left( n^{2} \right)$.

Tri par insertion

Le tri par insertion consiste a insérer, un par un, les éléments de la liste non triée dans la liste triée.

Implémentation

def insertionSort(L):
    for i in range(1, len(L)):
        cle = L[i]
        j = i-1
        while j >= 0 and cle < L[j] :
                L[j + 1] = L[j]
                j -= 1
        L[j + 1] = cle

Complexité

La complexité de la boucle intérieure dépend de l’état de la liste.

  • Au meilleur des cas : La liste est déjà triée, la boucle while ne s’exécute pas. Complexité $O(n)$.

  • Au pire des cas : La liste est triée a l’ordre inverse, la boucle while se répété i fois, donc la complexité est $O\left( n^{2} \right)$.

Tri a bulles

Le tri a bulles consiste a faire plusieurs passages de comparaisons deux-a-deux, ou on va échanger deux éléments si il ne sont pas au bon ordre. Les éléments vont se déplacer petit a petit jusqu’à trouver leurs places. Ce déplacement global des éléments ressemble aux déplacements de bulles qui remontent dans un liquide.

Implémentation

def bubbleSort(L):
    n = len(L)
    for i in range(n):
        for j in range(0, n-i-1):
            if L[j] > L[j+1]:
                L[j], L[j+1] = L[j+1], L[j]

Complexité

La complexité est $O\left( n^{2} \right)$

Tri par fusion

Le tri par fusion est un tri récursif, ou on découpe la liste en deux, on trie chaque partie a l’aide d’un appel récursif, puis on fusionne les deux listes triées en une seule liste finale.

Implémentation

def fusion(L, R):
    i = j = 0
    S = []
    while i < len(L) and j < len(R):
        if L[i] <= R[j]:
            S.append(L[i])
            i += 1
        else:
            S.append(R[j])
            j += 1
    while i < len(L):
        S.append(L[i])
        i += 1
    while j < len(R):
        S.append(R[j])
        j += 1
    return S

def tri_fusion(L):
    if len(L) <= 1:
        return L
    mid = len(L)//2
    L1 = L[:mid]
    L2 = L[mid:]

    L1 = tri_fusion(L1)
    L2 = tri_fusion(L2)
    return fusion(L1, L2)

Complexité

La complexité $C(n)$ vérifie la relation de récurrence : $C(n) = 2C\left( \frac{n}{2} \right) + O(n)$

La complexité est donc $O\left( n\log_{2}(n) \right)$

Tri Rapide

Le tri rapide (ou tri pivot) est un tri récursif qui consiste a choisir un élément pivot p suivant lequel on découpe la liste en deux listes : L1 les éléments inférieurs au pivot, et L2 les éléments supérieurs au pivot, on trie alors chaque partie a l’aide d’un appel récursif, la liste totale triée est donc : L1 + [p] + L2.

Implémentation

def quick_sort(L):
    if len(L) <= 1:
        return L
    pivot = L[0]
    L1=[]
    L2=[]

    for i in range(1,len(L)):
        if L[i] <= p :
            L1.append(L[i])
        if L[i] > p :
            L2.append(L[i])
    return quick_sort(L1) + [p] + quick_sort(L2)

Complexité

On note $C(n)$ la complexité de la fonction quick_sort(L), avec n = len(L)

La boucle for a une complexité de $O(n)$

Le coût des appels récursifs quick_sort(L1) et quick_sort(L2) dépend des longueurs des listes L1 et L2:

  • Au pire des cas :

Le pivot est le plus grand (ou plus petit) élément de la liste, L1 et L2 auront les tailles n-1 et 0, donc : $$C(n) = O(n) + C(n - 1) + C(0)$$ Ce qui donne $$C(n) = O\left( n^{2} \right)$$

  • Au meilleur des cas :

Le pivot est au milieu de la liste, les listes L1 et L2 sont de taille a peu prés n/2, donc : $$C(n) = O(n) + 2C\left( \frac{n}{2} \right)$$ Ce qui donne $$C(n) = O\left( n\log(n) \right)$$