Performance 在F中高效地投影列表#

Performance 在F中高效地投影列表#,performance,list,f#,Performance,List,F#,我必须做列表的投影,它返回每个列表中每个元素的所有组合。例如: projection([[1]; [2; 3]]) = [[1; 2]; [1; 3]]. projection([[1]; [2; 3]; [4; 5]]) = [[1; 2; 4]; [1; 2; 5]; [1; 3; 4]; [1; 3; 5]]. 我提出了一个函数: let projection lss0 = let rec projectionUtil lss accs = match lss w

我必须做列表的投影,它返回每个列表中每个元素的所有组合。例如:

projection([[1]; [2; 3]]) = [[1; 2]; [1; 3]].
projection([[1]; [2; 3]; [4; 5]]) = [[1; 2; 4]; [1; 2; 5]; [1; 3; 4]; [1; 3; 5]].
我提出了一个函数:

let projection lss0 =
    let rec projectionUtil lss accs =
        match lss with
        | []        ->  accs
        | ls::lss'  ->  projectionUtil lss' (List.fold (fun accs' l -> 
                                                        accs' @ List.map (fun acc -> acc @ [l]) accs) 
                                                        [] ls)
match lss0 with
| [] -> []
| ls::lss' ->         
    projectionUtil lss' (List.map (fun l -> [l]) ls)
和一个测试用例:

#time "on";;
let N = 10
let fss0 = List.init N (fun i -> List.init (i+1) (fun j -> j+i*i+i));;
let fss1 = projection fss0;;

该功能现在相当慢,使用
N=10
需要10秒以上的时间才能完成。此外,我认为解决方案是不自然的,因为我必须以两种不同的方式分解同一个列表有什么建议可以提高函数的性能和可读性吗?

由于@(即List concat)操作,您的实现很慢,这是一个很慢的操作,并且以递归的方式进行了多次。@变慢的原因是,在函数式编程中,列表是链表,而对于concat2列表,您必须先到列表的末尾(逐个遍历元素),然后再附加另一个列表


请查看评论中建议的参考资料。我希望这些可以帮助您。

首先,尽可能避免列表串联(@),因为它是O(N)而不是O(1)前缀

我将从一个(相对)容易遵循的计划开始,该计划涉及如何计算列表的笛卡尔外积

  • 将第一个列表的每个元素前置到剩余列表的笛卡尔乘积中的每个子列表
  • 注意基本情况
第一版:

let rec cartesian = function
  | [] -> [[]]
  | L::Ls -> [for C in cartesian Ls do yield! [for x in L do yield x::C]]
这是上面句子到代码的直接翻译

现在加快速度:使用列表连接和映射代替列表理解:

let rec cartesian2 = function
  | [] -> [[]]
  | L::Ls -> cartesian2 Ls |> List.collect (fun C -> L |> List.map (fun x->x::C))
通过按需通过顺序计算列表,可以更快地实现这一点:

let rec cartesian3 = function
  | [] -> Seq.singleton []
  | L::Ls -> cartesian3 Ls |> Seq.collect (fun C -> L |> Seq.map (fun x->x::C))
最后一种形式是我自己使用的,因为我通常只需要迭代结果,而不是一次获得所有结果

我的机器上的一些基准测试: 测试代码:

let test f N = 
  let fss0 = List.init N (fun i -> List.init (i+1) (fun j -> j+i*i+i))
  f fss0 |> Seq.length
FSI的结果:

> test projection 10;;
Real: 00:00:18.066, CPU: 00:00:18.062, GC gen0: 168, gen1: 157, gen2: 7
val it : int = 3628800
> test cartesian 10;;
Real: 00:00:19.822, CPU: 00:00:19.828, GC gen0: 244, gen1: 121, gen2: 3
val it : int = 3628800
> test cartesian2 10;;
Real: 00:00:09.247, CPU: 00:00:09.250, GC gen0: 94, gen1: 52, gen2: 2
val it : int = 3628800
> test cartesian3 10;;
Real: 00:00:04.254, CPU: 00:00:04.250, GC gen0: 359, gen1: 1, gen2: 0
val it : int = 3628800

此函数是Haskell的(尽管
序列
更通用)。翻译成F#:

在互动中:

> test projection 10;;
Real: 00:00:12.240, CPU: 00:00:12.807, GC gen0: 163, gen1: 155, gen2: 4
val it : int = 3628800
> test sequence 10;;
Real: 00:00:06.038, CPU: 00:00:06.021, GC gen0: 75, gen1: 74, gen2: 0
val it : int = 3628800

总体思路:避免显式递归,使用标准的组合符(折叠、映射等)

这是一个尾部递归版本。它不如其他一些解决方案快(仅比原始函数快25%),但内存使用是恒定的,因此它适用于非常大的结果集

let cartesian l = 
  let rec aux f = function
    | [] -> f (Seq.singleton [])
    | h::t -> aux (fun acc -> f (Seq.collect (fun x -> (Seq.map (fun y -> y::x) h)) acc)) t
  aux id l

可能的重复项:,,基本上,和的任何顶级搜索结果…为了比较,这里是我的笛卡尔积方案版本:您描述的是列表的笛卡尔积。看到优秀的答案,我可以看到思考的流程以及你是如何想出有效的解决方案的。我建议你也制作一个尾部递归版本。@Ankur:看看Ed'ka的答案,一个不会杀死堆栈的版本。以尾部递归的方式实现我的版本可能会涉及大量的延续和麻烦,并且不会很好地执行。+1表示折叠。不知何故,我从来没有想过向后遍历F#中的列表,因为它们是head::tail结构。但是这个版本不会对堆栈进行核攻击。
let cartesian l = 
  let rec aux f = function
    | [] -> f (Seq.singleton [])
    | h::t -> aux (fun acc -> f (Seq.collect (fun x -> (Seq.map (fun y -> y::x) h)) acc)) t
  aux id l
let crossProduct listA listB listC listD listE = 
  listA |> Seq.collect (fun a -> 
  listB |> Seq.collect (fun b -> 
  listC |> Seq.collect (fun c -> 
  listD |> Seq.collect (fun d -> 
  listE |> Seq.map (fun e -> a,b,c,d,e))