Maneira simples e limpa de comparar três números

11

Eu tenho algum código que tem uma sequência de ifs que funciona, mas me sinto confuso. Basicamente, quero escolher o maior de três números inteiros e definir um sinalizador de status para dizer qual foi escolhido. Meu código atual é assim:

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;

Esse padrão ocorre algumas vezes e, com nomes de variáveis ​​longos, fica um pouco difícil confirmar visualmente que cada um ifestá correto. Sinto que pode haver uma maneira melhor e mais clara de fazer isso; alguém pode sugerir alguma coisa?


Existem algumas duplicatas em potencial, mas elas não estão alinhadas com essa pergunta.

Na duplicata sugerida: Abordagens para verificar várias condições? todas as soluções sugeridas parecem igualmente desajeitadas como o código original, portanto, elas não fornecem uma solução melhor.

E este post Maneiras elegantes de lidar com se (se houver) outra coisa lida apenas com níveis de aninhamento e assimetria, que não é o problema aqui.

Ken YN
fonte
3
Para mim, o único problema é a repetição de código. O que você tem aqui é extremamente claro para ler, por que mudar isso? Basta dividi-lo em uma função que recebe o status a, b, ce retorna. Se você faz você se sentir melhor, solte um "inline" nele. Sem macros, sem complexidade, apenas boa extração de funções antigas. Agradeço o seu código claro, se precisar trabalhar com ele mais tarde.
J Trana
possível duplicata de abordagens para verificar várias condições?
mosquito
Observe que seus #defines são mal nomeados. Considere a = 40, b = 30, c = 30. O resultado é MOSTLY_A. Mas a maioria das coisas na verdade não é A, mas B ou C. Você pode tentar MORE_A (embora ainda ambíguo - mais A que B e mais A que C, ou mais A que B e C combinadas?) Ou A_LARGEST, MAX_IS_A, ...? (que também sugere max (a, max (b, c)) como resposta à pergunta).
tony

Respostas:

12

Fatorar lógica, retornar cedo

Conforme sugerido nos comentários, seria suficiente simplesmente agrupar sua lógica em uma função e sair mais cedo da com returna fim de simplificar bastante as coisas. Além disso, você pode fatorar um pouco da funcionalidade, delegando testes a outra função. Mais concretamente:

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;
}

Isso é mais curto que minha tentativa anterior:

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;
  }
}

O texto acima é um pouco mais detalhado, mas é fácil de ler o IMHO e não recalcula as comparações várias vezes.

Confirmação visual

Na sua resposta, você diz:

meu problema foi principalmente a confirmação visual de que todas as comparações usavam as mesmas variáveis

... também, na sua pergunta, você diz:

Esse padrão ocorre algumas vezes e, com nomes de variáveis ​​longos, fica um pouco difícil confirmar visualmente se cada um está correto.

Talvez eu não entenda o que você está tentando alcançar: deseja copiar e colar o padrão em todos os lugares que precisar? Com uma função como o descrito acima, você captura o padrão de uma vez, e você verificar uma vez por todas que todas as comparações usar a, be cconforme necessário. Então, você não precisa mais se preocupar quando chamar a função. Claro, talvez na prática o seu problema seja um pouco mais complexo do que o que você descreveu: se assim for, adicione alguns detalhes, se possível.

coredump
fonte
1
Não entendo seu comentário sobre DONT_KNOW , e se c for o menor e aeb forem iguais? O algoritmo retornaria que b é o maior, enquanto a é o mesmo que b.
Pieter B
@PieterB Eu estava errado assumindo que não importaria se retornássemos MOSTLY_Aou MOSTLY_Cno caso a == ce a > b. Isso está consertado. Obrigado.
coredump
@DocBrown Concedido, a maioria dos benefícios advém do comportamento de saída antecipada.
coredump
1
+1, agora é realmente uma melhoria em relação ao código original dos OPs.
Doc Brown
9

TL: DR; Seu código já está correto e "limpo".

Vejo muitas pessoas questionando a resposta, mas todo mundo está sentindo falta da floresta entre as árvores. Vamos fazer a análise completa da ciência da computação e matemática para entender completamente esta questão.

Primeiro, observamos que temos 3 variáveis, cada uma com 3 estados: <, = ou>. O número total de permutações é 3 ^ 3 = 27 estados, aos quais atribuirei um número único, denotado P #, para cada estado. Esse número P # é um sistema de números fatoriais .

