Dans les algorithmes de division et de conquête tels que le tri rapide et le fusionnement, l'entrée est généralement (au moins dans les textes d'introduction) divisée en deux, et les deux ensembles de données plus petits sont ensuite traités de manière récursive. Il est logique pour moi que cela facilite la résolution d'un problème si les deux moitiés nécessitent moins de la moitié du travail de traitement de l'ensemble de données. Mais pourquoi ne pas diviser l'ensemble de données en trois parties? Quatre? n ?
Je suppose que le travail de division des données en de nombreux sous-ensembles ne vaut pas la peine, mais je n'ai pas l'intuition de voir que l'on devrait s'arrêter à deux sous-ensembles.
J'ai également vu de nombreuses références au tri rapide à 3 voies. Quand est-ce plus rapide? Qu'est-ce qui est utilisé dans la pratique?
Il est logique pour moi que cela facilite la résolution d'un problème si les deux moitiés nécessitent moins de la moitié du travail de traitement de l'ensemble de données.
Ce n'est pas l'essence des algorithmes de division et de conquête. Habituellement, le fait est que les algorithmes ne peuvent pas du tout "traiter l'ensemble des données". Au lieu de cela, il est divisé en morceaux qui sont triviaux à résoudre (comme le tri de deux nombres), puis ceux-ci sont résolus trivialement et les résultats sont recombinés de manière à fournir une solution pour l'ensemble de données complet.
Mais pourquoi ne pas diviser l'ensemble de données en trois parties? Quatre? n?
Principalement parce que le diviser en plus de deux parties et recombiner plus de deux résultats entraîne une implémentation plus complexe mais ne change pas la caractéristique fondamentale (Big O) de l'algorithme - la différence est un facteur constant et peut entraîner un ralentissement si la division et la recombinaison de plus de 2 sous-ensembles créent une surcharge supplémentaire.
Par exemple, si vous effectuez un tri par fusion à trois, dans la phase de recombinaison, vous devez maintenant trouver le plus grand des 3 éléments pour chaque élément, ce qui nécessite 2 comparaisons au lieu de 1, vous ferez donc deux fois plus de comparaisons dans l'ensemble . En échange, vous réduisez la profondeur de récursivité d'un facteur ln (2)/ln (3) == 0,63, vous avez donc 37% de swaps en moins, mais 2 * 0,63 == 26% de comparaisons (et accès mémoire) en plus. Que ce soit bon ou mauvais dépend de ce qui est le plus cher dans votre matériel.
J'ai également vu de nombreuses références au tri rapide à 3 voies. Quand est-ce plus rapide?
Apparemment, une variante à double pivot de quicksort peut nécessiter le même nombre de comparaisons mais en moyenne 20% de swaps en moins, c'est donc un gain net.
Qu'est-ce qui est utilisé dans la pratique?
De nos jours, presque personne ne programme plus ses propres algorithmes de tri; ils utilisent celui fourni par une bibliothèque. Par exemple, Java 7 API utilise en fait le tri rapide à double pivot.
Les personnes qui programment réellement leur propre algorithme de tri pour une raison quelconque auront tendance à s'en tenir à la variante simple à 2 voies, car moins de risques d'erreurs surpassent la plupart du temps 20% de meilleures performances. N'oubliez pas: l'amélioration de performance la plus importante est de loin lorsque le code passe de "ne fonctionne pas" à "fonctionne".
Asymptotiquement parlant, cela n'a pas d'importance. Par exemple, la recherche binaire rend approximativement le journal2n comparaisons, et la recherche ternaire rend approximativement log3n comparaisons. Si vous connaissez vos logarithmes, vous connaissez ce journalunex = logbx/logba, donc la recherche binaire ne fait qu'environ 1/log32 ≈ 1,5 fois plus de comparaisons que la recherche ternaire. C'est aussi la raison pour laquelle personne ne spécifie jamais la base du logarithme en grande notation Oh: c'est toujours un facteur constant loin du logarithme dans une base donnée, quelle que soit la base réelle. Ainsi, la division du problème en plusieurs sous-ensembles n'améliore pas la complexité temporelle et ne suffit pratiquement pas à l'emporter sur la logique plus complexe. En fait, cette complexité peut affecter négativement les performances pratiques, augmenter la pression du cache ou rendre les micro-optimisations moins irréalisables.
D'un autre côté, certaines structures de données arborescentes utilisent un facteur de ramification élevé (beaucoup plus grand que 3, souvent 32 ou plus), mais généralement pour d'autres raisons. Il améliore l'utilisation de la hiérarchie de la mémoire: les structures de données stockées dans RAM font un meilleur usage du cache, les structures de données stockées sur disque nécessitent moins de lectures HDD-> RAM.
Il existe des algorithmes de recherche/tri qui se subdivisent non pas par deux, mais par N.
Un exemple simple est la recherche par codage de hachage, qui prend O(1) temps).
Si la fonction de hachage préserve l'ordre, elle peut être utilisée pour créer un algorithme de tri O(N). (Vous pouvez considérer n'importe quel algorithme de tri comme faisant simplement N recherche l'endroit où un nombre doit aller dans le résultat.)
Le problème fondamental est, lorsqu'un programme examine certaines données et entre ensuite dans certains états suivants, combien y a-t-il d'états suivants et à quel point leurs probabilités sont-elles égales?
Lorsqu'un ordinateur fait une comparaison de deux nombres, disons, puis saute ou non, si les deux chemins sont également probables, le compteur de programme "connaît" un bit d'information de plus sur chaque chemin, donc en moyenne il en a "appris" un bit. Si un problème nécessite que M bits soient appris, alors en utilisant des décisions binaires, il ne peut pas obtenir la réponse en moins de M décisions. Ainsi, par exemple, la recherche d'un nombre dans une table triée de taille 1024 ne peut pas être effectuée en moins de 10 décisions binaires, ne serait-ce que parce que moins de résultats n'auraient pas suffisamment de résultats, mais cela peut certainement être fait en plus.
Lorsqu'un ordinateur prend un nombre et le transforme en index en tableau, il "apprend" à enregistrer la base 2 du nombre d'éléments dans le tableau, et il le fait en temps constant. Par exemple, s'il existe une table de sauts de 1024 entrées, toutes plus ou moins également probables, le fait de sauter dans cette table "apprend" 10 bits. C'est l'astuce fondamentale derrière le codage de hachage. Un exemple de tri de ceci est la façon dont vous pouvez trier un jeu de cartes. Avoir 52 bacs, un pour chaque carte. Jetez chaque carte dans son bac, puis récupérez-les toutes. Aucune subdivision requise.
Étant donné qu'il s'agit d'une question de division et de conquête générale, et pas seulement de tri, je suis surpris que personne n'ait soulevé le Master Theorem
En bref, le temps d'exécution des algorithmes de division et de conquête est déterminé par deux forces de contre-dévoilement: l'avantage que vous obtenez de transformer des problèmes plus importants en petits problèmes et le prix à payer pour résoudre plus de problèmes. Selon les détails de l'algorithme, il peut être payant ou non de diviser un problème en plus de deux morceaux. Si vous divisez le même nombre de sous-problèmes à chaque étape et que vous connaissez la complexité temporelle de la combinaison des résultats à chaque étape, le théorème maître vous indiquera la complexité temporelle de l'algorithme global.
L'algorithme Karatsuba pour la multiplication utilise une division et une conquête à 3 voies pour atteindre un temps d'exécution de O (3 n ^ log_2 3) qui bat le O (n ^ 2) pour l'algorithme de multiplication ordinaire (n est le nombre de chiffres dans les nombres).