É possível simplificar (x == 0 || x == 1) em uma única operação?


106

Então, eu estava tentando escrever o n º número na seqüência de Fibonacci em como a função de um compacto quanto possível:

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

Mas estou me perguntando se posso tornar isso ainda mais compacto e eficiente mudando

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

em uma única comparação. Existe alguma operação de mudança de bit sofisticada que pode fazer isso?


111
Por quê? É legível, a intenção é muito clara e não é caro. Por que alterá-lo para alguma correspondência de padrão de bits "inteligente" que é mais difícil de entender e não identifica claramente a intenção?
D Stanley

9
Isso não é realmente fibonaci certo?
n8wrl de

9
fibonaci adiciona os dois valores anteriores. Você quis dizer em fibn(N-1) + fibn(N-2) vez de N * fibn(N-1)?
juharr de

46
Eu sou totalmente a favor de cortar nanossegundos, mas se você tem uma comparação simples em um método que usa recursão, por que gastar esforço na eficiência da comparação e deixar a recursão aí?
Jon Hanna de

25
Você usa uma forma recursiva para calcular o número de Fabonacci, então você quer melhorar o desempenho? Por que não transformá-lo em um loop? ou usar energia rápida?
notbad

Respostas:


-9

Este também funciona

Math.Sqrt(N) == N 

a raiz quadrada de 0 e 1 retornará 0 e 1, respectivamente.


20
Math.Sqrté uma função de ponto flutuante complicada. Ele roda lentamente em comparação com as alternativas somente inteiras !!
Nayuki

1
Parece claro, mas há maneiras melhores do que isso se você verificar as outras respostas.
Mafii de

9
Se eu encontrasse isso em qualquer código em que estivesse trabalhando, provavelmente iria, no mínimo, ir até a mesa dessa pessoa e perguntar diretamente que substância ela estava consumindo naquele momento.
um CVn de

Quem, em sã consciência, marcou esta como a resposta? Sem palavras.
squashed.bugaboo

212

Existem várias maneiras de implementar seu teste aritmético usando aritmética bit a bit. Sua expressão:

  • x == 0 || x == 1

é logicamente equivalente a cada um destes:

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

Bônus:

  • x * x == x (a prova exige um pouco de esforço)

Mas, falando de forma prática, esses formulários são os mais legíveis, e a pequena diferença no desempenho não vale o uso da aritmética bit a bit:

  • x == 0 || x == 1
  • x <= 1(porque xé um inteiro sem sinal)
  • x < 2(porque xé um inteiro sem sinal)

6
Não se esqueça(x & ~1) == 0
Lee Daniel Crocker,

71
Mas não aposte que nenhum deles em particular será "mais eficiente". gcc realmente gera menos código para do x == 0 || x == 1que para (x & ~1) == 0ou (x | 1) == 1. Para o primeiro, é inteligente o suficiente para reconhecê-lo como equivalente a x <= 1e resulta em um simples cmpl; setbe. Os outros o confundem e fazem com que ele gere um código pior.
hobbs,

13
x <= 1 ou x <2 é mais simples.
gnasher729

9
@Kevin True para C ++, porque esse padrão tenta muito, muito dificilmente tornar impossível escrever código compatível. Felizmente, esta é uma pergunta sobre C #;)
Voo

5
A maioria dos compiladores modernos já pode otimizar comparações como essa, embora eu não saiba o quão inteligentes são o compilador C # e o .NET JITter. Apenas uma única comparação é necessária no código real
phuclv

78

Uma vez que o argumento é uint( sem sinal ), você pode colocar

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

Menos legível (IMHO), mas se você contar cada personagem ( Code Golf ou similar)

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

Editar : para sua pergunta editada :

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

Ou

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

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

36

Você também pode verificar se todos os outros bits são 0, como este:

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

Para completar, graças a Matt, a solução ainda melhor:

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

Em ambos os casos, você precisa cuidar dos parênteses porque os operadores bit a bit têm prioridade menor do que ==.


Eu gosto disso! Obrigado.
user6048670

15
1 personagem a menos:(N|1)==1
Matt,

1
@atk 3 | 1 é 3 porque b0011 | b0001 é b0011
René Vogt

3
@atk Isto é bit a bit ou, não lógico ou. Não há curto-circuito.
isaacg de

2
@Hoten Correto, mas Matt disse 1 caractere a menos , não 1 operação a menos .
Ivan Stoev,

20

Se o que você deseja fazer é tornar a função mais eficiente, use uma tabela de pesquisa. A tabela de pesquisa é surpreendentemente pequena com apenas 47 entradas - a próxima entrada estouraria um inteiro não assinado de 32 bits. É claro que também torna a função trivial de escrever.

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

