Qu'est-ce qui est le plus efficace pour GCD?

26

Je sais que l'algorithme d'Euclide est le meilleur algorithme pour obtenir le GCD (grand diviseur commun) d'une liste d'entiers positifs. Mais en pratique, vous pouvez coder cet algorithme de différentes manières. (Dans mon cas, j'ai décidé d'utiliser Java, mais C / C ++ peut être une autre option).

J'ai besoin d'utiliser le code le plus efficace possible dans mon programme.

En mode récursif, vous pouvez écrire:

static long gcd (long a, long b){
    a = Math.abs(a); b = Math.abs(b);
    return (b==0) ? a : gcd(b, a%b);
  }

Et en mode itératif, cela ressemble à ceci:

static long gcd (long a, long b) {
  long r, i;
  while(b!=0){
    r = a % b;
    a = b;
    b = r;
  }
  return a;
}

Il y a aussi l'algorithme binaire pour le GCD, qui peut être codé simplement comme ceci:

int gcd (int a, int b)
{
    while(b) b ^= a ^= b ^= a %= b;
    return a;
}
jonaprieto
la source
3
Je pense que c'est trop subjectif, et peut-être même mieux adapté à StackOverflow. "Le plus efficace dans la pratique" dépend de nombreux facteurs (même imprévisibles), tels que l'architecture sous-jacente, la hiérarchie de la mémoire, la taille et la forme de l'entrée, etc.
Juho
5
Il s'agit du même algorithme exprimé de manière récursive et itérative. Je pense que leur différence est négligeable car l'algorithme Euclid converge assez rapidement. Choisissez celui qui correspond à vos préférences.
pad
6
Vous voudrez peut-être essayer de profiler ces deux. Étant donné que la version récursive est un appel de queue, il n'est pas improbable que le compilateur émette en fait presque le même code.
Louis
1
c'est faux. devrait être alors que b! = 0, puis retourner a. Sinon, il bogue sur la division par zéro. n'utilisez pas non plus la récursivité si vous avez de très gros gcds .... vous obtenez une pile d'états de pile et de fonction ... pourquoi ne pas simplement aller itératif?
Cris Stringfellow
4
Notez qu'il existe des algorithmes GCD asymptotiquement plus rapides. Par exemple en.wikipedia.org/wiki/Binary_GCD_algorithm
Neal Young

Réponses:

21

Vos deux algorithmes sont équivalents (au moins pour les entiers positifs, ce qui se passe avec les entiers négatifs dans la version impérative dépend de la sémantique de Java %que je ne connais pas par cœur). Dans la version récursive, soit et b i l'argument du i ème appel récursif: a i + 1 = b i b i + 1 = a i m o d b iunejebjeje

uneje+1=bjebje+1=unejemobje

Dans la version impérative, soit et b i les valeurs des variables et au début de la i ème itération de la boucle. a i + 1 = b i b i + 1 = a i m o d b iunejebjeabje

uneje+1=bjebje+1=unejemobje

