【问题标题】:Is it possible to simplify (x == 0 || x == 1) into a single operation?是否可以将 (x == 0 || x == 1) 简化为单个操作?
【发布时间】: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)
  • 我完全赞成减少纳秒,但是如果您在使用递归的方法中进行了简单的比较,为什么要在比较的效率上花费精力,而将递归留在那里呢?
  • 您使用递归的方式计算法波纳契数,那么您想提高性能吗?为什么不把它改成循环呢?还是使用快速电源?

标签: c# algorithm optimization arithmetic-expressions


【解决方案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)(参见这个推导 stackoverflow.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 它归结为一个查找表,答案中解释了视觉方面。 stackoverflow.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)对于特定问题,您还可以优化哈希函数以是纯粹的,或者甚至像其他答案中建议的那样使用巧妙排列的按位运算。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-09-16
          • 1970-01-01
          • 2013-01-09
          • 1970-01-01
          • 2011-11-20
          相关资源
          最近更新 更多