Quickselect
En algorithmique, quickselect est un algorithme de sélection qui retourne le ke plus petit élément dans une liste non ordonnée. Comme l'algorithme de tri quicksort, il a été créé par Tony Hoare et il est donc aussi connu comme l' algorithme de sélection de Hoare[1].
DĂ©couvreur ou inventeur | |
---|---|
Date de découverte | |
ProblÚme lié | |
Structure des données | |
Ă l'origine de |
Pire cas | |
---|---|
Moyenne | |
Meilleur cas |
Pire cas |
---|
Quickselect utilise la mĂȘme approche que quicksort, choisissant un Ă©lĂ©ment Ă la fois, afin de partitionner les Ă©lĂ©ments selon le pivot. Cependant, au lieu de sĂ©parer l'ensemble en deux parties comme dans quicksort, l'algorithme quickselect n'utilise la rĂ©cursion que sur un cĂŽtĂ© - le cĂŽtĂ© contenant l'Ă©lĂ©ment qu'il cherche. Cela rĂ©duit la complexitĂ© en moyenne O(n log n) de quicksort Ă la complexitĂ© en moyenne O(n) de quickselect.
Tout comme quicksort, l'algorithme quickselect est en général implémenté en place, et en plus de sélectionner le ke élément, il trie une partie des données. Comme quicksort, il est efficace en pratique avec un temps moyen de . Quickselect et ses variantes sont des algorithmes souvent utilisés dans le monde réel.
Algorithme
Quicksort utilise une fonction auxiliaire, appelée partition, qui réorganise la liste allant de la position gauche
Ă la position droite
en deux parties : les éléments qui sont plus petits qu'un certain élément appelé pivot
, et ceux qui sont supĂ©rieurs ou Ă©gaux au mĂȘme Ă©lĂ©ment. Voici le pseudo-code qui crĂ©e cette partition:
fonction partition(list, gauche, droite, pivot) ValeurPivot := list[pivot] échanger list[pivot] et list[droite] // Déplace le pivot à la fin IndiceStockage := gauche pour i de gauche à droite-1 si list[i] < ValeurPivot échanger list[IndiceStockage] et list[i] incrémenter IndiceStockage échanger list[droite] et list[IndiceStockage] // Met le pivot à sa place définitive. retourner IndiceStockage
Quicksort trie récursivement les deux branches, créant un pire cas en temps de Ω(n log n). Cependant la sélection choisit de quel cÎté de la partition se trouve l'élément, puisque le pivot est à sa place finale, avec les éléments plus petits à gauche et les plus grands à droite. Donc un seul appel récursif suffit à créer l'algorithme quickselect:
// Retourne le n-iĂšme plus petit Ă©lĂ©ment de la liste entre gauche et droite incluse (i.e. gauche †n †droite). // La taille de la liste reste la mĂȘme Ă chaque appel rĂ©cursif, donc n n'a pas besoin d'ĂȘtre changĂ©. fonction select(list, gauche, droite, n) si gauche = droite // S'il n'y a qu'un Ă©lĂ©ment retourner list[gauche] // Retourne cet Ă©lĂ©ment pivot := ... // Choisit un pivot entre gauche et droite, par exemple // gauche + Math.floor(Math.random() * (droite - gauche + 1)) pivot := partition(list, gauche, droite, pivot) // Si le pivot est Ă sa position finale si n = pivot retourner list[n] sinon si n < pivot retourner select(list, gauche, pivot - 1, n) sinon retourner select(list, pivot + 1, droite, n)
Remarquons la ressemblance avec quicksort: tout comme l'algorithme basĂ© sur la sĂ©lection de l'Ă©lĂ©ment minimal est un algorithme de tri partiel, quickselect est un quicksort partiel, crĂ©ant et partitionnant uniquement O(log n) de ces O(n) partitions. Cette procĂ©dure a en moyenne un temps d'exĂ©cution linĂ©aire, et de bonnes performances en pratique. C'est en plus un algorithme en place, utilisant un espace constant supplĂ©mentaire puisque la rĂ©cursion terminale peut ĂȘtre Ă©liminĂ©e avec une boucle telle que:
fonction select(list, gauche, droite, n) boucle si gauche = droite retourner list[gauche] pivot := ... // Choix du pivot entre gauche et droite pivot := partition(list, gauche, droite, pivot) si n = pivot retourner list[n] sinon si n < pivot droite := pivot - 1 sinon gauche := pivot + 1
Complexité en temps
Tout comme quicksort, l'efficacitĂ© de l'algorithme quickselect dĂ©pend du choix du pivot. Si le choix de pivot permet de faire dĂ©croĂźtre la taille du tableau Ă chaque Ă©tape d'une mĂȘme fraction, alors la dĂ©croissance est exponentielle, et la complexitĂ© en temps est linĂ©aire, comme une sĂ©rie gĂ©omĂ©trique. Si au contraire le pivot ne fait diminuer la taille que de 1, alors le pire cas a une complexitĂ© quadratique, O(n2). Cela arrive principalement si le pivot est l'Ă©lĂ©ment le plus Ă droite et que le tableau est dĂ©jĂ triĂ©.
Variantes
La solution la plus simple est de choisir un pivot alĂ©atoire, qui donne avec grande probabilitĂ© un temps d'exĂ©cution linĂ©aire. Comme algorithme dĂ©terministe, la stratĂ©gie de la mĂ©diane de 3 valeurs, comme dans quicksort, donne un temps d'exĂ©cution linĂ©aire sur les donnĂ©es partiellement triĂ©es, ce qui arrive souvent en vrai. Mais la complexitĂ© pire-cas peut toujours ĂȘtre atteinte, David Musser a montrĂ© comment atteindre ce pire cas, contre lequel il existe son algorithme introselect.
Une complexitĂ© linĂ©aire en pire-cas peut-ĂȘtre obtenue via la mĂ©thode de mĂ©diane des mĂ©dianes, avec un surcout pratique considĂ©rable, ce qui fait qu'il n'est pas utilisĂ© en pratique. Il est possible de le combiner avec le quickselect basique, et cet algorithme pour le pire cas, afin d'avoir de bons rĂ©sultats en pratiques et une bonne complexitĂ© dans le pire cas, c'est ce que fait introselect.
Plus prĂ©cisĂ©ment, la complexitĂ© moyenne en temps est pour un pivot alĂ©atoire (dans le cas de la recherche de la mĂ©diane, d'autres k sont plus rapides)[2]. La constante peut ĂȘtre descendue Ă 3/2 avec un pivot plus complexe, utilisant l'algorithme de FloydâRivest, dont la complexitĂ© moyenne en temps est de pour le calcul de la mĂ©diane, et plus rapide avec d'autres k.
Notes et références
- (en) C.A.R. Hoare, « Algorithm 65: find », Communications of the ACM, vol. 4, no 7,â , p. 321-322 (DOI 10.1145/366622.366647).
- Blum-style analysis of Quickselect, David Eppstein, October 9, 2007.