Prosty i przejrzysty sposób porównywania trzech liczb

11

Mam kod, który ma sekwencję ifs, która działa, ale po prostu niechlujny. Zasadniczo chcę wybrać największą z trzech liczb całkowitych i ustawić flagę statusu, aby powiedzieć, która została wybrana. Mój obecny kod wygląda następująco:

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;

Ten wzór występuje kilka razy, a przy długich nazwach zmiennych trudno jest wizualnie potwierdzić, że każda z nich ifjest poprawna. Wydaje mi się, że może być lepszy i jaśniejszy sposób na zrobienie tego; czy ktoś może coś zasugerować?


Istnieje kilka potencjalnych duplikatów, ale nie do końca odpowiadają temu pytaniu.

W sugerowanym duplikacie: Podejścia do sprawdzania wielu warunków? wszystkie sugerowane rozwiązania wydają się równie nieporadne jak oryginalny kod, więc nie zapewniają lepszego rozwiązania.

I ten post Eleganckie sposoby radzenia sobie, jeśli (jeśli inaczej) dotyczą tylko poziomów zagnieżdżenia i asymetrii, co nie jest tutaj problemem.

Ken YN
źródło
3
Dla mnie jedynym problemem jest powtórzenie kodu. To, co tu masz, jest niezwykle czytelne, po co to zmieniać? Po prostu podziel go na funkcję, która przyjmuje status a, b, c i zwraca status. Jeśli to sprawi, że poczujesz się lepiej, upuść na nim „inline”. Bez makr, bez złożoności, po prostu dobra stara ekstrakcja funkcji. Będę ci wdzięczny za twój czytelny kod, jeśli będę musiał z nim później pracować.
J Trana,
Pamiętaj, że Twoje # definicje są źle nazwane. Rozważmy a = 40, b = 30, c = 30. Wynik to MOSTLY_A. Ale większość rzeczy w rzeczywistości nie jest A, ale B lub C. Możesz spróbować MORE_A (choć nadal niejednoznaczne - więcej A niż B i więcej A niż C lub więcej A niż B i C łącznie?) Lub A_LARGEST, MAX_IS_A, ...? (co sugeruje również max (a, max (b, c)) jako odpowiedź na pytanie).
Tony

Odpowiedzi:

12

Rozłóż logikę na czynniki, wróć wcześniej

Jak sugerowano w komentarzach, wystarczy po prostu zawinąć logikę w funkcję i wyjść wcześniej z return's', aby znacznie uprościć sprawę . Można również rozłożyć na czynniki trochę funkcjonalność, delegując testy do innej funkcji. Bardziej konkretnie:

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

To jest krótsze niż moja poprzednia próba:

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

Powyższe jest nieco bardziej szczegółowe, ale jest łatwe do odczytania IMHO i nie oblicza porównań wiele razy.

Potwierdzenie wizualne

W swojej odpowiedzi mówisz:

moim problemem było głównie wizualne potwierdzenie, że wszystkie porównania wykorzystywały te same zmienne

... również w swoim pytaniu mówisz:

Ten wzorzec występuje kilka razy, a przy długich nazwach zmiennych trudno jest wizualnie potwierdzić, że każdy z nich jest poprawny.

Mogę nie rozumieć, co próbujesz osiągnąć: czy chcesz skopiować i wkleić wzór wszędzie tam, gdzie go potrzebujesz? Z funkcji takich jak ten powyżej, przechwytywanie wzorca raz i sprawdzić raz na zawsze, że wszystkie porównania użyć a, ba cw razie potrzeby. Następnie nie musisz się już martwić, kiedy wywołujesz tę funkcję. Oczywiście, może w praktyce twój problem jest nieco bardziej złożony niż opisany przez Ciebie: jeśli tak, proszę dodaj szczegóły, jeśli to możliwe.

