我正在进行Haskell任务,我正在考虑如何让我的代码更快。 例如,我的下面的因子函数找到某个整数的除数的数量。
factors :: Int -> Int
factors x = length [n | n <- [1..x], mod x n == 0]
但是,我突然意识到,通过避免使用“长度”,我可以使代码更快。
factors :: Int -> Int
factors x = go 1
where
go :: Int -> Int
go i
| i == x = 1
| mod x i == 0 = 1 + go (i + 1)
| otherwise = go (i + 1)
我想知道Haskell的长度函数是否为O(n),如C中的strlen()或Java中的String.length()中的O(1)。
此外,编写代码是否更好或更有效?
答案 0 :(得分:2)
此外,编写代码是否更好或更有效?
整数因子分解是最着名的问题之一。肯定已经为此提出了很多算法,即使我不够专业也不能提出建议(CS.SE即将到来,如果需要可以提供帮助)。这些提议都不是多项式时间,但这并不能阻止它们比琐碎的方法更快。
即使不查看文献,也可以找到一些简单的优化。
原始代码会扫描整个列表[1..x]
,但这不是必需的。我们可以在sqrt x
停留,因为此后不再有除数。
更多:在我们找到除数m
后,我们可以将x
除以m
(尽可能多次),然后用这个新数字递归。例如。如果我们尝试x = 1000
后m=2
,我们会计算1000 -> 500 -> 250 -> 125
,然后在2
中找到新的除数(大于125
)。请注意这是如何使数字更小。
我将在Haskell中实施这些策略作为练习:-P
答案 1 :(得分:2)
Willem Van Onsem领导着一个奇怪的,我认为不容易辩护的论点(在某些“理论”意义上我们无法知道length
的复杂性,因为编译器可以自由地优化代码和数据结构)。撇开这是否是正确的还是埋没的矛盾,我不认为这是解决这类问题的一种非常富有成效的方法。
通过查看length
的定义,您实际上可以推断[a]
(和许多其他函数)的复杂性:
Prelude> :info []
data [] a = [] | a : [a] -- Defined in ‘GHC.Types’
列表是归纳定义的;您可以从该定义(几乎只是常规的haskell)中看到,在顶层,列表是构造函数[]
或:
。显然length
必须在此结构上递归n
次,因此必须是O(n)。
能够以这种方式至少直观地推理是非常重要的,特别是关于无处不在的列表。例如快速(!!)
的复杂性是什么?
如果你想在懒惰的情况下深入研究时间复杂性,那么你需要选择Okasaki的“Purely Functional Data Structures”。
答案 2 :(得分:1)
从理论的角度来看,我们无法知道 length
是否为θ(n),我们知道它是 O(n),但它从技术上讲,Haskell可以更快地为已知列表实现它。
因为Haskell编译器可以随心所欲地实现列表。但是无所谓,因为在那种情况下生成首先列出的列表将采用θ(n)。
请注意,即使编译器使用更专用的数据结构,Haskell也是惰性的,因此 list comprehension 不会产生完整的列表,但更多的是可以>的函数em>懒洋洋地生成一个列表。
最后,如果我们急切地评估列表理解,那么它将再次要求 O(n)首先生成列表。因此,即使获得长度非常快,生成列表也需要 O(n)作为下限。因此无论length
的效率如何,算法仍将与输入线性对比。
您自己的实现再次使用 O(n)(说实话并不是很安全)。不过,您可以轻松地将数字的因子分解加速到 O(sqrt n):
factors :: Int -> Int
factors x = go 1
where
go :: Int -> Int
go i | i2 > x = 0
| i2 == x = 1
| mod x i == 0 = 2 + go (i+1)
| otherwise = go (i + 1)
where i2 = i*i
这里我们枚举从1到sqrt(n)。每次我们找到因子a
时,我们都知道存在 co </ em> - 因子b = x/a
。只要a
不等于sqrt(x),我们就知道它们是不同的。如果a
等于sqrt(x),我们知道a
等于b
,因此我们将其视为一个。
话虽如此,肯定有更快的方法。这是一个涉及大量研究的主题,已经产生了更有效的算法。我不是说上面的速度最快,但在时间复杂度方面肯定是一个巨大的改进。
答案 3 :(得分:1)
使用列表推导构建列表已经O(n)
。因此,在使用长度函数时没有太多开销,在最坏的情况下,该函数应该具有复杂度O(n)
。