是否可以将(x == 0 || x == 1)简化为单个操作?


106

所以我试图用尽可能紧凑的函数写斐波那契数列中的第n个数字:

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

但是我想知道是否可以通过更改来使它更加紧凑和高效

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

进行一次比较。是否有一些高级的移位操作可以做到这一点?


111
为什么?它可读性强,意图很明确,而且并不昂贵。为什么将其更改为一些“智能”位模式匹配,这些匹配较难理解并且不能清楚地识别意图?
D Stanley

9
这真的不是斐波那契吧?
n8wrl

9
fibonaci将先前的两个值相加。您是fibn(N-1) + fibn(N-2) 不是要说N * fibn(N-1)
juharr '16

46
我全力以赴地节省纳秒的时间,但是如果您在使用递归的方法中进行了简单的比较,为什么还要花精力在比较的效率上,而将递归留在那儿呢?
乔恩·汉纳

25
您使用递归方法来计算Fabonacci数,那么您想提高性能吗?为什么不将其更改为循环?还是使用快速电源?
notbad

Answers:


-9

这个也可以

Math.Sqrt(N) == N 

0和1的平方根将分别返回0和1。


20
Math.Sqrt是一个复杂的浮点函数。与仅整数替代方案相比,它运行缓慢!
纳希基(Nayuki)

1
这看起来很干净,但是如果您检查其他答案,还有更好的方法。
马菲

9
如果我在编写的任何代码中都遇到了这种情况,那么我可能至少要走到那个人的办公桌前,并明确地问他们当时正在消耗什么物质。
CVn

谁在他们的右脑中将其标记为答案?无语。
squashed.bugaboo

212

有多种方法可以使用按位算术来实现算术测试。您的表情:

  • x == 0 || x == 1

在逻辑上等效于以下各项:

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

奖金:

  • x * x == x (证明需要一些努力)

但是实际上,这些形式是最易读的,并且性能上的微小差异实际上不值得使用按位算术:

  • x == 0 || x == 1
  • x <= 1(因为x是无符号整数)
  • x < 2(因为x是无符号整数)

6
不要忘记(x & ~1) == 0
Lee Daniel Crocker

71
但是不要打赌他们中的任何一个都是“更有效率的”。实际上,gcc生成的代码x == 0 || x == 1少于(x & ~1) == 0(x | 1) == 1。对于第一个,它足够聪明,可以将其识别为等效x <= 1并输出一个simple cmpl; setbe。其他人将其混淆并使其生成更糟糕的代码。
hobbs's

13
x <= 1或x <2更简单。
gnasher729 '16

9
@Kevin True for C ++,因为该标准会非常非常努力地使其无法编写兼容的代码。幸运的是,这是一个有关C#的问题;)
Voo

5
尽管我不知道C#编译器和.NET JITter的智能程度,但是大多数现代编译器已经可以像这样优化比较。实际代码中只需要一个比较
phuclv

78

由于参数是uintunsigned)您可以

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

可读性差(IMHO),但如果您计算每个字符(Code Golf或类似字符)

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

编辑:针对您编辑的问题

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

要么

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

12
如果是Code Golf,那就是return N<2?1:f(N-1)+f(n-2)。:P
Conor O'Brien

36

您还可以像这样检查所有其他位是否为0:

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

为了完整起见,感谢Matt提供了更好的解决方案:

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

在这两种情况下,您都需要注意括号,因为按位运算符的优先级低于==


我喜欢!谢谢。
user6048670 '16

15
少1个角色:(N|1)==1
马特

1
@atk 3 | 1是3,因为b0011 | B0001是b0011
勒内·沃格特

3
@atk这是按位或,不是逻辑或。没有短路。
isaacg '16

2
@Hoten正确的,但马特说少1个字符,而不是1个少操作
伊万·斯托夫

20

如果要使函数更有效,请使用查找表。查找表只有47个条目,令人惊讶地小-下一个条目将溢出32位无符号整数。当然,这也使得该函数的编写很简单。

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

您显然可以对阶乘做同样的事情。


14

如何使用位移位

如果要使用位移位并​​使代码有些晦涩(但简短),则可以执行以下操作:

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

对于N语言c 的无符号整数,N>>1丢弃低位。如果结果不为零,则表示N大于1。

注意:此算法效率极低,因为它不必要地重新计算已计算序列中的值。

更快的方式

一遍计算它,而不是隐式地构建一个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;
}

正如某些人所提到的,即使是64位无符号整数也不会花费很长时间。根据要尝试的大小,您将需要使用任意精度的整数。


1
不仅避免了指数增长的树,而且还避免了可能阻塞现代CPU管道的三元运算符的潜在分支。
mathreadler '16

2
您的“更快的方式”代码在C#中不起作用,因为uint它不能隐式转换为bool,并且该问题专门标记为C#。
法拉普

1
@pharap然后--N != 0代替。关键是O(n)比O(fibn(n))更可取。
马修·冈恩