rdzeń rdzeniowy
źródło
1
Nie rozumiem twojego komentarza o DONT_KNOW , co jeśli c jest najmniejsze, a aib są takie same? Algorytm zwróciłby, że b jest największy, podczas gdy a jest taki sam jak b.
Pieter B
@PieterB byłem wronlgy zakładając, że nie ważne czy mamy zwrocone MOSTLY_Alub MOSTLY_Cw przypadku a == ca a > b. Zostało to naprawione. Dzięki.
coredump
@DocBrown To prawda, że ​​większość korzyści wynika z zachowania wczesnego wyjścia.
coredump
1
+1, teraz jest to rzeczywiście poprawa w stosunku do oryginalnego kodu OP.
Doc Brown,
9

TL: DR; Twój kod jest już poprawny i „czysty”.

Widzę wielu ludzi wahających się wokół odpowiedzi, ale wszyscy tęsknią za lasem przez drzewa. Zróbmy pełną informatykę i analizę matematyczną, aby całkowicie zrozumieć to pytanie.

Po pierwsze, zauważamy, że mamy 3 zmienne, każda z 3 stanami: <, = lub>. Całkowita liczba permutacji wynosi 3 ^ 3 = 27 stanów, do których przypisam unikalny numer, oznaczony P #, dla każdego stanu. Ten numer P # to system liczb czynnikowych .

Zliczając wszystkie permutacje, które mamy:

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

Po kontroli stwierdzamy, że mamy:

  • 3 stany, w których A jest wartością maksymalną,
  • 3 stany, w których B jest maksimum,
  • 3 stany, w których C jest wartością maksymalną, oraz
  • 4 stany, w których A = B lub B = C.

Napiszmy program (patrz przypis), aby wyliczyć wszystkie te permutacje wartościami A, B i C. Sortowanie stabilne według 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

Jeśli zastanawiasz się, skąd wiedziałem, które stany P # są niemożliwe, teraz już wiesz. :-)

Minimalna liczba porównań w celu ustalenia kolejności wynosi:

Log2 (27) = Log (27) / Log (2) = ~ 4,75 = 5 porównań

tzn. rdzeń rdzeniowy dał prawidłową 5 minimalną liczbę porównań. Sformatowałbym jego kod jako:

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

W przypadku Twojego problemu nie zależy nam na testowaniu równości, więc możemy pominąć 2 testy.

Nie ma znaczenia, jak czysty / zły jest kod, jeśli otrzyma złą odpowiedź, więc jest to dobry znak, że poprawnie załatwiasz wszystkie sprawy!

Następnie, jeśli chodzi o prostotę, ludzie próbują „poprawić” odpowiedź, w której ich zdaniem poprawa oznacza „optymalizację” liczby porównań, ale nie jest to dokładnie to, o co prosisz. Zdezorientowałeś wszystkich, gdy zapytałeś „Czuję, że może być lepszy”, ale nie zdefiniowałeś, co oznacza „lepszy”. Mniej porównań? Mniej kodu? Optymalne porównania?

Teraz, gdy pytasz o czytelność kodu (biorąc pod uwagę poprawność), dokonałbym tylko jednej zmiany w twoim kodzie dla czytelności: Dopasuj pierwszy test do pozostałych.

        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

Osobiście napisałbym to w następujący sposób, ale może to być zbyt niekonwencjonalne dla twoich standardów kodowania:

        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

Przypis: Oto kod C ++ do generowania permutacji:

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

Edycje: Na podstawie opinii, przeniesiono TL: DR na górę, usunięto nieposortowaną tabelę, wyjaśniono 27, wyczyściłem kod, opisałem niemożliwe stany.

