Exercice 1 : Complexité spatio-temporelle du tri rapide (QuickSort)
Problème guidé : Analyse fine de la complexité spatio-temporelle du tri rapide (Quicksort)
Section titled “Problème guidé : Analyse fine de la complexité spatio-temporelle du tri rapide (Quicksort)”Énoncé
Section titled “Énoncé”Le tri rapide (Quicksort) est un algorithme de tri par comparaison fondé sur le principe “diviser pour régner”. Bien que sa complexité temporelle en moyenne soit de , son comportement pire-cas et son empreinte spatiale (due à la pile des appels récursifs) nécessitent une analyse rigoureuse, en particulier dans un contexte de programmation système où la taille de la pile d’exécution est limitée.
On s’intéresse dans ce problème à un tableau d’éléments permutables de taille . On utilisera le schéma de partitionnement de Lomuto ou de Hoare, en supposant le choix du pivot déterministe (par exemple, le dernier élément du sous-tableau) ou aléatoire.
Questions
Section titled “Questions”Partie I : Rappels et comportement pire-cas (Temps et Espace)
Section titled “Partie I : Rappels et comportement pire-cas (Temps et Espace)”- Écrire une fonction en OCaml ou en Python réalisant le partitionnement d’un sous-tableau selon le pivot (partition de Lomuto). Préciser sa complexité temporelle et spatiale.
- En déduire la structure de la fonction récursive
quicksort. - Établir la relation de récurrence vérifiée par le temps d’exécution dans le pire des cas (par exemple, un tableau déjà trié avec un pivot mal choisi). Résoudre cette récurrence.
- Quelle est la consommation maximale de la pile d’appels récursifs dans ce pire des cas ? Conclure sur les limites de l’implémentation naïve face à des entrées de grande taille ().
Partie II : Optimisation de l’empreinte spatiale
Section titled “Partie II : Optimisation de l’empreinte spatiale”- Expliquer comment modifier l’algorithme récursif de telle sorte que la profondeur de la pile d’appels récursifs soit garantie en dans le pire des cas temporels, en traitant d’abord le plus petit sous-tableau.
- Démontrer que la taille maximale de la pile d’appels avec cette stratégie d’optimisation est .
Partie III : Analyse probabiliste du cas moyen
Section titled “Partie III : Analyse probabiliste du cas moyen”- On suppose désormais que le pivot est choisi uniformément au hasard parmi les indices du sous-tableau. Soit la variable aléatoire représentant le nombre de comparaisons effectuées pour trier un tableau de taille . Établir l’équation de récurrence portant sur l’espérance .
- Résoudre cette récurrence pour montrer que .
Pistes et Indications
Section titled “Pistes et Indications”- Partition de Lomuto : Maintenir un indice de séparation entre les éléments inférieurs au pivot et les éléments supérieurs. Complexité en en temps, en espace auxiliaire.
- Pire-cas : À chaque étape, le pivot sépare le tableau en un sous-tableau vide et un sous-tableau de taille .
- Optimisation spatiale : Penser à l’ordre des appels récursifs et à l’élimination de la récursion terminale (tail-call optimization).
- Espérance : Utiliser la probabilité que le -ème et le -ème éléments plus petits soient comparés durant l’algorithme.
Corrigé pas à pas
Section titled “Corrigé pas à pas”Partie I
Section titled “Partie I”1. Partition de Lomuto (en Python) :
def partition(T, low, high): pivot = T[high] i = low - 1 for j in range(low, high): if T[j] <= pivot: i += 1 T[i], T[j] = T[j], T[i] T[i + 1], T[high] = T[high], T[i + 1] return i + 1Complexité : Temporelle en (parcours linéaire du sous-tableau) et spatiale en en espace auxiliaire (tri en place).
2. Fonction récursive quicksort :
def quicksort(T, low, high): if low < high: pi = partition(T, low, high) quicksort(T, low, pi - 1) quicksort(T, pi + 1, high)3. Relation de récurrence (Pire des cas) : Si le tableau est déjà trié et qu’on choisit toujours le dernier élément comme pivot, , le sous-tableau de gauche est de taille et celui de droite est de taille . La relation de récurrence pour le nombre d’opérations est : Par télescopage, on obtient . Le pire des cas est donc quadratique.
4. Consommation de la pile : À chaque appel récursif non trivial, la taille du problème diminue seulement de . La profondeur maximale de la pile d’appels est donc de . Limite du modèle : Pour un tableau de taille (ordre de grandeur usuel en traitement de données), une profondeur de récursion de frames provoque systématiquement un dépassement de pile (Stack Overflow) sur les environnements standards (où la pile système est typiquement limitée à quelques mégaoctets, soit environ à appels imbriqués).
Partie II
Section titled “Partie II”5 & 6. Optimisation de la profondeur de pile : Pour garantir une pile en , on effectue l’appel récursif sur le plus petit sous-tableau en premier, et on traite le plus grand par récursion terminale (ou itération).
def quicksort_optimise(T, low, high): while low < high: pi = partition(T, low, high) if pi - low < high - pi: quicksort_optimise(T, low, pi - 1) low = pi + 1 # Équivaut à l'appel récursif pour la partie droite else: quicksort_optimise(T, pi + 1, high) high = pi - 1Puisque l’on appelle récursivement la fonction sur le sous-tableau de taille strictement inférieure à la moitié (), la taille du problème traité par récursion diminue d’au moins un facteur 2 à chaque niveau où l’on descend. La profondeur maximale de la pile est donc de .
Partie III
Section titled “Partie III”7 & 8. Analyse probabiliste du cas moyen : Soit le coût (nombre de comparaisons). Si le pivot choisi aléatoirement est le -ième plus petit élément (avec uniformément réparti entre et ), les sous-tableaux ont pour tailles et . L’espérance vérifie : En sommant sur toutes les équiprobabilités : Par symétrie : Par une méthode classique de résolution par différence (en calculant ), on montre par récurrence forte que : Cette complexité moyenne garantit l’efficacité du tri rapide pour des volumes de données réalistes, sous réserve d’une gestion rigoureuse de la pile d’exécution.