将连续的list元素重复包装到Ocaml的子列表中

时间:2015-05-19 00:26:26

标签: ocaml

我在网站 99 ocaml 中发现了这个问题。经过一番思考后,我通过将问题分解为几个较小的子问题来解决它。这是我的代码:

let rec frequency x l=
match l with 
|[]-> 0
|h::t-> if x=[h] then 1+(frequency x t)
else frequency x t
;;

let rec expand x n=
match n with
|0->[]
|1-> x
|_-> (expand x (n-1)) @ x
;;


let rec deduct a b=
match b with 
|[]-> []
|h::t -> if a=[h] then (deduct a t)
else [h]@ (deduct a t)
;;

let rec pack l=
match l with
|[]-> []
|h::t -> [(expand [h] (frequency [h] l))]@ (pack (deduct [h] t))
;;

很明显,这个实现是过度的,因为我必须计算列表中每个元素的频率,展开它并从列表中删除相同的元素,然后重复该过程。算法复杂度约为O(N *(N + N + N))= O(N ^ 2),即使达到了所需的目的,也不适用于大型列表。我试着阅读网站上的官方解决方案,其中说:

# let pack list =
    let rec aux current acc = function
      | [] -> []    (* Can only be reached if original list is empty *)
      | [x] -> (x :: current) :: acc
      | a :: (b :: _ as t) ->
         if a = b then aux (a :: current) acc t
         else aux [] ((a :: current) :: acc) t  in
    List.rev (aux [] [] list);;
val pack : 'a list -> 'a list list = <fun>

代码应该更好,因为它更简洁并且做同样的事情。但我对内部使用“aux current acc”感到困惑。在我看来,作者在“pack”函数内部创建了一个新函数,经过一些精心设计的程序后,能够使用List.rev来反转列表,从而获得所需的结果。我不明白的是:

1)使用它有什么意义,这使得代码很难在第一眼看到?

2)在另一个需要3个输入的功能中使用累加器和辅助功能有什么好处?作者是否暗中使用了尾递归或其他什么?

3)是否有修改程序以便它可以像我的程序一样打包所有重复项?

1 个答案:

答案 0 :(得分:1)

这些问题主要是意见而不是事实。

1)在我看来,你的代码难以理解。

2a)在OCaml和其他功能语言中使用辅助功能非常常见。你应该把它想象成类似C语言的嵌套花括号而不是奇怪的东西。

2b)是的,代码使用的是尾递归,而你的代码并没有。您可以尝试为您的代码提供(例如)200,000个不同元素的列表。然后尝试与官方解决方案相同。您可以尝试确定代码可以处理的最长不同值列表,然后尝试为该长度计算两个不同的实现。

2c)为了编写尾递归函数,有时需要在结束时反转结果。这只会增加线性成本,这通常不足以引起注意。

3)我怀疑你的代码没有解决问题。如果您只想压缩相邻的元素,那么您的代码就不会这样做。如果您想使用官方解决方案执行代码所做的操作,则可以事先对列表进行排序。或者您可以使用地图或散列表来保持计数。

一般来说,官方解决方案在很多方面都比你的好得多。再一次,你要求提出意见,这是我的意见。

<强>更新

官方解决方案使用名为aux的辅助函数,该函数采用三个参数:当前累积的子列表(相同值的某些重复次数),当前累积的结果(以相反的顺序),以及剩余的输入待处理。

不变量是第一个参数(名为current)中的所有值与未处理列表的头值相同。最初这是真的,因为current为空。

该函数查看未处理列表的前两个元素。如果它们相同,则将它们中的第一个添加到current的开头,并继续列表的尾部(除了第一个之外)。如果它们不同,则希望开始在current中累积不同的值。它通过将当前(添加到前面的一个额外值)添加到累积结果来执行此操作,然后继续使用空值为当前处理尾部。请注意,这两个都保持不变。