Obviamente, você pode fazer a mesma coisa para fatoriais.


14

Como fazer isso com bitshift

Se você quiser usar bitshift e tornar o código um tanto obscuro (mas curto), você pode fazer:

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

Para um inteiro sem sinal Nna linguagem c, N>>1descarta o bit de ordem inferior. Se esse resultado for diferente de zero, isso implica que N é maior que 1.

Nota: este algoritmo é terrivelmente ineficiente, pois recalcula desnecessariamente os valores na sequência que já foram calculados.

Algo BEM MUITO mais rápido

Calcule uma passagem, em vez de construir implicitamente uma árvore do tamanho de 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;
}

Como algumas pessoas mencionaram, não demora muito para estourar até mesmo um número inteiro sem sinal de 64 bits. Dependendo de quão grande você está tentando ir, você precisará usar inteiros de precisão arbitrária.


1
Não apenas evitando a árvore de crescimento exponencial, mas também evitando a ramificação potencial do operador ternário que poderia obstruir os pipelines da CPU moderna.
mathreadler

2
Seu código 'muito mais rápido' não funcionará em C # porque uintnão pode ser convertido implicitamente em bool, e a pergunta está especificamente marcada como C #.
Pharap

1
@pharap então faça --N != 0. A questão é que algo O (n) é preferível a O (fibn (n)).
Matthew Gunn,

1
para expandir o ponto de @ MatthewGunn, O (fib (n)) é O (phi ^ n) (veja esta derivação stackoverflow.com/a/360773/2788187 )
Connor Clark

@RenéVogt Não sou um desenvolvedor c #. Eu estava principalmente tentando comentar sobre o completo absurdo de um algoritmo O (fibn (N)). Compila agora? (Eu adicionei! = 0, pois c # não trata resultados diferentes de zero como verdadeiros.) Funciona (e funcionou) em c direto se você substituir uint por algo padrão como uint64_t.
Matthew Gunn,

10

Ao usar um uint, que não pode ser negativo, você pode verificar se n < 2

EDITAR

Ou para esse caso de função especial, você poderia escrevê-lo da seguinte forma:

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

que levará ao mesmo resultado, é claro, ao custo de uma etapa de recursão adicional.


4
@CatthalMF: mas o resultado é o mesmo, porque1 * fibn(0) = 1 * 1 = 1
derpirscher

3
Sua função não calcula fatorial, e não fibonacci?
Barmar,

2
@Barmar sim, na verdade isso é fatorial, porque essa era a pergunta original
derpirscher

3
Pode ser melhor não chamá-lo fibnentão
pie3636

1
@ pie3636 chamei de fibn porque era assim que era chamado na pergunta original e não atualizei a resposta mais tarde
derpirscher

6

Basta verificar se Né <= 1, pois você sabe que N não tem sinal, só pode haver 2 condições N <= 1que resultam em TRUE: 0 e 1

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

Faz diferença se está assinado ou não? O algoritmo produz recursão infinita com entradas negativas, então não há mal nenhum em tratá-los equivalentes a 0 ou 1.
Barmar

@Barmar claro que é importante, principalmente neste caso específico. O OP perguntou se ele poderia simplificar exatamente (N == 0 || N == 1). Você sabe que não será menor que 0 (porque então estaria assinado!), E o máximo poderia ser 1. N <= 1simplifica. Acho que o tipo sem sinal não é garantido, mas isso deve ser tratado em outro lugar, eu diria.
James

Meu ponto é que, se fosse declarado int N, e você mantivesse a condição original, ele ocorreria infinitamente quando N fosse negativo com sua condição original. Como esse é um comportamento indefinido, não precisamos nos preocupar com isso. Portanto, podemos supor que N é não negativo, independentemente da declaração.
Barmar

Ou podemos fazer o que quisermos com entradas negativas, incluindo tratá-las como o caso base da recursão.
Barmar

@Barmar tenho certeza que uint sempre será convertido para não assinado se você tentar definir como negativo
James

6

Isenção de responsabilidade: não conheço C # e não testei este código:

Mas estou me perguntando se posso tornar isso ainda mais compacto e eficiente mudando [...] para uma única comparação ...

Não há necessidade de bitshifting ou algo semelhante, ele usa apenas uma comparação e deve ser muito mais eficiente (O (n) vs O (2 ^ n), eu acho?). O corpo da função é mais compacto , embora acabe sendo um pouco mais longo com a declaração.

