List.fold和foldBack的渐近时间复杂度

时间:2012-03-05 21:06:25

标签: algorithm f# complexity-theory f#-interactive

我试图理解这两个功能的时间复杂性。我尝试过两种方法,这就是我提出的方法

List.foldBack (@) [[1];[2];[3];[4]] [] => [1] @ List.foldBack (@) [[2];[3];[4]] []
=> [1] @ ([2] @ List.foldBack (@) [[3];[4]] [])
=> [1] @ ([2] @ ([3] @ List.foldBack (@) [4] []))
=> [1] @ ([2]@([3] @ ([4] @ List.foldBack[])))
=> [1]@([2]@([3]@([4]@([])))
=> [1; 2; 3; 4]


List.fold (@) [] [[1];[2];[3];[4]]
=> List.fold (@) (([],[1])@ [2]) [[3];[4]]
=> List.fold (@)  ((([]@[1])@[2])@[3]) [[4]]
=> List.fold (@)  (((([]@[1])@[2])@[3])@[4]) []
=> (((([]@[1])@[2])@[3])@[4])

现在在我看来它们都是线性的,因为它需要相同的计算量来实现相同的结果。我是正确的还是我缺少的东西?

3 个答案:

答案 0 :(得分:5)

如果每个内部操作都是Θ(1),List.foldList.foldBack是O(n),其中n是列表的长度。

然而,要估计渐近时间复杂度,您需要依赖Θ(1)运算。在你的例子中,事情有点微妙。

假设您需要连接n列表,其中每个列表都包含m个元素。由于@是左操作数长度的O(n),因此我们的复杂度为foldBack

  m + ... + m // n occurences of m
= O(m*n)

fold

  0 + m + 2*m + ... + (n-1)*m // each time length of left operand increases by m
= m*n*(n-1)/2
= O(m*n^2)

因此,通过您使用@的天真方式,foldBack是线性的,而fold是输入列表大小的二次方。

值得注意的是@是关联的(a @(b @ c)=(a @ b)@ c);因此,在这种情况下,foldfoldBack的结果相同。

实际上,如果内部运算符是非关联的,我们需要使用foldfoldBack来选择正确的顺序。并且通过将列表转换为数组,使F#中的List.foldBack成为尾递归;这个操作也有一些开销。

答案 1 :(得分:3)

List.foldList.foldBack函数都是对其函数参数的T( n )调用,其中 n 是列表的长度。但是,您传递的是(@)函数,它不是T(1)而是T( m ),其中 m 是第一个参数列表的长度。

特别是:

(((([]@[1])@[2])@[3])@[4])

是T( n ²),因为[1]@[2]是一个操作,然后[1;2]@[3]是另外两个操作,然后[1;2;3]@[4]是另外三个操作。

答案 2 :(得分:1)

在一个天真的实现FoldBackO(n^2),因为你需要继续遍历列表。在F#中,编译器实际创建一个临时数组并将其反转,然后调用Fold,因此两者的时间复杂度(O)为O(n),尽管Fold会以稍微快一点的速度加快