Michał Anioł 007
źródło
-1: czy zmniejszenie liczby decyzji nie prowadziłoby do prostszych ścieżek kodu i bardziej czytelnego kodu? Twój argument nie jest jasny: po pierwsze, mówisz, że wszyscy się mylą; następnie stawiasz nie jeden lub dwa, ale trzy tabele; Miałem nadzieję, że doprowadzą one do prostszego sposobu obliczenia wyniku, ale zamiast tego potwierdziliście to, co wszyscy już wiedzieli (kod OP robi to dobrze). Jasne, pytanie dotyczy czytelności, ale czytelność nie jest osiągana tylko przez modyfikację układu kodu (przyznajesz, że twoje zmiany nie pasowałyby do istniejących standardów kodu). Sensowne jest uproszczenie logiki przy optymalizacji pod kątem czytelności.
coredump
Bardziej konstruktywnie: proponuję uprościć odpowiedź, pomijając pewne szczegóły i zastanawiając się nad strukturą odpowiedzi. Rozumiem, że poświęciłeś trochę czasu na napisanie i opublikowanie permutacji generujących kod C ++, ale może mógłbyś dać główny wynik i tylko jedną tabelę: tak jak teraz wygląda na to, że zrzuciłeś całą swoją pracę taką, jaka jest. Prawie nie zauważyłem rzeczy TL; DR (możesz zacząć od tego). Mam nadzieję, że to pomoże.
coredump
2
Dzięki za konstruktywną informację zwrotną. Usunąłem środkową nieposortowaną tabelę, ponieważ można ją łatwo zweryfikować.
Michaelangel007
2
Jezus Chrystus! Kto by powiedział, że porównanie trzech liczb jest prawie tak złożone, jak nauka o rakietach?
Mandrill
@Mandrill Jak informatyków naszym zadaniem jest, aby zrozumieć problem dokładnie . Tylko poprzez wyliczenie wszystkich 27 możliwych kombinacji dla porównania 3-drogowego możemy zweryfikować, że nasze rozwiązanie działa we WSZYSTKICH przypadkach. Ostatnią rzeczą, jakiej pragniemy jako programiści, są ukryte błędy i niezliczone przypadki krawędzi. Szczegółowość to cena, jaką płaci się za poprawność.
Michaelangel007
5

@msw powiedział ci, abyś użył tablicy zamiast a, b, c, a @Basile powiedział ci, aby przełożyć logikę „max” na funkcję. Połączenie tych dwóch pomysłów prowadzi do

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

następnie podaj funkcję, która oblicza maksymalny indeks dowolnej tablicy:

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

i nazwać to tak

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

Wydaje się, że całkowita liczba LOC wzrosła w porównaniu z pierwotną, ale teraz masz podstawową logikę w funkcji wielokrotnego użytku, a jeśli możesz wielokrotnie użyć tej funkcji, zacznie się to opłacać. Co więcej, FindStrictMaxIndexfunkcja nie jest już powiązana z twoimi „wymaganiami biznesowymi” (oddzielenie problemów), dlatego ryzyko, które będziesz musiał zmodyfikować później, jest znacznie niższe niż w oryginalnej wersji (zasada otwartego zamknięcia). Na przykład ta funkcja nie będzie musiała zostać zmieniona, nawet jeśli liczba argumentów ulegnie zmianie, lub będzie trzeba użyć innych zwracanych wartości niż MOSTLY_ABC, lub jeśli przetwarzane są inne zmienne niż a, b, c. Ponadto użycie tablicy zamiast 3 różnych wartości a, b, c może uprościć kod również w innych miejscach.

Oczywiście, jeśli w całym twoim programie jest tylko jedno lub dwa miejsca do wywołania tej funkcji i nie masz żadnych innych aplikacji do przechowywania wartości w tablicy, prawdopodobnie zostawiłbym oryginalny kod w takiej postaci, w jakiej jest (lub użyj @ poprawa rdzeni).