Enumerando todas as permutações que temos:

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

Por inspeção, vemos que temos:

  • 3 estados em que A é o máximo,
  • 3 estados onde B é o máximo,
  • 3 estados onde C é o máximo e
  • 4 estados em que A = B ou B = C.

Vamos escrever um programa (veja a nota de rodapé) para enumerar todas essas permutações com valores para A, B e C. Classificação estável por 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

Caso você estivesse se perguntando como eu sabia quais estados P # eram impossíveis, agora você sabe. :-)

O número mínimo de comparações para determinar a ordem é:

Log2 (27) = Log (27) / Log (2) = ~ 4,75 = 5 comparações

isto é, o coredump deu o número mínimo correto de 5 comparações. Eu formataria seu código como:

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 ;
    }
}

Para o seu problema, não nos preocupamos em testar a igualdade, para que possamos omitir dois testes.

Não importa o quão limpo / ruim o código seja, se ele receber a resposta errada, é um bom sinal de que você está lidando com todos os casos corretamente!

Em seguida, quanto à simplicidade, as pessoas continuam tentando "melhorar" a resposta, onde pensam que melhorar significa "otimizar" o número de comparações, mas isso não é exatamente o que você está perguntando. Você confundiu todo mundo onde perguntou "Sinto que pode haver um melhor", mas não definiu o que "melhor" significa. Menos comparações? Menos código? Comparações ótimas?

Agora, como você está perguntando sobre a legibilidade do código (dada a correção), eu faria apenas uma alteração no seu código para facilitar a leitura: Alinhe o primeiro teste com os outros.

        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

Pessoalmente, eu escreveria da seguinte maneira, mas isso pode ser pouco ortodoxo para seus padrões de codificação:

        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

Nota de rodapé: Aqui está o código C ++ para gerar as permutações:

#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;
}

Edições: com base no feedback, moveu TL: DR para o topo, removeu a tabela não classificada, esclareceu 27, limpou o código, descreveu estados impossíveis.

Michaelangel007
fonte
-1: reduzir o número de decisões não levaria a caminhos de código mais simples e código mais legível? Seu argumento não é claro: primeiro, você diz que todo mundo está errado; então, você coloca não uma ou duas, mas três tabelas; Eu esperava que eles levassem a uma maneira mais simples de calcular o resultado, mas você confirmou o que todo mundo já sabia (o código do OP faz a coisa certa). Certamente, a pergunta é sobre legibilidade, mas a legibilidade não é alcançada apenas pela modificação do layout do código (você admite que suas alterações dificilmente se encaixariam nos padrões de código existentes). Faz sentido simplificar a lógica ao otimizar a legibilidade.
Coredump #
Mais construtivamente: sugiro simplificar sua resposta, deixando de fora alguns detalhes e pensando na estrutura da sua resposta. Compreendo que você tenha levado tempo para escrever e publicar as permutações de geração de código C ++, mas talvez você possa fornecer o resultado principal e apenas uma tabela: como é agora, parece que você descartou todo o seu trabalho como está. Quase não consegui identificar a coisa TL; DR (você pode começar com isso). Espero que ajude.
Coredump
2
Obrigado pelo feedback construtivo. Eu removi a tabela do meio sem classificação, pois ela é facilmente verificada.
Michaelangel007
2
Jesus Cristo! Quem diria que comparar três números é quase tão complexo quanto a ciência dos foguetes?
Mandrill
@ Mandrill Como cientistas da computação, é nosso trabalho entender um problema completamente . Somente enumerando todas as 27 permutações possíveis para uma comparação de três vias podemos verificar se nossa solução funciona em TODOS os casos. A última coisa que queremos como programadores são bugs ocultos e casos extremos não contabilizados. A verbosidade é o preço que se paga pela correção.
Michaelangel007
5

O @msw disse para você usar uma matriz em vez de a, b, ce @Basile disse para refatorar a lógica "max" em uma função. A combinação dessas duas idéias leva a

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};

forneça uma função que calcule o índice máximo de uma matriz arbitrária:

// 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;
}

e chame assim

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