(Para remover a sobrecarga da recursão, existe a versão iterativa, como na resposta de 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: Este é um padrão funcional comum para iteração com acumuladores. Se você substituir N--por, N-1estará efetivamente usando nenhuma mutação, o que o torna utilizável em uma abordagem puramente funcional.


4

Esta é minha solução, não há muito em otimizar esta função simples, por outro lado, o que estou oferecendo aqui é a legibilidade como uma definição matemática da função recursiva.

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

        case  1: return 1;

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

A definição matemática do número de Fibonacci de forma semelhante.

insira a descrição da imagem aqui

Levando isso adiante para forçar o caso de switch a construir uma tabela de pesquisa.

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
A vantagem da sua solução é que ela só é calculada quando necessário. O melhor seria uma tabela de consulta. bônus alternativo: f (n-1) = someCalcOf (f (n-2)), portanto, não é necessária uma nova execução completa.
Karsten

@Karsten Eu adicionei valores suficientes para o switch criar uma tabela de pesquisa para ele. Não tenho certeza de como funciona o bônus alternativo.
Khaled.K

1
Como isso responde à pergunta?
Clark Kent

@SaviourSelf tudo se resume a uma tabela de consulta e há o aspecto visual explicado na resposta. stackoverflow.com/a/395965/2128327
Khaled.K

Por que você usaria um switchquando pode ter uma variedade de respostas?
Nayuki


1

A resposta de Dmitry é melhor, mas se fosse um tipo de retorno Int32 e você tivesse um conjunto maior de números inteiros para escolher, você poderia fazer isso.

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

2
Como isso é mais curto do que o original?
MCM✓

2
@MCM✓ Não é mais curto. Como mencionei, só é melhor se o tipo de retorno original for um int32 e ele estiver selecionando em um grande conjunto de números válidos. Ao invés de ter que escrever (N == -1 || N == 0 || N == 1 || N == 2)
CathalMF

1
As razões do OP parecem estar relacionadas à otimização. Esta é uma má ideia por vários motivos: 1) instanciar um novo objeto dentro de cada chamada recursiva é uma péssima ideia, 2) List.Containsé O (n), 3) simplesmente fazer duas comparações ( N > -3 && N < 3) daria um código mais curto e legível.
Groo

@Groo E se os valores fossem -10, -2, 5, 7, 13
CathalMF

Não é o que OP perguntou. Mas de qualquer forma, você ainda 1) não gostaria de criar uma nova instância em cada chamada, 2) seria melhor usar um hashset (único) em vez disso, 3) para um problema específico, você também seria capaz de otimizar a função de hash para ser puro, ou mesmo usar operações bit a bit habilmente arranjadas, como sugerido em outras respostas.
Groo

0

A seqüência de Fibonacci é uma série de números onde um número é encontrado somando os dois números anteriores. Existem dois tipos de pontos de partida: ( 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
-----------------------------------------

A posição N, neste caso, começa de 1, não é 0-basedcomo um índice de array.

Usando o recurso Expression-body do C # 6 e a sugestão de Dmitry sobre o operador ternário , podemos escrever uma função de uma linha com cálculo correto para o tipo 1:

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

e para o tipo 2:

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

-2

Um pouco tarde para a festa, mas você também pode fazer (x==!!x)

!!xconverte o valor a em 1se não for 0e o deixa em 0se for.
Eu uso muito esse tipo de ofuscação.

Nota: Este é C, não tenho certeza se funciona em C #


4
Não tenho certeza por que isso foi votado. Mesmo superficial tentar isso como uint n = 1; if (n == !!n) { }Operator '!' cannot be applied to operand of type 'uint'no !nem C #. Só porque algo funciona em C não significa que funcione em C #; nem mesmo #include <stdio.h>funciona em C #, porque C # não tem a diretiva de pré-processador "include". As linguagens são mais diferentes do que C e C ++.
um CVn de

2
Oh. OK. Lamento :(
Uma noite normal,

@OneNormalNight (x == !! x) Como isso vai funcionar. Considere que minha entrada é 5. (5 == !! 5). O resultado será verdadeiro
VINOTH ENERGETIC

1
@VinothKumar !! 5 avalia como 1. (5 == !! 5) avalia (5 == 1) que avalia como falso.
Uma noite normal

@OneNormalNight sim, entendi agora. ! (5) dá 1 novamente aplicado dá 0. Não 1.
VINOTH ENERGETIC

-3

Então criei um Listdesses inteiros especiais e verifiquei se Npertence a ele.

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

Você também pode usar um método de extensão para finalidades diferentes onde Containsé chamado apenas uma vez (por exemplo, quando seu aplicativo está iniciando e carregando dados). Isso fornece um estilo mais claro e esclarece a relação principal com seu valor ( 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);
    }
}

Aplicam-na:

N.PertainsTo(ints)

Esta pode não ser a maneira mais rápida de fazer isso, mas para mim, parece ser um estilo melhor.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.