是否可以将 (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 语言中的无符号整数 N
,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 位无符号整数也不需要很长时间就会溢出。根据您尝试的大小,您需要使用任意精度的整数。
【讨论】:
不仅避免了指数增长树,而且还避免了可能阻塞现代 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 & 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
是一个无符号整数)
【讨论】:
别忘了(x & ~1) == 0
但不要打赌其中任何一个会“更有效率”。 gcc 实际上为x == 0 || x == 1
生成的代码比为(x & ~1) == 0
或(x | 1) == 1
生成的代码少。对于第一个,它足够聪明,可以将其识别为等同于x <= 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 <stdio.h>
在 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 < 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 <= 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<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 > -3 && N < 3
)会给出更短、更易读的代码。
@Groo 如果这些值是 -10、-2、5、7、13
这不是 OP 要求的。但无论如何,您仍然 1)不想在每次调用中创建一个新实例,2)最好使用(单个)哈希集,3)对于特定问题,您还可以优化哈希函数以是纯粹的,或者甚至像其他答案中建议的那样使用巧妙排列的按位运算。以上是关于是否可以将 (x == 0 || x == 1) 简化为单个操作?的主要内容,如果未能解决你的问题,请参考以下文章