O número total de LOC parece ter aumentado em relação ao original, mas agora você tem a lógica principal em uma função reutilizável e, se puder reutilizar a função várias vezes, ela começa a valer a pena. Além disso, a FindStrictMaxIndexfunção não está mais entrelaçada com seus "requisitos de negócios" (separação de interesses); portanto, o risco que você terá de modificá-lo mais tarde é muito menor do que na sua versão original (princípio aberto-fechado). Por exemplo, essa função não precisará ser alterada, mesmo que o número de argumentos seja alterado ou precise usar outros valores de retorno além de MOSTLY_ABC, ou você esteja processando outras variáveis ​​além de a, b, c. Além disso, o uso de uma matriz em vez de 3 valores diferentes a, b, c também pode simplificar seu código em outros lugares.

Obviamente, se em todo o programa houver apenas um ou dois locais para chamar essa função e você não tiver mais aplicativos para armazenar os valores em uma matriz, provavelmente eu deixaria o código original como está (ou use @ melhoria do coredump).

Doc Brown
fonte
Eu gosto disso - as entranhas de FindStrictMaxIndex()podem não estar muito limpas, mas do ponto de vista do interlocutor, é razoavelmente óbvio o que está tentando ser alcançado.
precisa
Ou, em vez de conter duas matrizes, mantenha uma matriz de pares de valores-chave: {MOSTLY_A, countAs ()}, pegue o primeiro elemento ordenado por valor e leia a chave.
Julia Hayward
@JuliaHayward: a principal razão pela qual não sugeri uma solução desse tipo foi a tag "C" da pergunta - em C, será necessário um código mais padronizado para lidar com pares de valores-chave e criar uma função digitada em termos de KVPs provavelmente não será tão reutilizável em contextos diferentes do que uma intfunção digitada simples . Mas concordo com o seu comentário se alguém usar uma linguagem diferente como Python ou Perl.
Doc Brown
1
@ gnasher729: depende de quantas "duplicatas" no código original são, quão semelhantes elas realmente são e com que frequência a FindStrictMaxIndexfunção pode ser reutilizada. Por uma ou apenas duas vezes de reutilização, isso não será recompensado, é claro, mas foi o que eu escrevi na minha resposta. Observe também as outras vantagens que mencionei acima em relação a mudanças futuras.
Doc Brown
1
... e observe que as 8 linhas originais podem ser substituídas por uma linha simples return result[FindStrictMaxIndex(val,3)]; no ponto do código em que as 8 linhas originais foram colocadas . As outras partes, especialmente FindStrictMaxIndexela própria, são completamente separadas da "lógica de negócios", que as tira do foco de alterar os requisitos de negócios.
Doc Brown
-1

Você provavelmente deve usar uma macro ou uma função que MAX dê o máximo de dois números.

Então você só quer:

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

Você pode ter definido

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

mas seja cauteloso (principalmente sobre efeitos colaterais) ao usar macros (pois MAX(i++,j--) se comportaria de maneira estranha)

Então, melhor defina uma função

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

e use-o (ou pelo menos #define MAX(X,Y) max2ints((X),(Y))....)

Se você precisar entender a origem do MAX, poderá ter uma macro longa como a #define COMPUTE_MAX_WITH_CAUSE(Status,Result,X,Y,Z) que é uma macro longa do{...}while(0)

#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)

Então você pode invocar COMPUTE_MAX_WITH_CAUSE(status,res,a,b,c) em vários lugares. É um pouco feio. I definido variáveis locais x, y, z para reduzir efeitos colaterais ruins ....

Basile Starynkevitch
fonte
2
Refatorar a lógica comum em uma função é a abordagem correta, mas eu evitaria duas coisas aqui: 1. Eu não "inventaria" novos requisitos (o OP não solicitou o cálculo do máximo). E segundo: mesmo que o código resultante se torne mais SECO, é muito discutível se isso justifica uma macro complicada.
Doc Brown
1
As macros devem ser uma ferramenta de último recurso. Definitivamente fora dos limites para esse problema.
precisa
-1

Eu pensei mais sobre isso, então, como meu problema era principalmente a confirmação visual de que todas as comparações usavam as mesmas variáveis, acho que essa pode ser uma abordagem útil:

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 cada macro leva a, be cna mesma ordem é fácil de confirmar, e o nome da macro salva-me ter que descobrir o que todas as comparações e ANDs estão fazendo.

Ken YN
fonte
1
(1) por que macros auxiliares em vez de funções? (2) por que você precisa de confirmação visual aqui? esse é realmente o seu problema principal ou a necessidade de confirmação visual é uma consequência da duplicação de código? Sua melhor opção é fatorar seu código em uma função única e simples que você verifique de uma vez por todas .
coredump