Quelqu'un a déjà vu cette amélioration à quicksort?
Manipulation des éléments répétés dans les précédents quicksorts
j'ai trouvé un moyen de gérer les éléments répétés plus efficacement à quicksort et je voudrais savoir si quelqu'un a déjà vu Cela fait avant.
cette méthode réduit considérablement les frais généraux liés au contrôle des éléments répétés, ce qui améliore les performances avec et sans éléments répétés. En général, les éléments répétés sont traités de plusieurs façons différentes que je vais d'abord énumérer.
tout d'abord, il y a la méthode du drapeau national néerlandais qui trie le tableau comme [ < pivot | == pivot | unsorted | > pivot]
.
deuxièmement, il y a la méthode de placer les éléments égaux à l'extrême gauche pendant le tri et ensuite les déplacer au centre le tri est [ == pivot | < pivot | unsorted | > pivot]
et puis après le tri les éléments ==
sont déplacés au centre.
Troisièmement, le partitionnement Bentley-McIlroy met les éléments ==
à la fois côtés de sorte que le tri est [ == pivot | < pivot | unsorted | > pivot | == pivot]
et puis les éléments ==
sont déplacés au milieu.
les deux dernières méthodes sont faites dans une tentative de réduire la tête.
Ma Méthode
maintenant, permettez-moi d'expliquer comment ma méthode améliore le quicksort en réduisant le nombre de comparaisons. J'utilise deux quicksort fonctions d'ensemble plutôt qu'une.
la première fonction que j'appellerai q1
et il trie un tableau comme [ < pivot | unsorted | >= pivot]
.
la deuxième fonction que j'appellerai q2
et elle trie le tableau comme [ <= pivot | unsorted | > pivot]
.
regardons maintenant l'utilisation de ces en tandem afin d'améliorer le traitement des éléments répétés.
tout d'Abord, nous appelons q1
pour trier tout le tableau. Il choisit un pivot que nous appellerons pivot1
et trie ensuite autour de pivot1
. Ainsi, notre tableau est classé à ce point comme [ < pivot1 | >= pivot1 ]
.
ensuite, pour la partition [ < pivot1]
, nous l'envoyons à q1
à nouveau, et cette partie est assez normale, alors trions d'abord l'autre partition.
pour la partition [ >= pivot1]
, nous l'envoyons à q2
. q2
choisit un pivot, que nous appellerons pivot2
à partir de ce sous-ensemble et le trie en [ <= pivot2 | > pivot2]
.
si nous regardez maintenant le tableau entier, notre tri ressemble à [ < pivot1 | >= pivot1 and <= pivot2 | > pivot2]
. Cela ressemble beaucoup à un double pivot quicksort.
maintenant, retournons au subarray à l'intérieur de q2
( [ <= pivot2 | > pivot2]
).
pour la partition [ > pivot2]
, nous la renvoyons simplement à q1
ce qui n'est pas très intéressant.
pour la partition [ <= pivot2]
, nous vérifions d'abord si pivot1 == pivot2
. S'ils sont égaux, alors cette partition est déjà trié parce qu'ils sont tous des éléments égaux! Si les pivots ne sont pas égaux, alors nous envoyons juste cette partition à q2
encore une fois qui pioche un pivot (plus loin pivot3
), tries, et si pivot3 == pivot1
, alors il n'a pas à trier le [ <= pivot 3]
et ainsi de suite.
J'espère que vous avez compris. L'amélioration avec cette technique est que les éléments égaux sont manipulés sans avoir à vérifier si chaque élément est également égale aux pivots. En d'autres termes, il utilise moins de comparaisons.
il y a une autre amélioration possible que je n'ai pas encore essayé qui est de vérifier dans qs2
si la taille de la partition [ <= pivot2]
est assez grande (ou la partition [> pivot2]
est très petite) par rapport à la taille de son sous-ensemble total et puis de faire un contrôle plus standard pour les éléments répétés dans ce cas (une des méthodes énumérées ci-dessus).
Code Source
ici sont deux fonctions très simplifiées qs1
et qs2
. Ils utilisent la méthode de tri des pointes convergentes de Sedgewick. Ils peuvent évidemment être très optimisés (ils choisissent pivots extrêmement mal par exemple), mais c'est juste pour montrer l'idée. Mon propre implémentation est plus longue, plus rapide et beaucoup plus difficile à lire donc commençons par ceci:
// qs sorts into [ < p | >= p ]
void qs1(int a[], long left, long right){
// Pick a pivot and set up some indicies
int pivot = a[right], temp;
long i = left - 1, j = right;
// do the sort
for(;;){
while(a[++i] < pivot);
while(a[--j] >= pivot) if(i == j) break;
if(i >= j) break;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
// Put the pivot in the correct spot
temp = a[i];
a[i] = a[right];
a[right] = temp;
// send the [ < p ] partition to qs1
if(left < i - 1)
qs1(a, left, i - 1);
// send the [ >= p] partition to qs2
if( right > i + 1)
qs2(a, i + 1, right);
}
void qs2(int a[], long left, long right){
// Pick a pivot and set up some indicies
int pivot = a[left], temp;
long i = left, j = right + 1;
// do the sort
for(;;){
while(a[--j] > pivot);
while(a[++i] <= pivot) if(i == j) break;
if(i >= j) break;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
// Put the pivot in the correct spot
temp = a[j];
a[j] = a[left];
a[left] = temp;
// Send the [ > p ] partition to qs1
if( right > j + 1)
qs1(a, j + 1, right);
// Here is where we check the pivots.
// a[left-1] is the other pivot we need to compare with.
// This handles the repeated elements.
if(pivot != a[left-1])
// since the pivots don't match, we pass [ <= p ] on to qs2
if(left < j - 1)
qs2(a, left, j - 1);
}
je sais que c'est une idée assez simple, mais il donne une amélioration assez significative dans l'exécution quand je ajoutez les améliorations standard de quicksort (choix du pivot médian de-3, et tri d'insertion pour un petit tableau pour un démarrage). Si vous allez tester en utilisant ce code, ne le faites que sur des données aléatoires en raison du mauvais choix du pivot (ou améliorer le choix du pivot). Pour utiliser cette sorte vous appelleriez:
qs1(array,0,indexofendofarray);
Quelques Repères
si vous voulez savoir à quelle vitesse il est, voici un peu de données pour commencer. Cela utilise ma version optimisée, pas la raison donnée ci-dessus. Cependant, celui ci-dessus est encore beaucoup plus proche dans le temps du quicksort à double pivot que le temps std::sort
.
sur des données hautement aléatoires avec 2.000.000 d'éléments, j'obtiens ces temps (à partir du tri de plusieurs ensembles de données consécutives):
std::sort - 1.609 seconds
dual-pivot quicksort - 1.25 seconds
qs1/qs2 - 1.172 seconds
où std::sort
est le C++ Standard Library sort, le quicksort à double pivot est celui qui est sorti il ya plusieurs mois par Vladimir Yaroslavskiy, et qs1/qs2
est mon implémentation rapide.
sur des données beaucoup moins aléatoires. avec 2 000 000 d'éléments et généré avec rand() % 1000
(ce qui signifie que chaque élément a environ 2000 copies) les temps sont:
std::sort - 0.468 seconds
dual-pivot quicksort - 0.438 seconds
qs1/qs2 - 0.407 seconds
il y a des cas où le quicksort à double pivot gagne et je me rends compte que le quicksort à double pivot pourrait être optimisé davantage, mais la même chose pourrait être énoncée en toute sécurité pour mon quicksort.
est-ce que quelqu'un a vu ceci avant?
je sais que c'est une longue question/explication, mais n'avez-vous vu cette amélioration? Si oui, alors pourquoi n'est-il pas être utilisé?
5 réponses
Vladimir Yaroslavskiy / 11 Sep 12: 35 remplacement de Quicksort en java.util.Matrices avec nouveau Quicksort à double Pivot
Visit http://permalink.gmane.org/gmane.comp.java.openjdk.core-libs.devel/2628
pour répondre À votre question, non je n'ai pas vu cette approche avant. Je ne vais pas établir le profil de votre code et faire le reste du travail, mais peut-être que les prochaines étapes/considérations suivantes sont les prochaines étapes / considérations pour présenter officiellement votre algorithme. Dans le monde réel, les algorithmes de tri sont mis en œuvre pour avoir:
bonne évolutivité / complexité et frais généraux réduits
échelle et au-dessus sont évidents et sont facile à mesurer. Lors du tri de profilage, en plus du temps mesurer le nombre de comparaisons et de swaps. La Performance sur les gros fichiers dépendra également du temps de recherche du disque. Par exemple, le tri de fusion fonctionne bien sur de grands fichiers avec un disque magnétique. ( voir aussi Tri Rapide Vs Fusion Tri )
large gamme d'entrées avec de bonnes performances
Il y a beaucoup de données à trier. Et les applications sont connues pour produire des données dans des modèles, il est donc important de faire le sort est résistant contre de mauvaises performances sous certains modèles. Votre algorithme est optimisé pour les nombres répétés. Que faire si tous les nombres sont répétées, mais une seule fois (c'est à dire seq 1000>fichier; seq 1000>>fichier; chouf fichier)? Que faire si les numéros sont déjà triés? triés à l'envers? que penser d'un modèle de 1,2,3,1,2,3,1,2,3,1,2,3? 1,2,3,4,5,6,7,6,5,4,3,2,1? 7,6,5,4,3,2,1,2,3,4,5,6,7? De mauvaises performances dans l'un de ces scénarios est un briseur d'affaire! Avant de comparer avec un algorithme général publié, il est sage de préparer cette analyse.
faible risque de performance pathologique
de toutes les permutations d'entrées, il y en a une qui fonctionne moins bien que les autres. Combien pire ne fait que réaliser que la moyenne? Et combien de permutations donneront une performance aussi médiocre?
bonne chance pour vos prochaines étapes!
c'est une grande amélioration et je suis sûr qu'il a été mis en œuvre spécifiquement si vous attendez beaucoup d'objets égaux. Il y a beaucoup de tweks de mur de ce genre.
si je comprends tout ce que vous avez écrit correctement, la raison pour laquelle il n'est pas généralement" connu " est qu'il améliore la performance de base O(n2). Cela signifie, doubler le nombre d'objets, quadrupler le temps. Votre amélioration ne change pas ceci à moins que tous les objets soient égaux.
std: le tri n'est pas vraiment rapide.
Voici les résultats que j'obtiens en la comparant à un quicksort non récursif parallèle et aléatoire:
pnrqSort (longs): .:.1 000 000 36ms (postes par ms: 27777.8)
.:.5 000 000 140ms (articles par ms: 35714.3)
.:.10 000 000 296ms (articles par ms: 33783.8)
.:.50 000 000 1 484ms (articles par ms: 33692.7)
.:.100 000 000 2s 936ms (articles par ms: 34059.9)
.:.250 000 000 8s 300ms (articles par ms: 30120.5)
.:.400 000 000 12s 611ms (articles par ms: 31718.3)
.:.500 000 000 16s 428ms (articles par ms: 30435.8)
std:: sort (longs) .:.1 000 000 134ms (articles par ms: 7462.69)
.:.5 000 000 716ms (articles par ms: 6983.24)
std:: sort vecteur de longs
1 000 000 511ms (articles par ms: 1956.95)
2 500 000 943ms (articles par ms: 2651.11)
puisque vous avez la méthode supplémentaire il va causer plus d'utilisation de la pile qui finira par ralentir les choses. Pourquoi la médiane de 3 est utilisée, Je ne sais pas, parce que c'est une mauvaise méthode, mais avec des points de pivot aléatoires quicksort n'a jamais de grands problèmes avec uniforme ou il n'y a pas de risque d'une moyenne intentionnelle de 3 données de tueur.
personne ne semble aimer votre algorithme, mais moi si.
Il me semble que c'est une bonne façon de refaire le quicksort classique d'une manière maintenant
sans danger pour une utilisation avec des éléments très répétés.
Vos sous-algorithmes q1 et q2, il me semble que sont en fait le même algorithme
sauf que < et <= opérateurs échangés et quelques autres choses, qui, si vous
wanted vous permettrait d'écrire un pseudo plus court pour cela (bien que pourrait être moins
efficace.) Vous recommandons de les lire
JL Bentley, MD McIlroy: une sorte D'Ingénierie Fonction
SOFTWARE-PRACTICE AND EXPERIENCE 23,11 (Nov 1993)1249-1265
e-disponible ici
http://www.skidmore.edu / ~meckmann / 2009Spring / cs206/papers / spe862jb.pdf
pour voir les tests qu'ils font subir à leur quicksort. Votre idée pourrait être plus agréable et / ou mieux,
mais il a besoin d'exécuter le gauntlet du genre de tests qu'ils ont essayé, en utilisant certains
méthode particulière de choix du pivot. Trouvez-en un qui passe tous les tests sans jamais souffrir de l'exécution quadratique. Puis si en plus votre algorithme est à la fois plus rapide et plus agréable que le leur, vous auriez alors clairement une contribution valable.
la chose "Tukey Ninther" qu'ils utilisent pour générer un pivot me semble Utilisable par vous aussi et il sera automatiquement très difficile pour le pire des temps quadratiques de se produire dans la pratique. Je veux dire, si vous utilisez juste la médiane-de-3 et essayez les éléments du milieu et deux extrémités du tableau comme votre de trois, puis un adversaire, le premier tableau de l'état en augmentant puis en diminuant, vous tomberez sur votre visage avec un temps d'exécution quadratique sur une entrée pas trop invraisemblable. Mais avec Tukey Ninther sur 9 éléments, c'est assez dur pour moi de construire une entrée plausible qui vous blesse avec l'exécution quadratique.
un autre point de vue & une suggestion: Pensez à la combinaison q1 séparant votre tableau, puis q2 séparant le subarray droit, comme un seul algorithme q12 produisant une Division 3-way du tableau. Maintenant, vous devez répéter sur le 3 subarrays (ou seulement 2 si les deux pivots sont égaux). Maintenant toujours sur la plus petite des subarrays sur laquelle vous alliez revenir, D'abord, et le plus grand dernier -- et ne mettez pas en œuvre ce plus grand comme une récursion, mais plutôt rester dans la même routine et boucler de nouveau vers le haut avec une fenêtre rétrécie. De cette façon vous avez 1 appel récursif de moins en q12 que vous auriez, mais le point principal de ceci est, il est maintenant IMPOSSIBLE pour la pile de récursion d'obtenir plus de O(logN) long. OK? Cela résout un autre problème ennuyeux dans le pire des cas quicksort peut souffrir tout en faisant votre code un peu plus rapide de toute façon.