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

Posted

技术标签:

【中文标题】是否可以将 (x == 0 || x == 1) 简化为单个操作?【英文标题】:Is it possible to simplify (x == 0 || x == 1) into a single operation? 【发布时间】:2016-07-21 10:53:39 【问题描述】:

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

public uint fibn ( uint N ) 

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

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

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

进行单一比较。有没有一些花哨的位移操作可以做到这一点?

【问题讨论】:

为什么?可读性强,意图很明确,而且也不贵。为什么要改成一些更难理解且不能明确识别意图的“聪明”位模式匹配? 这不是真正的斐波那契吧? fibonaci 将前两个值相加。您的意思是 fibn(N-1) + fibn(N-2) 而不是 N * fibn(N-1) 我完全赞成减少纳秒,但是如果您在使用递归的方法中进行了简单的比较,为什么要在比较的效率上花费精力,而将递归留在那里呢? 您使用递归的方式计算法波纳契数,那么您想提高性能吗?为什么不把它改成循环呢?还是使用快速电源? 【参考方案1】:

斐波那契数列是一系列数字,其中一个数字是通过将其前面的两个数字相加得到的。起点有两种类型:(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 feature 和Dmitry 对ternary operator 的建议,我们可以编写一个正确计算类型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】:

因为 N 是 uint,只需使用

N <= 1

【讨论】:

正是我的想法; N 是 uint!这应该是答案,真的。【参考方案3】:

如何用位移来做到这一点

如果您想使用 bitshift 并使代码有点晦涩(但简短),您可以这样做:

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

对于 c 语言中的无符号整数 NN&gt;&gt;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 位无符号整数也不需要很长时间就会溢出。根据您尝试的大小,您需要使用任意精度的整数。

【讨论】:

不仅避免了指数增长树,而且还避免了可能阻塞现代 CPU 管道的三元运算符的潜在分支。 您的“速度更快”代码在 C# 中不起作用,因为 uint 不能隐式转换为 bool,并且该问题专门标记为 C#。 @pharap 然后改用--N != 0。关键是 O(n) 比 O(fibn(n)) 更可取。 扩展@MatthewGunn 的观点,O(fib(n)) 是 O(phi^n)(参见这个推导 ***.com/a/360773/2788187) @RenéVogt 我不是 c# 开发人员。我主要是想评论 O(fibn(N)) 算法的完全荒谬性。现在可以编译了吗? (我添加了 != 0,因为 c# 不会将非零结果视为真。)如果您将 uint 替换为 uint64_t 之类的标准,它可以在直接 c 中工作(并且工作)。【参考方案4】:

这是我的解决方案,优化这个简单函数的内容不多,另一方面,我在这里提供的是作为递归函数的数学定义的可读性。

public uint fibn(uint N) 

    switch(N)
    
        case  0: return 1;

        case  1: return 1;

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

斐波那契数的数学定义以类似的方式..

进一步强制 switch case 构建查找表。

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

【讨论】:

您的解决方案的优点是它只在需要时才进行计算。最好是一个查找表。替代奖励:f(n-1) = someCalcOf( f(n-2) ),因此不需要完全重新运行。 @Karsten 我已经为开关添加了足够的值来为其创建查找表。我不确定替代奖金是如何运作的。 这如何回答这个问题? @SaviourSelf 它归结为一个查找表,答案中解释了视觉方面。 ***.com/a/395965/2128327 当您有一系列答案时,为什么还要使用switch【参考方案5】:

所以我创建了这些特殊整数的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)

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

【讨论】:

【参考方案6】:

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

x == 0 || x == 1

在逻辑上等同于以下每一个:

(x &amp; 1) == x (x &amp; ~1) == 0 (x | 1) == 1 (~x | 1) == (uint)-1 x &gt;&gt; 1 == 0

奖金:

x * x == x(证明有点费力)

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

x == 0 || x == 1 x &lt;= 1(因为x是一个无符号整数) x &lt; 2(因为x是一个无符号整数)

【讨论】:

别忘了(x &amp; ~1) == 0 但不要打赌其中任何一个会“更有效率”。 gcc 实际上为x == 0 || x == 1 生成的代码比为(x &amp; ~1) == 0(x | 1) == 1 生成的代码少。对于第一个,它足够聪明,可以将其识别为等同于x &lt;= 1,并输出一个简单的cmpl; setbe。其他人混淆它并使其生成更糟糕的代码。 x @Kevin True for C++,因为该标准非常非常努力地试图让编写兼容的代码成为不可能。幸运的是,这是一个关于 C# 的问题;) 大多数现代编译器已经可以optimize comparisons like this 虽然我不知道 C# 编译器和 .NET JITter 有多聪明。实际代码中只需要一次比较【参考方案7】:

派对有点晚了,但你也可以(x==!!x)

!!x 如果不是 0,则将 a 值转换为 1,如果是,则将其保留为 0。 我在 C 混淆中经常使用这种东西。

注意:这是 C,不确定它是否适用于 C#

【讨论】:

不知道为什么这会被赞成。即使是粗略地尝试这个 uint n = 1; if (n == !!n) 在 C# 中的 !n 上给出 Operator '!' cannot be applied to operand of type 'uint'。仅仅因为某些东西在 C 中有效并不意味着它在 C# 中有效;甚至 #include &lt;stdio.h&gt; 在 C# 中也不起作用,因为 C# 没有“include”预处理器指令。这些语言与 C 和 C++ 不同。 哦。好的。对不起:( @OneNormalNight (x==!!x) 这将如何工作。考虑我的输入是 5。(5 == !!5)。它会给结果为真 @VinothKumar !!5 评估为 1。 (5 == !!5) 评估 (5 == 1) 评估为 false。 @OneNormalNight 是的,我现在明白了。 !(5) 再次给出 1,它给出 0。不是 1。【参考方案8】:

如果你想做的是让函数更高效,那么使用查找表。查找表非常小,只有 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];
    

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

【讨论】:

【参考方案9】:

免责声明:我不懂 C#,也没有测试过这段代码:

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

不需要位移等,这仅使用一个比较,它应该更有效(我认为 O(n) vs O(2^n)?)。函数的主体更紧凑,尽管它在声明结束时有点长​​。

(为了消除递归的开销,有迭代版本,如Mathew Gunn's answer)

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,您实际上没有使用任何突变,这使得它可以在纯函数方法中使用。

【讨论】:

【参考方案10】:

当您使用不能为负数的 uint 时,您可以检查 n &lt; 2

编辑

或者对于那个特殊的函数情况,你可以这样写:

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

这将导致相同的结果,当然是以额外的递归步骤为代价的。

【讨论】:

@CatthalMF:但结果是一样的,因为1 * fibn(0) = 1 * 1 = 1 你的函数不是计算阶乘,不是斐波那契吗? @Barmar 是的,确实是阶乘,因为那是最初的问题 那么最好不要叫它fibn @pie3636 我称它为 fibn 是因为它在原始问题中是这样称呼的,后来我没有更新答案【参考方案11】:

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

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

感谢Matt 提供了更好的解决方案:

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

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

【讨论】:

我喜欢!谢谢。 少1个字符:(N|1)==1 @atk 3|1 是 3,因为 b0011|b0001 是 b0011 @atk 这是按位或,不是逻辑或。没有短路。 @Hoten 正确,但马特说少了 1 个字符,而不是少了 1 个操作【参考方案12】:

只需检查 N 是否 N <= 1 导致 TRUE:0 和 1

public uint fibn ( uint N ) 

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

【讨论】:

签名或未签名是否重要?该算法产生带有负输入的无限递归,因此将它们等同于 0 或 1 并没有什么坏处。 @Barmar 确定这很重要,尤其是在这种特定情况下。 OP问他是否可以完全简化(N == 0 || N == 1)。你知道它不会小于 0(因为它会被签名!),最大值可能是 1。N &lt;= 1 简化了它。我猜 unsigned 类型不能保证,但我会说应该在其他地方处理。 我的意思是,如果它被声明为int N,并且您保持原始条件,那么当 N 的原始条件为负时,它将无限递归。由于这是未定义的行为,我们实际上不需要担心它。所以我们可以假设 N 是非负的,不管声明如何。 或者我们可以对负输入做任何我们想做的事情,包括将它们视为递归的基本情况。 @Barmar 很确定如果您尝试设置为负数,uint 将始终转换为无符号数【参考方案13】:

因为参数是uint (unsigned) 你可以放

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

可读性较差(恕我直言),但如果您计算每个字符(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);

【讨论】:

如果是 Code Golf,那就是 return N&lt;2?1:f(N-1)+f(n-2)。 :P【参考方案14】:

Dmitry 的答案是最好的,但如果它是 Int32 返回类型并且您有一组更大的整数可供选择,您可以这样做。

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

【讨论】:

怎么比原来的短? @MCMastery 它并不短。正如我所提到的,只有当原始返回类型是 int32 并且他从大量有效数字中进行选择时才会更好。不必写 (N == -1 || N == 0 || N == 1 || N == 2) OP的原因似乎与优化有关。这是一个坏主意,有几个原因:1)在每个递归调用中实例化一个新对象是一个非常糟糕的主意,2)List.Contains 是 O(n),3)简单地进行两次比较(N &gt; -3 &amp;&amp; N &lt; 3)会给出更短、更易读的代码。 @Groo 如果这些值是 -10、-2、5、7、13 这不是 OP 要求的。但无论如何,您仍然 1)不想在每次调用中创建一个新实例,2)最好使用(单个)哈希集,3)对于特定问题,您还可以优化哈希函数以是纯粹的,或者甚至像其他答案中建议的那样使用巧妙排列的按位运算。

以上是关于是否可以将 (x == 0 || x == 1) 简化为单个操作?的主要内容,如果未能解决你的问题,请参考以下文章

CF 633 E. Binary Table

Matlab如何提取非零元素

是否可以在 Bash 脚本的 switch case 中结合两个条件?

下面代码的C语言中while(x--)是啥意思

Pow(x, n) -- leetcode

判定一个数num是否为x的幂