È possibile semplificare (x == 0 || x == 1) in un'unica operazione?


106

Così ho cercato di scrivere il n ° numero della sequenza di Fibonacci come funzione di un compatto possibile:

public uint fibn ( uint N ) 
{
   return (N == 0 || N == 1) ? 1 : fibn(N-1) + fibn(N-2);
}

Ma mi chiedo se posso renderlo ancora più compatto ed efficiente cambiando

(N == 0 || N == 1)

in un unico confronto. C'è qualche operazione di spostamento di bit fantasia che può farlo?


111
Perché? È leggibile, l'intento è molto chiaro e non è costoso. Perché cambiarlo in qualche corrispondenza di pattern di bit "intelligente" che è più difficile da capire e non identifica chiaramente l'intento?
D Stanley

9
Questo non è davvero fibonaci vero?
n8wrl

9
fibonaci aggiunge i due valori precedenti. Intendevi fibn(N-1) + fibn(N-2) invece di N * fibn(N-1)?
juharr

46
Sono assolutamente favorevole a ridurre i nanosecondi, ma se hai un semplice confronto in un metodo che utilizza la ricorsione, perché dedicare sforzi all'efficienza del confronto e lasciare lì la ricorsione?
Jon Hanna

25
Usi un metodo ricorsivo per calcolare il numero di Fabonacci, quindi vuoi migliorare le prestazioni? Perché non trasformarlo in un ciclo? o usi il potere veloce?
male il

Risposte:


-9

Anche questo funziona

Math.Sqrt(N) == N 

radice quadrata di 0 e 1 restituirà rispettivamente 0 e 1.


20
Math.Sqrtè una complicata funzione in virgola mobile. Funziona lentamente rispetto alle alternative solo intere !!
Nayuki

1
Sembra pulito, ma ci sono modi migliori di questo se controlli le altre risposte.
Mafii

9
Se mi sono imbattuto in questo in qualsiasi codice su cui stavo lavorando, probabilmente, come minimo, mi avvicinerei alla scrivania di quella persona e chiederei esplicitamente quale sostanza stavano consumando in quel momento.
un CVn

Chi sano di mente, ha contrassegnato questa come la risposta? Muto.
squashed.bugaboo

212

Esistono diversi modi per implementare il test aritmetico utilizzando l'aritmetica bit per bit. La tua espressione:

  • x == 0 || x == 1

è logicamente equivalente a ciascuno di questi:

  • (x & 1) == x
  • (x & ~1) == 0
  • (x | 1) == 1
  • (~x | 1) == (uint)-1
  • x >> 1 == 0

