我试图理解这两个功能的时间复杂性。我尝试过两种方法,这就是我提出的方法
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])
现在在我看来它们都是线性的,因为它需要相同的计算量来实现相同的结果。我是正确的还是我缺少的东西?
答案 0 :(得分:5)
如果每个内部操作都是Θ(1),List.fold
和List.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);因此,在这种情况下,fold
和foldBack
的结果相同。
实际上,如果内部运算符是非关联的,我们需要使用fold
或foldBack
来选择正确的顺序。并且通过将列表转换为数组,使F#中的List.foldBack
成为尾递归;这个操作也有一些开销。
答案 1 :(得分:3)
List.fold
和List.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)
在一个天真的实现FoldBack
是O(n^2)
,因为你需要继续遍历列表。在F#中,编译器实际创建一个临时数组并将其反转,然后调用Fold
,因此两者的时间复杂度(O
)为O(n)
,尽管Fold
会以稍微快一点的速度加快