1
扩大对@ MatthewGunn的点,O(FIB(N))是O(PHI ^ N)(见本推导stackoverflow.com/a/360773/2788187
康纳·克拉克

@RenéVogt我不是AC#开发人员。我主要是想评论O(fibn(N))算法的完全荒谬性。现在可以编译吗?(我添加了!= 0,因为c#不会将非零结果视为true。)如果将uint替换为uint64_t之类的标准东西,它将在直接c语言中工作(并起作用)。
马修·冈恩

10

使用uint(不能取负)时,可以检查是否 n < 2

编辑

或者对于特殊功能情况,您可以编写如下:

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

当然,这将导致相同的结果,但要付出额外的递归步骤。


4
@CatthalMF:但结果是一样的,因为1 * fibn(0) = 1 * 1 = 1
derpirscher '16

3
您的函数不是在计算阶乘,不是斐波那契吗?
Barmar '16

2
@Barmar是的,的确是析因,因为那是原始问题
derpirscher 2016年

3
也许是最好不要把它fibn
pie3636

1
@ pie3636我称它为fibn,是因为它在原始问题中是这样称呼的,后来我没有更新答案
derpirscher

6

只需检查是否N<= 1,因为您知道N是无符号的,那么只能有2个条件N <= 1导致TRUE:0和1

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

它是签名的还是未签名的,甚至都无关紧要?该算法产生无限递归与负输入端,所以有在处理它们没有害处相当于0或1
Barmar

@Barmar确保这很重要,尤其是在这种情况下。OP问他是否可以简化(N == 0 || N == 1)。您知道它不会小于0(因为它将被签名!),最大值可能是1。 N <= 1简化了它。我想不能保证无符号类型,但是应该在其他地方处理。
james

我的观点是,如果声明了int N,并且您保留了原始条件,那么当N的原始条件为N时,它将无限递归。由于这是未定义的行为,因此我们实际上无需担心。因此,无论声明如何,我们都可以假定N为非负数。
Barmar

或者,我们可以使用负输入来做任何我们想做的事情,包括将它们视为递归的基本情况。
Barmar

@Barmar非常确定,如果尝试将其设置为负数,则uint将始终转换为无符号
詹姆斯

6

免责声明:我不了解C#,也没有测试此代码:

但是我想知道是否可以通过将其更改为单个比较来使其更加紧凑和高效...

无需进行移位或类似操作,它仅使用一个比较,并且效率应该更高(我认为O(n)vs O(2 ^ n)?)。该函数的主体更加紧凑,尽管声明结束了它的长度。

(要消除递归的开销,有一个迭代版本,如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:这是累加器迭代的常见功能模式。如果您替换为N--,则表示N-1您实际上没有在使用任何突变,因此可以在纯功能方法中使用它。


4

这是我的解决方案,在优化此简单函数方面没有太多的事情,另一方面,我在这里提供的是作为递归函数的数学定义的可读性。

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

        case  1: return 1;

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

斐波那契数的数学定义也是如此。

在此处输入图片说明

进一步迫使开关盒建立一个查找表。

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
您的解决方案的优点是仅在需要时才进行计算。最好是一个查询表。替代奖励:f(n-1)= someCalcOf(f(n-2)),因此不需要完整的重新运行。
卡斯滕

@Karsten我为开关添加了足够的值来为其创建查找表。我不确定替代奖金的工作方式。
Khaled.K,2016年

1
这如何回答这个问题?
克拉克·肯特

@SaviourSelf它归结为一个查找表,答案中有视觉方面的解释。stackoverflow.com/a/395965/2128327
Khaled.K,2016年

switch当您有一系列答案时,为什么还要使用a ?
纳希基(Nayuki)


1

Dmitry的答案是最好的,但是如果它是Int32返回类型并且您有更大的整数集可供选择,则可以执行此操作。

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

2
那比原始的短多少?
MCMastery '16

2
@MCMastery它不短。正如我提到的那样,如果原始返回类型为int32,并且他从大量有效数字中进行选择,则更好。不得不写的
想法

1
OP的原因似乎与优化有关。这是一个坏主意,原因有几个:1)在每个递归调用中实例化一个新对象是一个非常不好的主意,2)List.Contains是O(n),3)简单地做两个比较而不是(N > -3 && N < 3)将给出更短,更易读的代码。
Groo

@Groo如果值是-1​​0,-2、5、7、13,该怎么办
CathalMF '16

这不是OP要求的。但是无论如何,您仍然1)不想在每个调用中创建新实例,2)最好使用(单个)哈希集,3)针对特定问题,还可以优化哈希函数以是纯净的,甚至使用像其他答案中所建议的那样巧妙地安排按位运算。
Groo

0

斐波那契数列是一系列数字,其中一个数字是通过将两个数字相加而得到的。有两种类型的起点:(0,1,1,2,..)和(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
-----------------------------------------

N在这种情况下,位置从开始1,而不是0-based数组索引。

使用C#6 Expression-body功能和Dmitry关于三元运算符的建议,我们可以针对类型1编写具有正确计算的单行函数:

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

对于类型2:

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

-2

参加聚会有点晚,但您也可以这样做 (x==!!x)

!!x将a值转换为1not 0,然后将其保留0
我在C模糊处理中经常使用这种东西。

注意:这是C,不确定是否可以在C#中使用


4
不知道为什么会这样。即使粗略试图将此作为uint n = 1; if (n == !!n) { }Operator '!' cannot be applied to operand of type 'uint'!nC#中。仅仅因为某些东西可以在C中工作并不意味着它可以在C#中工作;甚至#include <stdio.h>在C#中也不起作用,因为C#没有“ include”预处理程序指令。这些语言与C和C ++的区别更大。
CVn

2
哦。好的。抱歉:(
一个正常的夜晚

@OneNormalNight(x == !! x)这将如何工作。考虑我的输入是5。(5 == !! 5)。它将给出真实的结果
VINOTH ENERGETIC 2016年

1
@VinothKumar !! 5计算为1。(5 == !! 5)计算为(5 == 1),其计算为false。
一个正常的夜晚

@OneNormalNight是的,我现在知道了。!(5)给出1再次施加它给出0而不是1
VINOTH高能

-3

因此,我创建了一个List这些特殊整数,并检查是否N与之有关。

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

您也可以将扩展方法用于不同的用途,该方法Contains仅被调用一次(例如,当您的应用程序正在启动和加载数据时)。这提供了更清晰的样式,并阐明了与值(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);
    }
}

应用它:

N.PertainsTo(ints)

这可能不是最快的方法,但是对我来说,这似乎是一种更好的风格。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.