Bonus:

  • x * x == x (la prova richiede un po 'di sforzo)

Ma in pratica, queste forme sono le più leggibili e la piccola differenza di prestazioni non vale davvero la pena usare l'aritmetica bit per bit:

  • x == 0 || x == 1
  • x <= 1 (perché xè un numero intero senza segno)
  • x < 2(perché xè un numero intero senza segno)

6
Non dimenticare(x & ~1) == 0
Lee Daniel Crocker

71
Ma non scommettere sul fatto che nessuno di loro sia "più efficiente". gcc in realtà genera meno codice per x == 0 || x == 1che per (x & ~1) == 0o (x | 1) == 1. Per il primo è abbastanza intelligente da riconoscerlo come equivalente a x <= 1e restituisce un semplice cmpl; setbe. Gli altri lo confondono e gli fanno generare codice peggiore.
hobbs

13
x <= 1 o x <2 è più semplice.
gnasher729

9
@Kevin True per C ++, perché quello standard si sforza davvero di rendere impossibile scrivere codice conforme. Fortunatamente questa è una domanda su C #;)
Voo

5
La maggior parte dei compilatori moderni può già ottimizzare i confronti in questo modo, anche se non so quanto siano intelligenti il ​​compilatore C # e .NET JITter. È necessario un solo confronto nel codice reale
phuclv

78

Poiché l'argomento è uint( non firmato ) puoi inserire

  return (N <= 1) ? 1 : N * fibn(N-1);

Meno leggibile (IMHO) ma se conti ogni carattere ( Code Golf o simili)

  return N < 2 ? 1 : N * fibn(N-1);

Modifica : per la domanda modificata :

  return (N <= 1) ? 1 : fibn(N-1) + fibn(N-2);

O

  return N < 2 ? 1 : fibn(N-1) + fibn(N-2);

12
Se fosse Code Golf, lo sarebbe return N<2?1:f(N-1)+f(n-2). : P
Conor O'Brien

36

Puoi anche controllare che tutti gli altri bit siano 0 in questo modo:

return (N & ~1) == 0 ? 1 : N * fibn(N-1);

Per completezza grazie a Matt la soluzione ancora migliore:

return (N | 1) == 1 ? 1 : N * fibn(N-1);

In entrambi i casi è necessario fare attenzione alle parentesi perché gli operatori bit per bit hanno priorità inferiore a ==.


Mi piace! Grazie.
user6048670

15
1 carattere in meno:(N|1)==1
Matt

1
@atk 3 | 1 è 3 perché b0011 | b0001 è b0011
René Vogt

3
@atk Questo è o bit per bit, non logico o. Non c'è cortocircuito.
isaacg

2
@Hoten Correct, ma Matt ha detto 1 carattere in meno , non 1 operazione in meno .
Ivan Stoev

20

Se quello che vuoi fare è rendere la funzione più efficiente, usa una tabella di ricerca. La tabella di ricerca è sorprendentemente piccola con sole 47 voci: la voce successiva andrebbe in overflow di un intero senza segno a 32 bit. Ovviamente rende anche la funzione banale da scrivere.

class Sequences
{
    // Store the complete list of values that will fit in a 32-bit unsigned integer without overflow.
    private static readonly uint[] FibonacciSequence = { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,
        233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418,
        317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169,
        63245986, 102334155, 165580141, 267914296, 433494437, 701408733, 1134903170, 1836311903, 2971215073
    };

    public uint fibn(uint N)
    {
        return FibonacciSequence[N];
    }
}

Ovviamente puoi fare la stessa cosa per i fattoriali.


14

Come farlo con bitshift

Se vuoi usare bitshift e rendere il codice un po 'oscuro (ma breve) potresti fare:

public uint fibn ( uint N ) {
   return N >> 1 != 0? fibn(N-1) + finb(N-2): 1;
}

Per un numero intero senza segno Nnella lingua c,N>>1 elimina il bit di ordine inferiore. Se il risultato è diverso da zero, significa che N è maggiore di 1.

Nota: questo algoritmo è orribilmente inefficiente in quanto ricalcola inutilmente i valori nella sequenza che sono già stati calcolati.

Qualcosa di MODO MODO più veloce

Calcolalo un passaggio piuttosto che costruire implicitamente un albero di dimensioni fibonaci (N):

uint faster_fibn(uint N) { //requires N > 1 to work
  uint a = 1, b = 1, c = 1;
  while(--N != 0) {
    c = b + a;
    a = b;
    b = c;
  }
  return c;
}

Come alcune persone hanno già detto, non ci vuole molto per traboccare anche un intero senza segno a 64 bit. A seconda di quanto grande stai cercando di andare, dovrai usare numeri interi di precisione arbitraria.


1
Non solo si evita la crescita esponenziale dell'albero, ma si evita anche la potenziale ramificazione dell'operatore ternario che potrebbe intasare le moderne pipeline della CPU.
mathreadler

2
Il tuo codice "molto più veloce" non funzionerà in C # perché uintnon è implicitamente lanciabile in boole la domanda è specificamente contrassegnata come C #.
Pharap

1
@pharap allora fai --N != 0invece. Il punto è che qualcosa O (n) è preferibile a O (fibn (n)).
Matthew Gunn

1
per espandere il punto di @ MatthewGunn, O (fib (n)) è O (phi ^ n) (vedere questa derivazione stackoverflow.com/a/360773/2788187 )
Connor Clark

@ RenéVogt Non sono uno sviluppatore ac #. Stavo principalmente cercando di commentare la completa assurdità di un algoritmo O (fibn (N)). Si compila ora? (Ho aggiunto! = 0 poiché c # non tratta i risultati diversi da zero come veri.) Funziona (e ha funzionato) in c lineare se si sostituisce uint con qualcosa di standard come uint64_t.
Matthew Gunn

10

Mentre usi un uint, che non può essere negativo, puoi controllare se n < 2

MODIFICARE

O per quel caso di funzione speciale potresti scriverlo come segue:

public uint fibn(uint N)
    return (N == 0) ? 1 : N * fibn(N-1);
}

