【问题标题】:Transforming recursion into tail recursion?将递归转换为尾递归?
【发布时间】:2013-03-07 22:59:25
【问题描述】:

我正在尝试编写一个谓词,以递归方式找到某个数字的 n 次方 [A^n = A * A^(n-1)] 并使用快捷方式 A^(2n) = A^n * A^n。

这是目前的解决方案。

p(_,0,1):-!.
p(A,N,R):-N mod 2=0,!,N1=N/2,p(A,N1,R1),R=R1*R1.
p(A,N,R):-N1=N-1,p(A,N1,R1),R=R1*A.

现在我想让这个尾部递归。我可以为简单的情况做tail,例如没有捷径的阶乘和幂(通过添加一个累加器),但这很难。

非常感谢任何帮助!

【问题讨论】:

  • 我认为这是不可能的。等着看有没有人知道方法...
  • @CapelliC 是否可以在没有累加器的情况下以尾递归方式进行阶乘和幂运算?
  • 最初的问题不清楚:你的意思是你根本不想使用累加器,还是你想不出一种方法来合并你的快捷方式(A^2N = A ^N*A^N) 变成可以有累加器的尾递归解?
  • @Boris 后者。我知道我必须使用累加器进行尾递归,只是不知道如何合并快捷方式。
  • 如果你有对数算法,通常把代码转换成TR形式就不太紧急。最大递归堆栈深度不太可能被破坏(防止堆栈溢出的可能性可能是试图完全避免线性递归的唯一/主要原因)。

标签: prolog exponentiation logic-programming


【解决方案1】:

看来还是有可能的,从另一端开始吧:

pow(A,N,R) :-
    pow(A,N,A,1,R).

pow(_,N,R,N,R) :- !.
pow(A,N,Acc,M,R) :-
    M =< N div 2, !,
    M1 is M*2,
    NewAcc is Acc * Acc,
    pow(A,N,NewAcc,M1,R).
pow(A,N,Acc,M,R) :-
    M < N,
    M1 is M+1,
    NewAcc is A * Acc,
    pow(A,N,NewAcc,M1,R).

它将快捷方式应用到小于 N 的 2 的最大幂,这与您的算法所做的不一样。

【讨论】:

  • 毕竟是一样的,即使是相同的乘法顺序,只是你从A开始而不是A * 1。想一想:原始算法中的乘法以调用的相反顺序发生,这意味着结果是按顺序获取的,例如 N=5: 1, 1*A, (1*A)*( 1*A)、((1*A)*(1*A)*(1*A)*(1*A))、((1*A)*(1*A)*(1*A)* (1*A))*A。完全相同,在您的算法中,您从 A 开始,然后尽可能频繁地将其平方并仅在最后一次迭代时与 A 相乘。
  • @MikeHartl:你可能是对的,我不确定。总之,在可能具有高度优化的内置幂函数的语言实现中,这种算法在某种程度上是不必要的。
  • @Boris 谢谢!在我看来也一样。这只是一个练习,我可能永远不会在现实世界的应用程序中使用自己编写的数学算法。
  • @Boris:但是这个算法在除实数之外的其他代数中可能仍然有用。通过平方取幂可以很容易地适应产生字符串、树和其他结构。
  • 尝试A^7: OP: A*( (A*(A^2))^2) (4 mults);这个答案:((A^2)^2)*A*A*A (5 mults) ; A^15:15-14-7-6-3-2 与 2-4-8-9-10-11-12-13-14-15。所以OP算法最多执行2*log2(n)乘法,但是这个算法执行~ n/2乘法。 不是改进。但是您可以使用带有 log(n) 步骤的 TR 算法:通过减半,将结果收集到列表中,然后返回查看列表以执行与 OP 算法完全相同的步骤(= Mike 的回答) )。或者,在此处计算时收集所有能量,然后将它们“背包”到:2-4-8-12-14-15。
【解决方案2】:

Boris 是对的,他的算法所做的与原来的不同。但是,如果您真的想复制它,可以使用以下方法:

请注意,您可以从数字的二进制表示中确定操作的顺序。设N=7,然后二进制N=111,记为N=7~111

现在您可以在原始算法中看到该方案:

N      Op     N'
7~111  Mul    6~110 (= zero last bit) 
6~110  Squ    3~011 (= shift right)
3~011  Mul    2~010 
2~010  Squ    1~001
1~001  Base

考虑到由于算法的递归性质,这些步骤是从上到下进行的,你得到Base - Squ - Mul - Squ - Mul = ((A*A)*A)*((A*A)*A))*A = A**7

将此与鲍里斯的算法进行对比:

N      Op     N'
1~001  Squ    2~010 (=shift left)
2~010  Squ    4~100 (=shift left)
4~100  Mul    5~101 (=add one)
5~101  Mul    6~110 (=add one)
6~110  Mul    7~111 (=add one)

所以这个先做所有的移位,而原来的考虑除了 N 中的第一个之外的每个位,从右到左依次“排队”(因为自下而上)Mul, Squ 如果该位被设置或者只是 Squ 如果未设置。

要重现这种行为(更有效,因为你永远不会做比平方更简单的乘法),你可以从二进制的N 开始,然后执行以下操作(这里是一般伪代码,你可以很容易地翻译成序言):

Acc=A
for i in (reverse(tail(bits(N)))):
    Acc*=Acc
    if i==1:
       Acc*=A

这是给N&gt;=1的。 N=0 是特例,必须分开处理。

我很确定这是正确的。如果您有疑问,请考虑您的原始算法:测试mod 2 == 0 与测试最后一位是否为零相同。如果不是,则减一与将最后一位归零相同,而加倍和减半只是二进制左移或右移。

【讨论】:

  • 感谢您提供这些宝贵的见解!
猜你喜欢
  • 1970-01-01
  • 2013-09-27
  • 1970-01-01
  • 2023-01-13
  • 2019-09-14
  • 2016-01-06
  • 1970-01-01
  • 1970-01-01
  • 2011-06-30
相关资源
最近更新 更多