惰性评估和时间复杂度


72

我一直在研究stackoverflow非临时性惰性评估,这使我想到了Keegan McAllister的演讲:为什么要学习Haskell。在幻灯片8中,他显示了最小功能,定义为:

minimum = head . sort

并指出其复杂度为O(n)。我不明白,如果按替换排序为O(nlog n),为什么说复杂度是线性的。帖子中提到的排序不能是线性的,因为它不假设任何有关数据的信息,这是线性排序方法(例如计数排序)所要求的。

懒惰的评估在这里扮演着神秘的角色吗?如果是这样,其背后的解释是什么?


9
请注意,该幻灯片正确地标明了“谨慎sort实施”。很容易写出sort这样的文章O(n log n),但撰写该文章所花费的时间或更短,但并非出于您的考虑。

Answers:


60

minimum = head . sort中,sort将不能完全做到,因为它不会做前期。将sort根据需要生产出的第一个元素,通过要求才会被尽可能多的完成head

在例如mergesort中,首先n将成对比较列表中的数字,然后将获胜者进行配对和比较(n/2数字),然后将新的获胜者(n/4)等。总而言之,O(n)进行比较以产生最小的元素。

mergesortBy less [] = []
mergesortBy less xs = head $ until (null.tail) pairs [[x] | x <- xs]
  where
    pairs (x:y:t) = merge x y : pairs t
    pairs xs      = xs
    merge (x:xs) (y:ys) | less y x  = y : merge (x:xs) ys
                        | otherwise = x : merge  xs (y:ys)
    merge  xs     []                = xs
    merge  []     ys                = ys

可以扩展上述代码,以使用产生的大量比较标记它产生的每个数字:

mgsort xs = go $ map ((,) 0) xs  where
  go [] = []
  go xs = head $ until (null.tail) pairs [[x] | x <- xs]   where
    ....
    merge ((a,b):xs) ((c,d):ys) 
            | (d < b)   = (a+c+1,d) : merge ((a+1,b):xs) ys    -- cumulative
            | otherwise = (a+c+1,b) : merge  xs ((c+1,d):ys)   --   cost
    ....

g n = concat [[a,b] | (a,b) <- zip [1,3..n] [n,n-2..1]]   -- a little scrambler

运行它几个列表长度,我们看到它确实是~ n

*Main> map (fst . head . mgsort . g) [10, 20, 40, 80, 160, 1600]
[9,19,39,79,159,1599]

要查看排序代码本身是否为~ n log n,我们对其进行更改,以使每个产生的数字都带有其自身的成本,然后通过对整个排序列表求和得出总成本:

    merge ((a,b):xs) ((c,d):ys) 
            | (d < b)   = (c+1,d) : merge ((a+1,b):xs) ys      -- individual
            | otherwise = (a+1,b) : merge  xs ((c+1,d):ys)     --   cost

这是各种长度列表的结果,

*Main> let xs = map (sum . map fst . mgsort . g) [20, 40, 80, 160, 320, 640]
[138,342,810,1866,4218,9402]

*Main> map (logBase 2) $ zipWith (/) (tail xs) xs
[1.309328,1.2439256,1.2039552,1.1766101,1.1564085]

上面显示了随着列表长度的增加,经验的增长顺序,n正如~ n log n计算通常显示的那样,列表迅速减少。另请参阅此博客文章。快速相关检查:

*Main> let xs = [n*log n | n<- [20, 40, 80, 160, 320, 640]] in 
                                    map (logBase 2) $ zipWith (/) (tail xs) xs
[1.3002739,1.2484156,1.211859,1.1846942,1.1637106]

编辑:懒惰的评估可以比喻为生产者/消费者习惯用语1,以独立的备注存储为中介。我们编写的任何生产性定义都定义了一个生产者,该生产者将根据其消费者的需求(但不尽早)一点一点地产生其输出。所产生的任何内容都会被记录下来,这样,如果另一个消费者以不同的速度消费相同的输出,它将访问先前填充的相同存储。

当没有更多的消费者引用存储时,就会收集垃圾。有时,通过优化,编译器可以完全消除中间存储,从而淘汰中间人。

1另请参见:Oleg Kiselyov,Simon Peyton-Jones和Amr Sabry撰写的Simple Generators v。Lazy Evaluation


21

假设minimum' :: (Ord a) => [a] -> (a, [a])是一个函数,它返回列表中最小的元素以及删除了该元素的列表。显然,这可以在O(n)时间内完成。如果然后定义sort

sort :: (Ord a) => [a] -> [a]
sort xs = xmin:(sort xs')
    where
      (xmin, xs') = minimum' xs

那么惰性评估意味着(head . sort) xs仅在第一个元素中进行过计算。如您所见,该元素只是对中的第一个元素minimum' xs,它以O(n)时间计算。

当然,正如delnan所指出的,复杂度取决于的实现sort


您的观点是正确的,但请注意您的排序不是O(n log n),但我知道了;)
leco

16
@LeonardoPassos就是这个答案的美。:)这是一个选择排序,O(n ^ 2),但它在O(n)时间内产生最小值。
Will Ness

4
这个例子有点让人误解,因为我们想(head . sort)在O(n)中用作最小函数,但是您的排序需要这样的最小函数。这是更有趣的从一种,它获得O(n)的最小值已经使用这样的功能。
约阿希姆·布雷特纳

@JoachimBreitner:我同意。但是我可能会说它是人为的,而不是误导性的:)
gspr

这里的另一个答案是通过将发现最小值和将其去除分开来避免此问题,当已知最小元素时,这在O(n)中就变得微不足道了。
尼斯,