che porterà allo stesso risultato, ovviamente a costo di un ulteriore passaggio di ricorsione.


4
@CatthalMF: ma il risultato è lo stesso, perché1 * fibn(0) = 1 * 1 = 1
derpirscher

3
La tua funzione non calcola fattoriale, non Fibonacci?
Barmar

2
@Barmar sì, in effetti è fattoriale, perché questa era la domanda originale
derpirscher

3
Potrebbe essere meglio non chiamarlo fibnallora
pie3636

1
@ pie3636 l'ho chiamato fibn perché è così che era chiamato nella domanda originale e non ho aggiornato la risposta in seguito
derpirscher

6

Controlla semplicemente se Nè <= 1 poiché sai che N non è firmato, possono esserci solo 2 condizioni N <= 1che risultano in TRUE: 0 e 1

public uint fibn ( uint N ) 
{
   return (N <= 1) ? 1 : fibn(N-1) + finb(N-2);
}

Ha importanza se è firmato o non firmato? L'algoritmo produce una ricorsione infinita con input negativi, quindi non c'è nulla di male nel trattarli equivalenti a 0 o 1.
Barmar

@Barmar certo che sia importante, soprattutto in questo caso specifico. L'OP ha chiesto se poteva semplificare esattamente (N == 0 || N == 1). Sai che non sarà minore di 0 (perché allora sarebbe firmato!), E il massimo potrebbe essere 1. lo N <= 1semplifica. Immagino che il tipo senza segno non sia garantito, ma dovrebbe essere gestito altrove, direi.
james

Il punto è che se fosse dichiarato int N, e tu conservassi la condizione originale, si ripresenterebbe all'infinito quando N è negativo con la sua condizione originale. Dato che si tratta di un comportamento indefinito, in realtà non dobbiamo preoccuparcene. Quindi possiamo supporre che N non sia negativo, indipendentemente dalla dichiarazione.
Barmar

Oppure possiamo fare tutto ciò che vogliamo con input negativi, incluso trattarli come il caso base della ricorsione.
Barmar

@Barmar abbastanza sicuro che uint verrà sempre convertito in non firmato se provi a impostare su negativo
james

6

Dichiarazione di non responsabilità: non conosco C # e non ho testato questo codice:

Ma mi chiedo se posso renderlo ancora più compatto ed efficiente trasformandolo [...] in un unico confronto ...

Non c'è bisogno di bitshifting o simili, questo utilizza solo un confronto e dovrebbe essere molto più efficiente (O (n) vs O (2 ^ n) penso?). Il corpo della funzione è più compatto , anche se finisce per essere un po 'più lungo con la dichiarazione.

(Per rimuovere l'overhead dalla ricorsione, c'è la versione iterativa, come nella risposta di Mathew Gunn )

public uint fibn ( uint N, uint B=1, uint A=0 ) 
{
    return N == 0 ? A : fibn( N--, A+B, B );
}

                     fibn( 5 ) =
                     fibn( 5,   1,   0 ) =
return 5  == 0 ? 0 : fibn( 5--, 0+1, 1 ) =
                     fibn( 4,   1,   1 ) =
return 4  == 0 ? 1 : fibn( 4--, 1+1, 1 ) =
                     fibn( 3,   2,   1 ) =
return 3  == 0 ? 1 : fibn( 3--, 1+2, 2 ) =
                     fibn( 2,   3,   2 ) =
return 2  == 0 ? 2 : fibn( 2--, 2+3, 3 ) =
                     fibn( 1,   5,   3 ) =