Doktor Brown
źródło
Podoba mi się - wnętrzności FindStrictMaxIndex()mogą nie być zbyt czyste, ale z punktu widzenia dzwoniącego jest dość oczywiste, co próbuje się osiągnąć.
Ken YN
Lub zamiast trzymać dwie tablice, przytrzymaj jedną tablicę par klucz-wartość: {MOSTLY_A, countAs ()}, weź pierwszy element uporządkowany według wartości i odczytaj klucz.
Julia Hayward
@JuliaHayward: głównym powodem, dla którego nie zasugerowałem takiego rozwiązania, był znacznik „C” pytania - w C trzeba będzie trochę więcej kodu bazowego, aby poradzić sobie z parami klucz-wartość, i stworzyć funkcję wpisaną w kategoriach KVP prawdopodobnie nie będą tak wielokrotnego użytku w różnych kontekstach niż zwykła intfunkcja pisania na klawiaturze. Ale zgadzam się na twój komentarz, jeśli używa się innego języka, takiego jak Python lub Perl.
Doc Brown
1
@ gnasher729: zależy to od liczby „duplikatów” w kodzie oryginalnym, jak bardzo są one podobne i jak często FindStrictMaxIndexmożna ponownie użyć tej funkcji. W przypadku jednego lub tylko dwóch powtórzeń, to się nie opłaca, ale o tym już pisałem w odpowiedzi. Zwróć też uwagę na inne zalety, o których wspomniałem powyżej, dotyczące przyszłych zmian.
Doc Brown
1
... i zwróć uwagę, że oryginalne 8 wierszy można zastąpić prostym jednowierszowym return result[FindStrictMaxIndex(val,3)]; w punkcie kodu, w którym zostały umieszczone oryginalne 8 wierszy . Pozostałe części, a zwłaszcza FindStrictMaxIndexsama w sobie, są całkowicie oddzielone od „logiki biznesowej”, która odsuwa je od centrum zmieniających się wymagań biznesowych.
Doc Brown
-1

Prawdopodobnie powinieneś użyć makra lub funkcji MAX podającej maksymalnie dwie liczby.

Więc po prostu chcesz:

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

Mogłeś zdefiniować

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

ale MAX(i++,j--) zachowaj ostrożność - szczególnie przy skutkach ubocznych - podczas korzystania z makr (ponieważ zachowywałby się dziwnie)

Więc lepiej zdefiniuj funkcję

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

i użyj go (lub przynajmniej #define MAX(X,Y) max2ints((X),(Y))...)

Jeśli chcesz zrozumieć pochodzenie MAX, możesz mieć długie makro, #define COMPUTE_MAX_WITH_CAUSE(Status,Result,X,Y,Z) które może być długim do{... }while(0) makro

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

Następnie możesz wywoływać COMPUTE_MAX_WITH_CAUSE(status,res,a,b,c) w kilku miejscach. To jest trochę brzydkie. I zdefiniowane zmienne lokalne x, y, z aby obniżyć złych skutków ubocznych ....

Basile Starynkevitch
źródło
2
Przeformułowanie wspólnej logiki na funkcję jest właściwym podejściem, ale tak naprawdę unikałbym tutaj dwóch rzeczy: 1. Nie wymyślałbym nowych wymagań (PO nie poprosił o obliczenie maksimum). I po drugie: nawet jeśli wynikowy kod może stać się bardziej SUCHY, jest bardzo dyskusyjny, jeśli uzasadnia to skomplikowane makro.
Doc Brown
1
Makra powinny być narzędziem ostatecznym. Zdecydowanie poza granicami tego problemu.
kevin cline
-1

Zastanowiłem się nad tym, więc ponieważ mój problem był głównie wizualnym potwierdzeniem, że wszystkie porównania wykorzystują te same zmienne, myślę, że może to być przydatne podejście:

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? */

Że każde makro trwa a, bi cw tej samej kolejności jest łatwe, aby potwierdzić, a nazwa makra oszczędza mi mający wypracować co wszystkie porównania i łączeniu robią.

Ken YN
źródło
1
(1) dlaczego makra pomocnicze zamiast funkcji? (2) dlaczego potrzebujesz tutaj potwierdzenia wizualnego? czy to naprawdę jest twój główny problem, czy też potrzeba wizualnego potwierdzenia jest konsekwencją duplikacji kodu? Najlepszą opcją jest podzielenie kodu na pojedynczą, prostą funkcję, którą sprawdzasz raz na zawsze .
coredump