14

您已经获得了很多解决具体问题的答案 head . sort。我只添加一些更一般的陈述。

通过急切的评估,各种算法的计算复杂性以简单的方式组成。例如,的最小上限(LUB)f . g必须是和的LUB之fg。因此,您可以治疗fg视为黑匣子,并仅根据其LUB进行推理。

懒惰的评价,但是,f . g可以更好地拥有比LUB的总和fg的鲁布斯。您不能使用黑盒推理来证明LUB。您必须分析实现及其交互。

因此,经常被引用的事实是,懒惰评估的复杂性要比热切评估难得多。请考虑以下内容。假设您正在尝试改善形式为的一段代码的渐近性能f . g。在热切的语言,还有对显而易见的策略,你可以跟着这样做:挑选更复杂的fg,并首先提高的那一个。如果成功了,那么您就f . g完成了任务。

另一方面,用一种惰性语言,您可能会遇到以下情况:

  • 您可以改善fand的复杂性g,但是f . g并不能改善(甚至变得更糟)。
  • 您可以通过f . g无济于事(甚至恶化f或的方式进行改进g

12

解释取决于的实现sort,在某些实现中,它不是正确的。例如,如果将插入排序插入列表的末尾,则惰性求值无济于事。因此,让我们选择一个要查看的实现,为了简单起见,让我们使用选择排序:

sort [] = []
sort (x:xs) = m : sort (delete m (x:xs)) 
  where m = foldl (\x y -> if x < y then x else y) x xs

该函数显然使用O(n ^ 2)时间对列表进行排序,但是由于head只需要列表的第一个元素,sort (delete x xs)因此永远不会求值!


8

并不是那么神秘。您需要对列表中的多少进行排序才能提供第一个元素?您需要找到最小的元素,可以在线性时间内轻松完成。碰巧的是,对于某些sort惰性评估的实现,它会为您完成此任务。


7

在实践中看到这一现象的一种有趣方式是跟踪比较功能。

import Debug.Trace
import Data.List

myCmp x y = trace (" myCmp " ++ show x ++ " " ++ show y) $ compare x y

xs = [5,8,1,3,0,54,2,5,2,98,7]

main = do
    print "Sorting entire list"
    print $ sortBy myCmp xs

    print "Head of sorted list"
    print $ head $ sortBy myCmp xs

首先,请注意整个列表的输出与跟踪消息交错的方式。其次,注意仅计算头部时跟踪消息的相似之处。

我刚刚通过Ghci进行了测试,它不完全是O(n):需要15次比较才能找到第一个元素,而不是应该需要的10个元素。但是它仍然小于O(n log n)。

编辑:正如Vitus指出的那样,采用15个比较而不是10个比较与说不是O(n)是不同的。我只是说这需要花费比理论上的最小值更多的费用。


9
O(n)并不意味着使用10个比较。即使您使用了50个比较,也不能说它不是O(n),因为O(5n) = O(n)。您必须查看比较次数如何随输入长度变化。
Vitus 2012年

+1这是一个很好的答案(除了对O(n)的轻微误导),我不明白为什么它被否决了。
休恩

3
这是比较计数的图表:imgur.com/vfEPp。蓝线是对整个列表进行排序时的比较,绿线
Daniel

1
@DanielVelkov,您应该在此处添加此图像作为答案。
尼斯,

我同意那个。很棒的图。它还很好地演示了O(n。log n)与O(n)的接近程度。顺便说一下,下线的斜率是多少?看起来像1,但很难看。
Paul Johnson

6

受到保罗·约翰逊(Paul Johnson)答案的启发,我绘制了这两个函数的增长率。首先,我修改了他的代码,以每个比较显示一个字符:

import System.Random
import Debug.Trace
import Data.List
import System.Environment

rs n = do
    gen <- newStdGen
    let ns = randoms gen :: [Int]
    return $ take n ns

cmp1 x y = trace "*" $ compare x y
cmp2 x y = trace "#" $ compare x y

main = do
    n <- fmap (read . (!!0)) getArgs
    xs <- rs n
    print "Sorting entire list"
    print $ sortBy cmp1 xs

    print "Head of sorted list"
    print $ head $ sortBy cmp2 xs

计算*#字符,我们可以在等距的点采样比较计数(对不起,我的python):

import matplotlib.pyplot as plt
import numpy as np
import envoy

res = []
x = range(10,500000,10000)
for i in x:
    r = envoy.run('./sortCount %i' % i)
    res.append((r.std_err.count('*'), r.std_err.count('#')))

plt.plot(x, map(lambda x:x[0], res), label="sort")
plt.plot(x, map(lambda x:x[1], res), label="minimum")
plt.plot(x, x*np.log2(x), label="n*log(n)")
plt.plot(x, x, label="n")
plt.legend()
plt.show()

运行脚本将为我们提供以下图形:

增长率

下线的斜率是..

>>> import numpy as np
>>> np.polyfit(x, map(lambda x:x[1], res), deg=1)
array([  1.41324057, -17.7512292 ])

..1.41324057(假设它是线性函数)


1
您的“ n * log(n)”行使用日志以“ e”为基(2.714 ..),但是排序函数通常随对数为2而增加(由于比较运算符强加了二进制拆分)。根据python文档,您可以将基数作为第二个参数传递给“ log”,在这种情况下,我怀疑您的红线和蓝线会同时出现。比较您的“最小”行(我假设实际上是“ head。sortBy cmp2”与线性“(n-1)”行)进行比较(这是“最小”的理论最小比较数)也很有趣。 。
保罗·约翰逊
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.