Une manière simple et propre de comparer trois nombres

11

J'ai du code qui a une séquence de ifs qui fonctionne, mais je me sens juste en désordre. Fondamentalement, je veux choisir le plus grand des trois entiers et définir un indicateur d'état pour indiquer celui qui a été choisi. Mon code actuel ressemble à ceci:

a = countAs();
b = countBs();
c = countCs();

if (a > b && a > c)
    status = MOSTLY_A;
else if (b > a && b > c)
    status = MOSTLY_B;
else if (c > a && c > b)
    status = MOSTLY_C;
else
    status = DONT_KNOW;

Ce modèle se produit plusieurs fois, et avec des noms de variables longs, il devient un peu difficile de confirmer visuellement que chacun ifest correct. Je pense qu'il pourrait y avoir un moyen meilleur et plus clair de le faire; quelqu'un peut-il suggérer quelque chose?


Il existe des doublons potentiels, mais ils ne correspondent pas tout à fait à cette question.

Dans le doublon suggéré: Approches pour vérifier plusieurs conditions? toutes les solutions suggérées semblent aussi maladroites que le code d'origine, donc elles ne fournissent pas une meilleure solution.

Et ce post Des façons élégantes de gérer si (sinon) d'autre ne concerne que les niveaux d'imbrication et l'asymétrie, ce qui n'est pas le problème ici.

Ken YN
la source
3
Pour moi, le seul problème est la répétition du code. Ce que vous avez ici est extrêmement clair à lire, pourquoi changer cela? Décomposez-le simplement en une fonction qui prend en a, b, c et renvoie le statut. Si vous vous sentez mieux, déposez un "inline" dessus. Pas de macros, pas de complexité, juste une bonne extraction des anciennes fonctions. Je vous remercierai pour votre code clair si j'ai besoin de travailler avec lui plus tard.
J Trana
reproduction possible d' approches pour vérifier plusieurs conditions?
moucher
Notez que vos #defines sont mal nommés. Considérez a = 40, b = 30, c = 30. Le résultat est MOSTLY_A. Mais la plupart des choses ne sont en fait pas A, mais B ou C. Vous voudrez peut-être essayer MORE_A (bien que toujours ambigu - plus A que B et plus A que C, ou plus A que B et C combinés?), Ou A_LARGEST, MAX_IS_A, ...? (qui suggère également max (a, max (b, c)) comme réponse à la question).
tony

Réponses:

12

Factoriser la logique, revenir tôt

Comme suggéré dans les commentaires, il suffirait de simplement encapsuler votre logique dans une fonction et de sortir tôt avec returnles afin de simplifier beaucoup les choses. De plus, vous pouvez factoriser un peu de fonctionnalité en déléguant des tests à une autre fonction. Plus concrètement:

bool mostly(max,u,v) {
   return max > u && max > v;
}

status_t strictly_max_3(a,b,c)
{
  if mostly(a,b,c) return MOSTLY_A;
  if mostly(b,a,c) return MOSTLY_B;
  if mostly(c,a,b) return MOSTLY_C;
  return DONT_KNOW;
}

C'est plus court que ma précédente tentative:

status_t index_of_max_3(a,b,c)
{
  if (a > b) {
    if (a == c)
      return DONT_KNOW;
    if (a > c)
      return MOSTLY_A;
    else
      return MOSTLY_C;
  } else {
    if (a == b)
      return DONT_KNOW;
    if (b > c)
      return MOSTLY_B;
    else
      return MOSTLY_C;
  }
}

Ce qui précède est un peu plus détaillé, mais il est facile à lire à mon humble avis et ne recalcule pas les comparaisons plusieurs fois.

Confirmation visuelle

Dans votre réponse, vous dites:

mon problème était surtout la confirmation visuelle que toutes les comparaisons utilisaient les mêmes variables

... aussi, dans votre question, vous dites:

Ce modèle se produit plusieurs fois, et avec de longs noms de variables, il devient un peu difficile de confirmer visuellement que chaque if est correct.

Je ne comprends peut-être pas ce que vous essayez de réaliser: voulez-vous copier-coller le motif partout où vous en avez besoin? Avec une fonction comme celle ci-dessus, vous capturez le motif une fois, et vous vérifiez une fois pour tout ce que toutes les comparaisons utilisent a, bet ccomme requis. Ensuite, vous n'avez plus à vous inquiéter lorsque vous appelez la fonction. Bien sûr, dans la pratique, votre problème est peut-être un peu plus complexe que celui que vous avez décrit: si c'est le cas, veuillez ajouter des détails si possible.

coredump
la source
1
Je ne comprends pas votre commentaire sur DONT_KNOW , que se passe -t-il si c est le plus petit et a et b sont les mêmes? L'algorithme retournerait que b est le plus grand, tandis que a est le même que b.
Pieter B
@PieterB J'étais wronlgy en supposant qu'il ne serait pas question si nous sommes retournés soit MOSTLY_Aou MOSTLY_Cen cas a == cet a > b. C'est corrigé. Merci.
coredump
@DocBrown Certes, la plupart des avantages proviennent du comportement de sortie précoce.
coredump
1
+1, il s'agit désormais d'une amélioration par rapport au code d'origine des PO.
Doc Brown
9

TL: DR; Votre code est déjà correct et "propre".

Je vois beaucoup de gens se débattre autour de la réponse mais tout le monde manque la forêt à travers les arbres. Faisons toute l'informatique et l'analyse mathématique pour bien comprendre cette question.

Tout d'abord, nous notons que nous avons 3 variables, chacune avec 3 états: <, = ou>. Le nombre total de permutations est de 3 ^ 3 = 27 états, auxquels j'attribuerai un numéro unique, noté P #, pour chaque état. Ce nombre P # est un système numérique factoriel .

Enumérant toutes les permutations que nous avons:

a ? b | a ? c | b ? c |P#| State
------+-------+-------+--+------------
a < b | a < c | b < c | 0| C
a = b | a < c | b < c | 1| C
a > b | a < c | b < c | 2| C
a < b | a = c | b < c | 3| impossible a<b b<a
a = b | a = c | b < c | 4| impossible a<a
a > b | a = c | b < c | 5| A=C > B
a < b | a > c | b < c | 6| impossible a<c a>c
a = b | a > c | b < c | 7| impossible a<c a>c
a > b | a > c | b < c | 8| A
a < b | a < c | b = c | 9| B=C > A
a = b | a < c | b = c |10| impossible a<a
a > b | a < c | b = c |11| impossible a<c a>c
a < b | a = c | b = c |12| impossible a<a
a = b | a = c | b = c |13| A=B=C
a > b | a = c | b = c |14| impossible a>a
a < b | a > c | b = c |15| impossible a<c a>c
a = b | a > c | b = c |16| impossible a>a
a > b | a > c | b = c |17| A
a < b | a < c | b > c |18| B
a = b | a < c | b > c |19| impossible b<c b>c
a > b | a < c | b > c |20| impossible a<c a>c
a < b | a = c | b > c |21| B
a = b | a = c | b > c |22| impossible a>a
a > b | a = c | b > c |23| impossible c>b b>c
a < b | a > c | b > c |24| B
a = b | a > c | b > c |25| A=B > C
a > b | a > c | b > c |26| A

Par inspection, nous voyons que nous avons:

  • 3 états où A est le maximum,
  • 3 états où B est le maximum,
  • 3 états où C est le maximum, et
  • 4 états où A = B ou B = C.

Écrivons un programme (voir la note de bas de page) pour énumérer toutes ces permutations avec des valeurs pour A, B et C. Tri stable par P #:

a ?? b | a ?? c | b ?? c |P#| State
1 <  2 | 1 <  3 | 2 <  3 | 0| C
1 == 1 | 1 <  2 | 1 <  2 | 1| C
1 == 1 | 1 <  3 | 1 <  3 | 1| C
2 == 2 | 2 <  3 | 2 <  3 | 1| C
2  > 1 | 2 <  3 | 1 <  3 | 2| C
2  > 1 | 2 == 2 | 1 <  2 | 5| ??
3  > 1 | 3 == 3 | 1 <  3 | 5| ??
3  > 2 | 3 == 3 | 2 <  3 | 5| ??
3  > 1 | 3  > 2 | 1 <  2 | 8| A
1 <  2 | 1 <  2 | 2 == 2 | 9| ??
1 <  3 | 1 <  3 | 3 == 3 | 9| ??
2 <  3 | 2 <  3 | 3 == 3 | 9| ??
1 == 1 | 1 == 1 | 1 == 1 |13| ??
2 == 2 | 2 == 2 | 2 == 2 |13| ??
3 == 3 | 3 == 3 | 3 == 3 |13| ??
2  > 1 | 2  > 1 | 1 == 1 |17| A
3  > 1 | 3  > 1 | 1 == 1 |17| A
3  > 2 | 3  > 2 | 2 == 2 |17| A
1 <  3 | 1 <  2 | 3  > 2 |18| B
1 <  2 | 1 == 1 | 2  > 1 |21| B
1 <  3 | 1 == 1 | 3  > 1 |21| B
2 <  3 | 2 == 2 | 3  > 2 |21| B
2 <  3 | 2  > 1 | 3  > 1 |24| B
2 == 2 | 2  > 1 | 2  > 1 |25| ??
3 == 3 | 3  > 1 | 3  > 1 |25| ??
3 == 3 | 3  > 2 | 3  > 2 |25| ??
3  > 2 | 3  > 1 | 2  > 1 |26| A

Au cas où vous vous demandiez comment je savais quels états P # étaient impossibles, maintenant vous savez. :-)

Le nombre minimum de comparaisons pour déterminer la commande est:

Log2 (27) = Log (27) / Log (2) = ~ 4,75 = 5 comparaisons

c'est-à-dire que coredump a donné le nombre minimal correct de 5 comparaisons. Je formaterais son code comme:

status_t index_of_max_3(a,b,c)
{
    if (a > b) {
        if (a == c) return DONT_KNOW; // max a or c
        if (a >  c) return MOSTLY_A ;
        else        return MOSTLY_C ;
    } else {
        if (a == b) return DONT_KNOW; // max a or b
        if (b >  c) return MOSTLY_B ;
        else        return MOSTLY_C ;
    }
}

Pour votre problème, nous ne nous soucions pas de tester l'égalité, nous pouvons donc omettre 2 tests.

Peu importe à quel point le code est propre / mauvais s'il obtient la mauvaise réponse, c'est donc un bon signe que vous gérez tous les cas correctement!

Ensuite, en ce qui concerne la simplicité, les gens continuent d'essayer "d'améliorer" la réponse, où ils pensent qu'améliorer signifie "optimiser" le nombre de comparaisons, mais ce n'est pas strictement ce que vous demandez. Vous avez confondu tout le monde lorsque vous avez demandé "Je pense qu'il pourrait y avoir un meilleur", mais n'avez pas défini ce que signifie "mieux". Moins de comparaisons? Moins de code? Des comparaisons optimales?