Vous remarquez une ressemblance? Votre version impérative et votre version récursive calculent exactement les mêmes valeurs. En outre, ils ont tous deux se terminent au même moment, quand (resp. Un ' i = 0 ), de sorte qu'ils effectuent le même nombre d'itérations. Donc, algorithmiquement parlant, il n'y a pas de différence entre les deux. Toute différence sera une question d'implémentation, très dépendante du compilateur, du matériel sur lequel il s'exécute, et très probablement du système d'exploitation et des autres programmes exécutés simultanément.uneje=0uneje=0

La version récursive ne fait que des appels récursifs de queue . La plupart des compilateurs pour les langages impératifs ne les optimisent pas, et il est donc probable que le code qu'ils génèrent perdra un peu de temps et de mémoire à construire un cadre de pile à chaque itération. Avec un compilateur qui optimise les appels de queue (les compilateurs pour les langages fonctionnels le font presque toujours), le code machine généré peut bien être le même pour les deux (en supposant que vous harmonisez ces appels à abs).

Gilles 'SO- arrête d'être méchant'
la source
8

Pour les nombres petits, l'algorithme GCD binaire est suffisant.

GMP, une bibliothèque bien entretenue et testée dans le monde réel, passera à un algorithme spécial demi-GCD après avoir passé un seuil spécial, une généralisation de l'algorithme de Lehmer. Lehmer utilise la multiplication matricielle pour améliorer les algorithmes euclidiens standard. Selon les documents, le temps de fonctionnement asymptotique du HGCD et du GCD est O(M(N)*log(N)), où M(N)est le temps pour multiplier deux nombres de N-membres.

Tous les détails sur leur algorithme peuvent être trouvés ici .

qwr
la source
Le lien ne fournit vraiment pas tous les détails, et ne définit même pas ce qu'est un "membre" ...
einpoklum - réintègre Monica
2

Comme je sais, Java ne prend pas en charge l'optimisation de la récursivité de queue en général, mais vous pouvez tester votre implémentation Java pour cela; s'il ne le supporte pas, une simple forboucle devrait être plus rapide, sinon la récursivité devrait être tout aussi rapide. D'un autre côté, ce sont des optimisations de bits, choisissez le code que vous pensez être plus facile et plus lisible.

Je dois également noter que l'algorithme GCD le plus rapide n'est pas l'algorithme d'Euclide, l'algorithme de Lehmer est un peu plus rapide.

PJTraill
la source
Voulez - vous dire que loin que je sache ? Voulez-vous dire que la spécification du langage ne rend pas obligatoire cette optimisation (il serait surprenant que ce soit le cas), ou que la plupart des implémentations ne l'implémentent pas?
PJTraill
1

Tout d'abord, n'utilisez pas la récursivité pour remplacer une boucle serrée. C'est lent. Ne comptez pas sur le compilateur pour l'optimiser. Deuxièmement, dans votre code, vous appelez Math.abs () dans chaque appel récursif, ce qui est inutile.

Dans votre boucle, vous pouvez facilement éviter les variables temporaires et permuter tout le temps a et b.

int gcd(int a, int b){
    if( a<0 ) a = -a;
    if( b<0 ) b = -b;
    while( b!=0 ){
        a %= b;
        if( a==0 ) return b;
        b %= a;
    }
    return a;
}

L'échange à l'aide de a ^ = b ^ = a ^ = b raccourcit la source mais nécessite de nombreuses instructions à exécuter. Il sera plus lent que l'échange ennuyeux avec une variable temporaire.

Florian F
la source
3
«Évitez la récursivité. C'est lent »- présenté comme un conseil général, c'est faux. Cela dépend du compilateur. Habituellement, même avec des compilateurs qui n'optimisent pas la récursivité, ce n'est pas lent, il suffit de consommer de la pile.
Gilles 'SO- arrête d'être méchant'
3
Mais pour un code court comme celui-ci, la différence est significative. Consommer de la pile signifie écrire et lire dans la mémoire. C'est lent. Le code ci-dessus fonctionne sur 2 registres. La récursivité signifie également faire des appels, ce qui est plus long qu'un saut conditionnel. Un appel récursif est beaucoup plus difficile pour la prédiction de branche et plus difficile à aligner.
Florian F
-2

Pour les petits nombres ,% est une opération assez coûteuse, peut-être la récursive la plus simple

GCD[a,b] := Which[ 
   a==b , Return[a],
   b > a, Return[ GCD[a, b-a]],
   a > b, Return[ GCD[b, a-b]]
];

est plus rapide? (Désolé, code Mathematica et non C ++)

Per Alexandersson
la source
Ça n'a pas l'air bien. Pour b == 1, il devrait retourner 1. Et GCD [2,1000000000] sera lent.
Florian F
Ah, oui, j'ai fait une erreur. Fixé (je pense) et clarifié.
Per Alexandersson
Normalement, GCD [a, 0] devrait également renvoyer a. La vôtre boucle pour toujours.
Florian F
Je vote en aval car votre réponse ne contient que du code. Nous aimons nous concentrer sur les idées de ce site. Par exemple, pourquoi% est-il une opération coûteuse? La spéculation sur un morceau de code n'est pas vraiment une bonne réponse pour ce site, à mon avis.
Juho
1
Je pense que l'idée que le modulo est plus lent que la soustraction peut être considérée comme du folklore. Cela vaut à la fois pour les petits entiers (la soustraction prend généralement un cycle, le modulo rarement) et pour les grands entiers (la soustraction est linéaire, je ne sais pas quelle est la meilleure complexité pour le modulo mais c'est certainement pire que cela). Bien sûr, vous devez également prendre en compte le nombre d'itérations nécessaires.
Gilles 'SO- arrête d'être méchant'
-2

L'algorithme Euclid est le plus efficace pour calculer GCD:

GCD long statique (long a, long b)
{
si (b == 0)
retourner un;
autre
retourner gcd (, a% b);
}

Exemple:-

Soit A = 16, B = 10.
GCD (16, 10) = GCD (10, 16% 10) = GCD (10, 6)
GCD (10, 6) = GCD (6, 10% 6) = GCD (6, 4)
GCD (6, 4) = GCD (4, 6% 4) = GCD (4, 2)
GCD (4, 2) = GCD (2, 4% 2) = GCD (2, 0)


Puisque B = 0, GCD (2, 0) renverra 2. 
Rohit-Pandey
la source
4
Cela ne répond pas à la question. Le demandeur présente deux versions d'Euclid et demande laquelle est la plus rapide. Vous ne semblez pas l'avoir remarqué et déclarez simplement que la version récursive est le seul algorithme d'Euclid, et affirmez sans aucune preuve que c'est plus rapide que tout le reste.
David Richerby