递归计算列表平均值

Recursively calculate a list average

我在OCaml中有作业要做,其中一题是计算一个列表的平均值。 我在 1 或 2 年前已经用另一种语言做到了,就像我第一次做的那样,我决定不仅对所有元素求和并除以长度。主要原因是怕浮点数溢出

所以我在维基百科上找到了我上次使用的公式:recursive average formula。

我在 OCaml 中是这样编码的:

let average = function
| []    -> raise Empty_list
| hd::l ->
    let rec aux average count = function
        | hd::l -> aux ((average*.(float (count-1))+.hd)/.(float (count))) (count+1) l
        | _     -> average
    in aux hd 1 l
;;

对我来说,这看起来像是 OCaml 中公式的精确转录。

但是它没有用,但是,在拿了一张纸,一支笔并考虑了一下之后,我设法通过替换行使其工作:

| hd::l -> aux ((average*.(float (count-1))+.hd)/.(float (count))) (count+1) l

与:

| hd::l -> aux ((average*.(float (count))+.hd)/.(float (count+1))) (count+1) l

它奏效了。

我告诉自己第二行在逻辑上是计算正确答案的好方法,但我不明白一开始哪里出了问题。我是否翻译了有偏见的公式?还是我翻译的时候遗漏了什么?

此时,它还在找我,第一行是公式的转录,第二行是计算正确答案的方法。但我相信这里有一些我无法理解的东西。有人可以帮我解释一下吗?

为什么搞得这么复杂?为什么不只计算总和和计数?

let int_avg lst =
  let rec int_avg_aux cnt sum lst =
    match lst with
    | [] -> (cnt, sum)
    | hd::tl -> int_avg_aux (cnt + 1) (hd + sum) tl in
  int_avg_aux 0 0 lst

let (c, s) = int_avg [1;2;3;4;5;]

let () = Printf.printf "%d %d\n" c s

现在你有了元素的个数和元素的总和。

我在 OCaml 中尝试了你的公式,我认为我做对了:

let avg c lst =
  let rec avg_aux c l =
  match l with
  | [] -> 0.0
  | hd::tl ->
    (((avg_aux (c -. 1.0) tl) *. (c -. 1.0)) +. hd) /. c in
  avg_aux c lst

let lst = [max_float;2.0;max_float;4.0;5.0;6.0]

let ans = avg (float(List.length lst)) lst

let () = Printf.printf "%f\n" ans

这是您要找的吗?

But I believe there's something I can't understand here

总的来说你的逻辑没有错,我认为公式本身就是混乱的根源。

很明显,在计算过程中,股息中的 (n - 1) 乘数不能变为零(否则你 "discard" 之前累积的值 - 这实际上发生在你的第一次尝试中),并且确保这一点的唯一方法是设置 n > 0。因此,第一个等式(默认情况)的索引必须为 1,而不是 0。

因此,对于基本情况,您有 n = 1,对于下一次迭代,n = 2 等。这与您的第二个(正确的)表达式匹配,而不是第一个...

平均值公式有一种更简洁的形式,它可以找到旧平均值和新观测值之间的差值,然后按样本大小缩放差值以更新平均值。基本情况是单个观察值的平均值就是该观察值。 (空列表的平均值未定义。)

在 OCaml 中:

let rec avg lst =
  match lst with
    | [x]     -> x
    | x::rest -> avg rest +. (x -. avg rest) /. float(List.length lst)
    | []      -> failwith "avg called on empty list!"
;;

递归调用应该只计算一次,因为它是纯粹的。

问题不在于公式,而在于你使用它的方式。

你打电话给aux hd 1 l。因此,您从列表头部的平均值和计数 1 开始。但是在公式中,您将前一个平均值乘以 count - 1,在第一次调用时为 0。所以你所做的就是把头扔掉。

这样写就是aux 0.0 1 (hd::tl)aux hd 2 tl.

如果您进一步允许空列表的平均值为 0.0,您甚至不需要外部函数的模式匹配。更进一步,如果你将 average 和 count 参数设为可选(分别默认为 0.0 和 1),你甚至不需要辅助函数:

let rec average ?(avg=0.0) ?(count=1) = function
| []     -> avg
| hd::tl -> average
                ~avg:((avg*.(float (count-1))+.hd)/.(float (count)))
                ~count:(count+1)
                tl;;
val average : ?avg:float -> ?count:int -> float list -> float = <fun>

# average [1.;2.;3.];;
- : float = 2.

作为参考,这里是一个不会溢出的函数版本,具有正确的时间复杂度:

let avg l =
  let mu_n' (n,mu_n) x =
    let n' = n + 1 in
    n', mu_n +. (x -. mu_n) /. float n' in
  snd (List.fold_left mu_n' (0,0.) l)

let x = avg [max_float; 1.; 2.; max_float;2.; 3.; max_float; 5.; 6.]
let relative_error = (x -. max_float /. 3.) /. (max_float /. 3.)

val relative_error : float = -1.66533453693773481e-16