Maintenant, puisque vous posez des questions sur la lisibilité du code (étant donné l'exactitude), je ne ferais qu'une seule modification à votre code pour la lisibilité: Alignez le premier test avec les autres.

        if      (a > b && a > c)
            status = MOSTLY_A;
        else if (b > a && b > c)
            status = MOSTLY_B;
        else if (c > a && c > b)
            status = MOSTLY_C;
        else
            status = DONT_KNOW; // a=b or b=c, we don't care

Personnellement, je l'écrirais de la manière suivante, mais cela peut être trop peu orthodoxe pour vos normes de codage:

        if      (a > b && a > c) status = MOSTLY_A ;
        else if (b > a && b > c) status = MOSTLY_B ;
        else if (c > a && c > b) status = MOSTLY_C ;
        else /*  a==b  || b ==c*/status = DONT_KNOW; // a=b or b=c, we don't care

Note de bas de page: Voici le code C ++ pour générer les permutations:

#include <stdio.h>

char txt[]  = "< == > ";
enum cmp      { LESS, EQUAL, GREATER };
int  val[3] = { 1, 2, 3 };

enum state    { DONT_KNOW, MOSTLY_A, MOSTLY_B, MOSTLY_C };
char descr[]= "??A B C ";

cmp Compare( int x, int y ) {
    if( x < y ) return LESS;
    if( x > y ) return GREATER;
    /*  x==y */ return EQUAL;
}

int main() {
    int i, j, k;
    int a, b, c;

    printf( "a ?? b | a ?? c | b ?? c |P#| State\n" );
    for( i = 0; i < 3; i++ ) {
        a = val[ i ];
        for( j = 0; j < 3; j++ ) {
            b = val[ j ];
            for( k = 0; k < 3; k++ ) {
                c = val[ k ];

                int cmpAB = Compare( a, b );
                int cmpAC = Compare( a, c );
                int cmpBC = Compare( b, c );
                int n     = (cmpBC * 9) + (cmpAC * 3) + cmpAB; // Reconstruct unique P#

                printf( "%d %c%c %d | %d %c%c %d | %d %c%c %d |%2d| "
                    , a, txt[cmpAB*2+0], txt[cmpAB*2+1], b
                    , a, txt[cmpAC*2+0], txt[cmpAC*2+1], c
                    , b, txt[cmpBC*2+0], txt[cmpBC*2+1], c
                    , n
                );

                int status;
                if      (a > b && a > c) status = MOSTLY_A;
                else if (b > a && b > c) status = MOSTLY_B;
                else if (c > a && c > b) status = MOSTLY_C;
                else /*  a ==b || b== c*/status = DONT_KNOW; // a=b, or b=c

                printf( "%c%c\n", descr[status*2+0], descr[status*2+1] );
            }
        }
    }
    return 0;
}

Modifications: sur la base des commentaires, déplacé TL: DR vers le haut, supprimé la table non triée, clarifié 27, nettoyé le code, décrit les états impossibles.

Michaelangel007
la source
-1: la réduction du nombre de décisions ne conduirait-elle pas à des chemins de code plus simples et à un code plus lisible? Votre argument n'est pas clair: d'abord, vous dites que tout le monde a tort; ensuite, vous mettez non pas une ou deux mais trois tables; J'espérais qu'ils mèneraient à un moyen plus simple de calculer le résultat, mais à la place, vous avez confirmé ce que tout le monde savait déjà (le code OP fait la bonne chose). Bien sûr, la question porte sur la lisibilité, mais la lisibilité n'est pas obtenue uniquement en modifiant la disposition du code (vous admettez que vos modifications ne correspondraient guère aux normes de code existantes). Il est logique de simplifier la logique lors de l'optimisation de la lisibilité.
coredump
Plus constructivement: je suggère de simplifier votre réponse en omettant certains détails et en réfléchissant à la structure de votre réponse. J'apprécie que vous ayez pris le temps d'écrire et de publier les permutations générant du code C ++, mais peut-être pourriez-vous donner le résultat principal et une seule table: tel qu'il est maintenant, il semble que vous ayez vidé tout votre travail tel quel. J'ai presque échoué à repérer le truc TL; DR (vous pouvez commencer par ça). J'espère que ça aide.
coredump
2
Merci pour le feedback constructif de coredump. J'ai supprimé la table non triée du milieu car elle est facilement vérifiée.
Michaelangel007
2
Jésus Christ! Qui dirait que comparer trois nombres est presque aussi complexe que la science des fusées?
Mandrill
@Mandrill Comme les informaticiens , il nous appartient de comprendre un problème à fond . Ce n'est qu'en énumérant les 27 permutations possibles pour une comparaison à 3 voies que nous pouvons vérifier que notre solution fonctionne dans TOUS les cas. La dernière chose que nous voulons en tant que programmeurs sont des bogues cachés et des cas marginaux non comptabilisés. La verbosité est le prix à payer pour l'exactitude.
Michaelangel007
5

@msw vous a dit d'utiliser un tableau au lieu de a, b, c et @Basile vous a dit de refactoriser la logique "max" en fonction. La combinaison de ces deux idées conduit à

val[0] = countAs();    // in the real code, one should probably define 
val[1] = countBs();    // some enum for the indexes 0,1,2 here
val[2] = countCs();

 int result[]={DONT_KNOW, MOSTLY_A, MOSTLY_B, MOSTLY_C};

fournir ensuite une fonction qui calcule l'indice max d'un tableau arbitraire:

// returns the index of the strict maximum, and -1 when the maximum is not strict
int FindStrictMaxIndex(int *values,int arraysize)
{
    int maxVal=INT_MIN;
    int maxIndex=-1;
    for(int i=0;i<arraysize;++i)
    {
       if(values[i]>maxVal)
       {
         maxVal=values[i];
         maxIndex=i;
       }
       else if (values[i]==maxVal)
       {
         maxIndex=-1;
       }
    }
    return maxIndex;
}

et l'appelle comme

 return result[FindStrictMaxIndex(val,3)+1];

Le nombre total de LOC semble avoir augmenté par rapport à celui d'origine, mais maintenant vous avez la logique principale dans une fonction réutilisable, et si vous pouvez réutiliser la fonction plusieurs fois, cela commence à porter ses fruits. De plus, la FindStrictMaxIndexfonction n'est plus liée à vos "exigences métiers" (séparation des préoccupations), donc le risque que vous aurez à la modifier ultérieurement est beaucoup plus faible que dans votre version d'origine (principe ouvert-fermé). Par exemple, cette fonction ne devra pas être modifiée même si le nombre d'arguments change, ou si vous devez utiliser d'autres valeurs de retour que MOSTLY_ABC, ou si vous traitez d'autres variables que a, b, c. De plus, l'utilisation d'un tableau au lieu de 3 valeurs différentes a, b, c pourrait également simplifier votre code à d'autres endroits.

Bien sûr, si dans tout votre programme il n'y a qu'un ou deux endroits pour appeler cette fonction et que vous n'avez pas d'autres applications pour conserver les valeurs dans un tableau, je laisserais probablement le code d'origine tel quel (ou utiliser @ amélioration de coredump).

Doc Brown
la source
J'aime ça - les tripes de FindStrictMaxIndex()ne sont peut-être pas trop propres, mais du point de vue de l'appelant, il est raisonnablement évident de ce qui est en train d'être réalisé.
Ken YN
Ou au lieu de contenir deux tableaux, maintenez un tableau de paires clé-valeur: {MOSTLY_A, countAs ()}, prenez le premier élément ordonné par valeur et lisez la clé.
Julia Hayward
@JuliaHayward: la principale raison pour laquelle je n'ai pas suggéré une telle solution était la balise "C" de la question - en C, il faudra un peu plus de code passe-partout pour gérer les paires clé-valeur, et créer une fonction typée en termes de KVP ne sera probablement pas aussi réutilisable dans différents contextes qu'une simple intfonction typée. Mais j'accepte votre commentaire si l'on utilise un langage différent comme Python ou Perl.
Doc Brown
1
@ gnasher729: cela dépend du nombre de "doublons" dans le code d'origine, de leur similitude réelle et de la fréquence de FindStrictMaxIndexréutilisation de la fonction. Pour une ou deux fois de réutilisation, cela ne sera pas payant, bien sûr, mais c'est ce que j'ai déjà écrit dans ma réponse. Notez également les autres avantages que j'ai mentionnés ci-dessus concernant les changements futurs.
Doc Brown
1
... et notez que les 8 lignes d'origine peuvent être remplacées par une simple doublure return result[FindStrictMaxIndex(val,3)]; au point de code où les 8 lignes d'origine ont été placées . Les autres parties, en particulier FindStrictMaxIndexelle-même, sont complètement séparées de la "logique métier", ce qui les éloigne de l'objectif de l'évolution des exigences métier.
Doc Brown
-1

Vous devriez probablement utiliser une macro ou une fonction MAX donnant au maximum deux nombres.

Alors vous voulez juste:

 status = MAX(a,MAX(b,c));

Vous avez peut-être défini

 #define MAX(X,Y) (((X)>(Y))?(X):(Y))

mais soyez prudent - notamment sur les effets secondaires - lorsque vous utilisez des macros (car MAX(i++,j--) cela se comporterait étrangement)

Mieux vaut donc définir une fonction

 static inline int max2ints(int x, int y) {
    return (x>y)?x:y;
 }

et l'utiliser (ou au moins #define MAX(X,Y) max2ints((X),(Y))....)

Si vous avez besoin de comprendre l'origine du MAX, vous pourriez avoir une longue macro comme celle #define COMPUTE_MAX_WITH_CAUSE(Status,Result,X,Y,Z) qui est une longue do{... }while(0) macro, peut-être

#define COMPUTE_MAX_WITH_CAUSE(Status,Result,X,Y,Z) do { \
  int x= (X), y= (Y), z=(Z); \
  if (x > y && y > z) \
    { Status = MOSTLY_FIRST; Result = x; } \
  else if (y > x && y > z) \
    { Status = MOSTLY_SECOND; Result = y; } \
  else if (z > x && z > y) \
    { Status = MOSTLY_THIRD; Result = z; } \
  /* etc */ \
  else { Status = UNKNOWN; Result = ... } \
} while(0)

