我是Scheme(通过Racket)和(在较小程度上)函数式编程的新手,并且可以通过变量与递归使用一些关于积累的优缺点的建议。出于本示例的目的,我正在尝试计算移动平均线。因此,对于列表'(1 2 3 4 5)
,3期移动平均值为'(1 2 2 3 4)
。这个想法是,期间之前的任何数字都不是计算的一部分,一旦我们达到集合中的期间长度,我们就会根据所选择的期间开始对列表的子集进行平均。
所以,我的第一次尝试看起来像这样:
(define (avg lst)
(cond
[(null? lst) '()]
[(/ (apply + lst) (length lst))]))
(define (make-averager period)
(let ([prev '()])
(lambda (i)
(set! prev (cons i prev))
(cond
[(< (length prev) period) i]
[else (avg (take prev period))]))))
(map (make-averager 3) '(1 2 3 4 5))
> '(1 2 2 3 4)
这很有效。我喜欢使用地图。它看起来很容易构建并且可以重构。我可以在将来看到像堂兄弟一样:
(map (make-bollinger 5) '(1 2 3 4 5))
(map (make-std-deviation 2) '(1 2 3 4 5))
等
但是,这不符合Scheme的精神(对吧?),因为我正在积累副作用。所以我把它改写成这样:
(define (moving-average l period)
(let loop ([l l] [acc '()])
(if (null? l)
l
(let* ([acc (cons (car l) acc)]
[next
(cond
[(< (length acc) period) (car acc)]
[else (avg (take acc period))])])
(cons next (loop (cdr l) acc))))))
(moving-average '(1 2 3 4 5) 3)
> '(1 2 2 3 4)
现在,这个版本乍一看更难以理解。所以我有几个问题:
是否有一种更优雅的方式来表达递归版本使用一些内置的球拍迭代结构(如for/fold
)?它是否像书面一样呈递尾递归?
有没有办法在不使用累加器变量的情况下编写第一个版本?
此类问题是否为可接受的最佳做法的较大模式的一部分,尤其是在Scheme中?
答案 0 :(得分:7)
我有点奇怪,你是在列表的第一个之前开始,但在它结束时急剧停止。也就是说,你自己拿第一个元素和前两个元素,但你不会对最后一个元素或最后两个元素做同样的事情。
这与问题的解决方案有点正交。我不认为累加器会让你的生活变得更轻松,我会在没有它的情况下编写解决方案:
#lang racket
(require rackunit)
;; given a list of numbers and a period,
;; return a list of the averages of all
;; consecutive sequences of 'period'
;; numbers taken from the list.
(define ((moving-average period) l)
(cond [(< (length l) period) empty]
[else (cons (mean (take l period))
((moving-average period) (rest l)))]))
;; compute the mean of a list of numbers
(define (mean l)
(/ (apply + l) (length l)))
(check-equal? (mean '(4 4 1)) 3)
(check-equal? ((moving-average 3) '(1 3 2 7 6)) '(2 4 5))
答案 1 :(得分:0)
嗯,作为一般规则,您希望将迭代和/或迭代的方式与迭代步骤的内容分开。你在问题中提到fold
,这指向了正确的步骤:你需要某种形式的高阶函数来处理列表遍历机制,并调用你提供的函数和窗口中的值。 / p>
我在三分钟内把它煮熟了;它在很多方面可能都是错的,但它应该给你一个想法:
;;;
;;; Traverse a list from left to right and call fn with the "windows"
;;; of the list. fn will be called like this:
;;;
;;; (fn prev cur next accum)
;;;
;;; where cur is the "current" element, prev and next are the
;;; predecessor and successor of cur, and accum either init or the
;;; accumulated result from the preceeding call to fn (like
;;; fold-left).
;;;
;;; The left-edge and right-edge arguments specify the values to use
;;; as the predecessor of the first element of the list and the
;;; successor of the last.
;;;
;;; If the list is empty, returns init.
;;;
(define (windowed-traversal fn left-end right-end init list)
(if (null? list)
init
(windowed-traversal fn
(car list)
right-end
(fn left-end
(car list)
(if (null? (cdr list))
right-end
(second list))
init)
(cdr list))))
(define (moving-average list)
(reverse!
(windowed-traversal (lambda (prev cur next list-accum)
(cons (avg (filter true? (list prev cur next)))
list-accum))
#f
#f
'()
list)))
答案 2 :(得分:0)
或者,您可以定义一个函数,将列表转换为n元素窗口,然后在窗口上平均映射。
(define (partition lst default size)
(define (iter lst len result)
(if (< len 3)
(reverse result)
(iter (rest lst)
(- len 1)
(cons (take lst 3) result))))
(iter (cons default (cons default lst))
(+ (length lst) 2)
empty))
(define (avg lst)
(cond
[(null? lst) 0]
[(/ (apply + lst) (length lst))]))
(map avg (partition (list 1 2 3 4 5) 0 3))
另请注意partition
函数是尾递归的,所以它不会占用堆栈空间 - 这是result
和reverse
调用的重点。我明确地跟踪列表的长度,以避免重复调用length
(这会导致O(N ^ 2)运行时)或者将at-least-size-3
函数混合在一起。如果您不关心尾递归,则partition
的以下变体应该有效:
(define (partition lst default size)
(define (iter lst len)
(if (< len 3)
empty
(cons (take lst 3)
(iter (rest lst)
(- len 1)))))
(iter (cons default (cons default lst))
(+ (length lst) 2)))
最终评论 - 如果您没有明确检查,则使用&#39;()作为空列表的默认值可能会很危险。如果你的数字大于0,0(或-1)可能会更好地作为默认值 - 他们不会杀死使用该值的任何代码,但很容易检查并且不会出现作为合法平均值