return 1  == 0 ? 3 : fibn( 1--, 3+5, 5 ) =
                     fibn( 0,   8,   5 ) =
return 0  == 0 ? 5 : fibn( 0--, 5+8, 8 ) =
                 5
fibn(5)=5

PS: questo è un modello funzionale comune per l'iterazione con gli accumulatori. Se sostituisci N--con N-1non stai effettivamente utilizzando alcuna mutazione, il che lo rende utilizzabile in un approccio funzionale puro.


4

Ecco la mia soluzione, non c'è molto nell'ottimizzazione di questa semplice funzione, d'altra parte ciò che sto offrendo qui è la leggibilità come definizione matematica della funzione ricorsiva.

public uint fibn(uint N) 
{
    switch(N)
    {
        case  0: return 1;

        case  1: return 1;

        default: return fibn(N-1) + fibn(N-2);
    }
}

La definizione matematica del numero di Fibonacci in modo simile ..

inserisci qui la descrizione dell'immagine

Andando oltre per forzare il caso dello switch a creare una tabella di ricerca.

public uint fibn(uint N) 
{
    switch(N)
    {
        case  0: return 1;
        case  1: return 1;
        case  2: return 2;
        case  3: return 3;
        case  4: return 5;
        case  5: return 8;
        case  6: return 13;
        case  7: return 21;
        case  8: return 34;
        case  9: return 55;
        case 10: return 89;
        case 11: return 144;
        case 12: return 233;
        case 13: return 377;
        case 14: return 610;
        case 15: return 987;
        case 16: return 1597;
        case 17: return 2584;
        case 18: return 4181;
        case 19: return 6765;
        case 20: return 10946;
        case 21: return 17711;
        case 22: return 28657;
        case 23: return 46368;
        case 24: return 75025;
        case 25: return 121393;
        case 26: return 196418;
        case 27: return 317811;
        case 28: return 514229;
        case 29: return 832040;
        case 30: return 1346269;
        case 31: return 2178309;
        case 32: return 3524578;
        case 33: return 5702887;
        case 34: return 9227465;
        case 35: return 14930352;
        case 36: return 24157817;
        case 37: return 39088169;
        case 38: return 63245986;
        case 39: return 102334155;
        case 40: return 165580141;
        case 41: return 267914296;
        case 42: return 433494437;
        case 43: return 701408733;
        case 44: return 1134903170;
        case 45: return 1836311903;
        case 46: return 2971215073;

        default: return fibn(N-1) + fibn(N-2);
    }
}

1
Il vantaggio della tua soluzione è che viene calcolata solo quando necessario. La cosa migliore sarebbe una tabella di ricerca. bonus alternativo: f (n-1) = someCalcOf (f (n-2)), quindi non è necessaria la riesecuzione completa.
Karsten

@Karsten Ho aggiunto valori sufficienti per lo switch per creare una tabella di ricerca per esso. Non sono sicuro di come funzioni il bonus alternativo.
Khaled.K

1
Come risponde questo alla domanda?
Clark Kent

@SaviourSelf si tratta di una tabella di ricerca, e c'è l'aspetto visivo spiegato nella risposta. stackoverflow.com/a/395965/2128327
Khaled.K

Perché dovresti usare un switchquando puoi avere una serie di risposte?
Nayuki

4

per N è uint, basta usare

N <= 1

Esattamente quello che stavo pensando; N is uint! Questa dovrebbe essere la risposta, davvero.
squashed.bugaboo

1

La risposta di Dmitry è la migliore, ma se fosse un tipo restituito Int32 e avessi un set più ampio di numeri interi tra cui scegliere, potresti farlo.

return new List<int>() { -1, 0, 1, 2 }.Contains(N) ? 1 : N * fibn(N-1);

2
Come è più corto dell'originale?
MCMastery

2
@MCMastery Non è più breve. Come ho già detto, è meglio se il tipo restituito originale è un int32 e sta selezionando da un ampio insieme di numeri validi. Insead di dover scrivere (N == -1 || N == 0 || N == 1 || N == 2)
CathalMF