Ensuite, vous pouvez invoquer COMPUTE_MAX_WITH_CAUSE(status,res,a,b,c) à plusieurs endroits. C'est un peu moche. J'ai défini les variables locales x, y, z pour réduire le mauvais effets secondaires ....

Basile Starynkevitch
la source
2
Refactoriser la logique commune dans une fonction est la bonne approche, mais j'éviterais en fait deux choses ici: 1. Je n'inventerais pas de nouvelles exigences (le PO n'a pas demandé le calcul du maximum). Et 2e: même si le code résultant peut devenir plus SEC, il est très discutable si cela justifie une macro compliquée.
Doc Brown
1
Les macros devraient être un outil de dernier recours. Certainement hors limites pour ce problème.
kevin cline
-1

J'ai plus réfléchi à cela, donc comme mon problème était principalement une confirmation visuelle que toutes les comparaisons utilisaient les mêmes variables, je pense que cela pourrait être une approche utile:

a = countAs();
b = countBs();
c = countCs();

if (FIRST_IS_LARGEST(a, b, c))
    status = MOSTLY_A;
else if (SECOND_IS_LARGEST(a, b, c))
    status = MOSTLY_B;
else if (THIRD_IS_LARGEST(a, b, c))
    status = MOSTLY_C;
else
    status = DONT_KNOW; /* NO_SINGLE_LARGEST is a better name? */

Que chaque macro prend a, bet cdans le même ordre est facile à confirmer, et le nom de la macro me évite d' avoir à travailler à ce que toutes les comparaisons et ANDs font.

Ken YN
la source
1
(1) pourquoi des macros auxiliaires au lieu de fonctions? (2) pourquoi avez-vous besoin d'une confirmation visuelle ici? est-ce vraiment votre problème principal ou la nécessité d'une confirmation visuelle est-elle une conséquence de la duplication de code? Votre meilleure option est de factoriser votre code en une fonction simple et simple que vous vérifiez une fois pour toutes .
coredump