我正在尝试比较这两个函数以查看哪个函数具有最佳算法。我一直在关注n复杂度的顺序,虽然我不知道如何以数学方式得出它(这是一种耻辱)但我有时会猜测它的顺序。我想要知道算法是否比另一个好,我需要从渐近时间,复杂性和实验方面来看待它们。
let flatten1 xs = List.fold (@) [] xs
let flatten2 xs = List.foldBack (@) xs []
我使用了F##time
功能,这就是我得到的。
Real: 00:00:00.001, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0
val it : int list =
[1; 2; 3; 5; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11; 12; 13; 14; 15; 16; 17; 18; 19;
20; 5; 4; 5; 6]
>
Real: 00:00:00.001, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0
val it : int list =
[1; 2; 3; 5; 2; 3; 4; 5; 6; 7; 8; 9; 10; 11; 12; 13; 14; 15; 16; 17; 18; 19;
20; 5; 4; 5; 6]
答案 0 :(得分:5)
xs
长度为n
且每个操作f
为O(1),List.fold f xs
和List.foldBack f xs
具有相同的复杂度O(n)
然而,@
比这更复杂。假设您在长度为flatten1
的{{1}}上运行flatten2
和xs
,其中每个元素都是长度为n
的列表。结果列表的长度为m
由于n*m
为@
,其中O(k)
是第一个列表的长度,k
的复杂性为:
flatten1
如果是// After each `@` call, the first list (the accumulator) increases its length by `m`
O(m + 2*m + 3*m + ... + (n-1)*m)
= O(n*(n-1)*m/2)
,则第一个列表始终是长度为flatten2
的列表:
m
您可以轻松地看到O(m + m + ... + m) // n-1 steps
= O((n-1)*m)
比flatten2
更有效率。无论如何,时间复杂度的差异将主导flatten1
的额外分配。为了说明,这是一个显示差异的快速测试
List.foldBack
请注意,您可以使用List.concat,这是let flatten1 xs = List.fold (@) [] xs
let flatten2 xs = List.foldBack (@) xs []
let xs = [ for _ in 1..1000 -> [1..100] ]
#time "on";;
// Real: 00:00:01.456, CPU: 00:00:01.466, GC gen0: 256, gen1: 124, gen2: 1
let xs1 = flatten1 xs;;
// Real: 00:00:00.007, CPU: 00:00:00.000, GC gen0: 1, gen1: 0, gen2: 0
let xs2 = flatten2 xs;;
函数的有效实现。
答案 1 :(得分:1)
如有疑问请查看来源(来自/src/fsharp/FSharp.Core/list.fs)
// this version doesn't causes stack overflow - it uses a private stack
[<CompiledName("FoldBack")>]
let foldBack<'T,'State> f (list:'T list) (acc:'State) =
let f = OptimizedClosures.FSharpFunc<_,_,_>.Adapt(f)
match list with
| [] -> acc
| [h] -> f.Invoke(h,acc)
| [h1;h2] -> f.Invoke(h1,f.Invoke(h2,acc))
| [h1;h2;h3] -> f.Invoke(h1,f.Invoke(h2,f.Invoke(h3,acc)))
| [h1;h2;h3;h4] -> f.Invoke(h1,f.Invoke(h2,f.Invoke(h3,f.Invoke(h4,acc))))
| _ ->
// It is faster to allocate and iterate an array than to create all those
// highly nested stacks. It also means we won't get stack overflows here.
let arr = toArray list
let arrn = arr.Length
foldArraySubRight f arr 0 (arrn - 1) acc
并弃牌
[<CompiledName("Fold")>]
let fold<'T,'State> f (s:'State) (list: 'T list) =
match list with
| [] -> s
| _ ->
let f = OptimizedClosures.FSharpFunc<_,_,_>.Adapt(f)
let rec loop s xs =
match xs with
| [] -> s
| h::t -> loop (f.Invoke(s,h)) t
loop s list
从中我们可以看出两者具有相同的时间复杂度(O(n))
。因为两者都对数据执行单个循环。但是,您可以使用foldback
的方式轻松实现O(n^2)
。从代码中可以看出foldback
中有更多开销,因为创建了一个临时数组,以便以相反的顺序遍历列表。