1
Le ragioni di OP sembrano essere legate all'ottimizzazione. Questa è una cattiva idea per diversi motivi: 1) istanziare un nuovo oggetto all'interno di ogni chiamata ricorsiva è davvero una cattiva idea, 2) List.Containsè O (n), 3) semplicemente fare due confronti invece ( N > -3 && N < 3) darebbe un codice più breve e più leggibile.
Groo

@Groo E se i valori fossero -10, -2, 5, 7, 13
CathalMF

Non è quello che ha chiesto OP. Ma comunque, ancora 1) non vorresti creare una nuova istanza in ogni chiamata, 2) sarebbe meglio usare invece un (singolo) hashset, 3) per un problema specifico, potresti anche ottimizzare la funzione hash per essere puro, o anche utilizzare operazioni bit per bit disposte in modo intelligente come suggerito in altre risposte.
Groo

0

La sequenza di Fibonacci è una serie di numeri in cui si trova un numero sommando i due numeri prima di esso. Esistono due tipi di punti di partenza: ( 0,1 , 1,2, ..) e ( 1,1 , 2,3).

-----------------------------------------
Position(N)| Value type 1 | Value type 2
-----------------------------------------  
1          |  0           |   1
2          |  1           |   1
3          |  1           |   2
4          |  2           |   3
5          |  3           |   5
6          |  5           |   8
7          |  8           |   13
-----------------------------------------

La posizione Nin questo caso inizia da 1, non è 0-basedcome un indice di matrice.

Usando la funzionalità Expression-body di C # 6 e il suggerimento di Dmitry sull'operatore ternario possiamo scrivere una funzione di una riga con il calcolo corretto per il tipo 1:

public uint fibn(uint N) => N<3? N-1: fibn(N-1)+fibn(N-2);

e per il tipo 2:

public uint fibn(uint N) => N<3? 1: fibn(N-1)+fibn(N-2);

-2

Un po 'tardi per la festa, ma potresti anche farlo (x==!!x)

!!xconverte il valore a in 1se non lo è 0e lo lascia a 0se lo è.
Uso molto questo genere di cose nell'offuscamento in C.

Nota: questo è C, non sono sicuro che funzioni in C #


4
Non sono sicuro del motivo per cui questo è stato votato. Anche superficiale tentare questo come uint n = 1; if (n == !!n) { }Operator '!' cannot be applied to operand of type 'uint'sulla !nin C #. Solo perché qualcosa funziona in C non significa che funzioni in C #; anche #include <stdio.h>non funziona in C #, perché C # non ha la direttiva del preprocessore "include". I linguaggi sono più diversi di C e C ++.
un CVn

2
Oh. Va bene. Mi dispiace :(
One Normal Night

@OneNormalNight (x == !! x) Come funzionerà. Considera che il mio input è 5. (5 == !! 5).
Darà

1
@VinothKumar !! 5 restituisce 1. (5 == !! 5) restituisce (5 == 1) che restituisce false.
Una notte normale

@OneNormalNight sì, ho capito ora. ! (5) dà 1 di nuovo applicato dà 0. Non 1.
VINOTH ENERGETIC

-3

Quindi ho creato uno Listdi questi numeri interi speciali e ho controllato se lo Nriguarda.

static List<uint> ints = new List<uint> { 0, 1 };

public uint fibn(uint N) 
{
   return ints.Contains(N) ? 1 : fibn(N-1) + fibn(N-2);
}

È inoltre possibile utilizzare un metodo di estensione per scopi diversi in cui Containsviene chiamato solo una volta (ad esempio, quando l'applicazione sta avviando e caricando i dati). Questo fornisce uno stile più chiaro e chiarisce la relazione principale con il tuo valore ( N):

static class ObjectHelper
{
    public static bool PertainsTo<T>(this T obj, IEnumerable<T> enumerable)
    {
        return (enumerable is List<T> ? (List<T>) enumerable : enumerable.ToList()).Contains(obj);
    }
}

Applicalo:

N.PertainsTo(ints)

Questo potrebbe non essere il modo più veloce per farlo, ma a me sembra essere uno stile migliore.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.