在Scheme中创建用于记忆的关联列表

时间:2018-04-13 03:57:11

标签: functional-programming scheme racket

我试图使用关联列表在Scheme中记住factorial函数。但是,我无法让备忘录过程正常工作。我已经通过

初始化了全局空关联列表
(define al '()) ; association list

接下来,我在Scheme中定义了阶乘函数:

(define (fac n)
 (cond ((<= n 1) 1)
 (else (* n (fac (- n 1))))))

然后我把记忆过程分成了三个部分;绑定函数,查找函数和因子存储函数。

lookup函数检查关联列表是否已包含特定阶乘操作的值。代码如下:

(define (lookup k al)  ;; lookup function
  (cond
    ((null? al) #f)
    ((equal? k (caar al)) (cadr (car al))) 
    (else (lookup k (cdr al)))))

如果因子函数的值不在关联列表中,bind函数将启动:

(define (bind k v al)  ;; bind function
    (cond 
        ((null? al) (set! al (list (list k v))))
        (else (set! al (cons (list k v) al)))) v)

最后,lookup函数中使用了bindfac_mem函数:

(define (fac_mem n)  ;; fac_mem function
  (cond
    ((equal? (lookup n al) #f) (set! al (bind n (fac n) al)))
    (else (begin
            (display "memoization hit \n")
            (lookup n al)))))

上述目的是让fac_mem函数取一个值n,检查n的阶乘是否已存在于关联列表中{{1} (通过先前计算)通过al函数,如果关联列表中不存在,则计算lookup的阶乘,并通过{{1}将其插入关联列表中功能。如果关联列表中存在n的阶乘,则程序将仅通过关联列表返回值,而不是再次计算它。

但是,每当我运行bind时,程序似乎都会提示第二个参数,就好像函数是curry一样。我不确定为什么这不起作用。

2 个答案:

答案 0 :(得分:2)

我为了不同的目的写了这个。你可能会发现它很有用。原始版本(附加注释)可用here

让我们考虑一个简单的程序来计算 n th 斐波纳契数。在数学上,定义是 n th 斐波那契数是 n -1 th 斐波那契数的总和和 n -2 th 斐波纳契数,前两个斐波纳契数是1和1.这直接转化为这样的方案:

(define (fib n)
  (if (< n 2) 1
    (+ (fib (- n 1)) (fib (- n 2)))))

计算时间显示随着 n 变大,它会迅速变慢,这是有道理的,因为算法需要指数时间来重复重新计算所有较小的斐波那契数:

> (time (fib 40))
(time (fib 40))
    no collections
    15069 ms elapsed cpu time
    15064 ms elapsed real time
    0 bytes allocated
165580141

15秒是很长一段时间来计算一个小数字。

很容易编写一个Scheme宏来记忆或缓存斐波纳契计算中固有的子问题的结果。这是宏:

(define-syntax define-memoized
  (syntax-rules ()
    ((define-memoized (f arg ...) body ...)
      (define f
        (let ((cache (list)))
          (lambda (arg ...)
            (cond ((assoc `(,arg ...) cache) => cdr)
            (else (let ((val (begin body ...)))
                    (set! cache (cons (cons `(,arg ...) val) cache))
                    val)))))))))

我们马上解释一下。但首先让我们看看如何使用该宏编写斐波纳契函数:

(define-memoized (fib n)
  (if (< n 2) 1
    (+ (fib (- n 1)) (fib (- n 2)))))

当然,这与早期的fibonacci函数完全相同,只是我们使用define-memoized而不是简单的define来编写函数。但请看一下备忘录的不同之处:

> (time (fib 40))
(time (fib 40))
    no collections
    0 ms elapsed cpu time
    0 ms elapsed real time
    5456 bytes allocated
165580141

我们已经从没有做任何工作从十五秒变为零,这是惊人的!即使计算像(fib 4000)这样的数字也不会造成任何创伤:

> (time (fib 4000))
(time (fib 4000))
    no collections
    141 ms elapsed cpu time
    144 ms elapsed real time
    1364296 bytes allocated
64574884490948173531376949015369595644413900640151342708407577598177210359034088
91444947780728724174376074152378381889749922700974218315248201906276355079874370
42751068564702163075936230573885067767672020696704775060888952943005092911660239
47866841763853953813982281703936665369922709095308006821399524780721049955829191
40702994362208777929645917401261014865952038117045259114133194933608057714170864
57836066360819419152173551158109939739457834939838445927496726613615480616157565
95818944317619922097369917676974058206341892088144549337974422952140132621568340
70101627342272782776272615306630309305298205175744474242803310752241946621965578
04131017595052316172225782924860810023912187851892996757577669202694023487336446
62725774717740924068828300186439425921761082545463164628807702653752619616157324
434040342057336683279284098590801501

它是如何工作的?高级解释是宏修改fib以在缓存内部存储具有相同参数的先前函数调用的结果,并直接返回它们而不是重新计算它们。因此,当(fib 40)需要(fib 39)(fib 38)的结果时,结果已经可用,无需重新计算。 缓存数据结构在Scheme中称为a-list(关联列表),这意味着它是键/值对的链接列表,其中键是 n 值为(fib n)。函数assoc缓存中查找键,`(,arg ...)是一个准引号,它扩展了函数的参数(fib只接受一个参数,但是宏允许多个函数占用多个)。 =>符号是将cond谓词的结果传递给其结果的语法,cdr是从键/值对中提取值的函数。 else的{​​{1}}子句计算cond表达式中前所未见的值,然后使用let更新缓存并返回新计算的值。

答案 1 :(得分:1)

你没有说你的程序“似乎”以什么方式提示争论,所以我无法解决这个问题(我什么也没得到)。
我会解决其他一些问题。

Scheme标准没有指定set!是否返回有意义的值,所以如果你依赖它就会发生奇怪的事情。

首先,请注意(list (list k v))(cons (list k v) '())相同,因此bind的两个分支都做同样的事情。

我个人更喜欢成对而不是列表,因为每个键只有一个值。

一个主要问题是bind修改了其参数al,该变量与全局al不是同一个变量。
这意味着您永远不会在表格中找到任何内容。

> (bind 12 "hello" al)
"hello"
> (lookup 12)
#f
> al
'()

(在开始将每个函数放在一起之前,先测试每个函数是一个非常好的主意。)

您需要set!全局:

(define (bind k v)
  (set! al (cons (cons k v) al))
  v)

您可以更改lookup以便在您使用全局时使用全局:

(define (lookup k)
  (cond
    ((null? al) #f)
    ((equal? k (car (car al))) (cdr (car al))) 
    (else (lookup k (cdr al)))))

> (bind 12 "hello")
"hello"
> (lookup 12)
"hello"
> al
'((12 . "hello"))

fac_mem中,bind已经更新了表格,因此您无需再次执行此操作。 使用let来避免重复查找,fac_mem变为:

(define (fac_mem n)
    (let ((memo (lookup n)))
          (if memo
              memo
              (bind n (fac n)))))

甚至

(define (fac_mem n)
  (or (lookup n)
      (bind n (fac n))))

or返回第一个不是#f的参数。)

现在你有另一个问题:

> (fac_mem 5)
120
> al
'((5 . 120))

备忘录实际上并没有太多记忆。

将此左侧固定为练习。