[英]Turning a recursive function into an iterative function
我编写了以下递归函数,但由于最大递归深度而导致运行时错误。 我想知道是否有可能编写一个迭代函数来克服这个问题:
def finaldistance(n):
if n%2 == 0:
return 1 + finaldistance(n//2)
elif n != 1:
a = finaldistance(n-1)+1
b = distance(n)
return min(a,b)
else:
return 0
我试过的是这个,但似乎没有用,
def finaldistance(n, acc):
while n > 1:
if n%2 == 0:
(n, acc) = (n//2, acc+1)
else:
a = finaldistance(n-1, acc) + 1
b = distance(n)
if a < b:
(n, acc) = (n-1, acc+1)
else:
(n, acc) =(1, acc + distance(n))
return acc
Johnbot的解决方案向您展示如何解决您的具体问题。 我们一般可以删除这个递归吗? 让我通过制作一系列小的,清晰正确,明确安全的重构来告诉你。
首先,这是一个稍微重写的函数版本。 我希望你同意它是一样的:
def f(n):
if n % 2 == 0:
return 1 + f(n // 2)
elif n != 1:
a = f(n - 1) + 1
b = d(n)
return min(a, b)
else:
return 0
我希望基础案例是第一位的。 此功能在逻辑上是相同的:
def f(n):
if n == 1:
return 0
if n % 2 == 0:
return 1 + f(n // 2)
a = f(n - 1) + 1
b = d(n)
return min(a, b)
我希望每次递归调用后的代码都是方法调用而不是其他任何东西。 这些功能在逻辑上是相同的:
def add_one(n, x):
return 1 + x
def min_distance(n, x):
a = x + 1
b = d(n)
return min(a, b)
def f(n):
if n == 1:
return 0
if n % 2 == 0:
return add_one(n, f(n // 2))
return min_distance(n, f(n - 1))
类似地,我们添加了计算递归参数的辅助函数:
def half(n):
return n // 2
def less_one(n):
return n - 1
def f(n):
if n == 1:
return 0
if n % 2 == 0:
return add_one(n, f(half(n))
return min_distance(n, f(less_one(n))
同样,请确保您同意此程序在逻辑上是相同的。 现在我要简化参数的计算:
def get_argument(n):
return half if n % 2 == 0 else less_one
def f(n):
if n == 1:
return 0
argument = get_argument(n) # argument is a function!
if n % 2 == 0:
return add_one(n, f(argument(n)))
return min_distance(n, f(argument(n)))
现在我将在递归之后对代码执行相同的操作,并且我们将进行单个递归:
def get_after(n):
return add_one if n % 2 == 0 else min_distance
def f(n):
if n == 1:
return 0
argument = get_argument(n)
after = get_after(n) # this is also a function!
return after(n, f(argument(n)))
现在我注意到我们将n传递给get_after,然后再将其传递给“after”。 我要讨好这些功能,以消除这一问题。 这一步很棘手 。 确保你理解它!
def add_one(n):
return lambda x: x + 1
def min_distance(n):
def nested(x):
a = x + 1
b = d(n)
return min(a, b)
return nested
这些函数确实有两个参数。 现在他们接受一个参数,并返回一个带有一个参数的函数! 所以我们重构使用网站:
def get_after(n):
return add_one(n) if n % 2 == 0 else min_distance(n)
和这里:
def f(n):
if n == 1:
return 0
argument = get_argument(n)
after = get_after(n) # now this is a function of one argument, not two
return after(f(argument(n)))
类似地,我们注意到我们正在调用get_argument(n)(n)
来获取参数。 让我们简化一下:
def get_argument(n):
return half(n) if n % 2 == 0 else less_one(n)
让我们稍微更一般:
base_case_value = 0
def is_base_case(n):
return n == 1
def f(n):
if is_base_case(n):
return base_case_value
argument = get_argument(n)
after = get_after(n)
return after(f(argument))
好的,我们现在的程序非常紧凑。 逻辑已经扩展到多个功能,其中一些是咖喱,当然。 但是现在函数处于这种形式,我们可以轻松地删除递归 。 这是一个非常棘手的问题是将整个事物转变为显式堆栈:
def f(n):
# Let's make a stack of afters.
afters = [ ]
while not is_base_case(n) :
argument = get_argument(n)
after = get_after(n)
afters.append(after)
n = argument
# Now we have a stack of afters:
x = base_case_value
while len(afters) != 0:
after = afters.pop()
x = after(x)
return x
仔细研究这个实现 。 你会从中学到很多东西。 请记住,当您进行递归调用时:
after(f(something))
你说的是f
after
的继续 - 接下来的事情 。 我们通常通过将有关调用者代码中的位置的信息放到“调用堆栈”上来实现延续。 我们在删除递归时所做的只是将连续信息从调用堆栈移到堆栈数据结构上。 但信息完全相同。
在这里要认识到的重要一点是,我们通常将调用堆栈视为“过去发生的事情是什么让我来到这里?”。 这完全倒退了 。 调用堆栈告诉您在完成此调用后您必须执行的操作! 这就是我们在显式堆栈中编码的信息。 由于我们不需要那些信息,因此我们在每个步骤之前编码我们在 “展开堆栈”时编码的内容 。
正如我在最初的评论中所说: 总有一种方法可以将递归算法转换为迭代算法,但这并不总是容易的 。 我已经在这里向你展示了如何做到这一点:仔细地重构递归方法,直到它非常简单 。 通过重构将其归结为单个递归。 然后,只有这样,才应用此转换将其转换为显式堆栈形式。 练习,直到你熟悉这个程序转换 。 然后,您可以继续使用更高级的技术来删除递归。
请注意,当然这几乎肯定不是解决这个问题的“pythonic”方式; 你可以使用懒惰的评估列表推导建立一个更紧凑,易懂的方法。 这个答案旨在回答所提到的具体问题: 我们一般如何将递归方法转换为迭代方法 ?
我在评论中提到,删除递归的标准技术是将显式列表构建为堆栈。 这表明了这种技术。 还有其他技术:尾递归,延续传递风格和蹦床。 这个答案已经太久了,所以我将在后续答复中介绍这些答案。
阅读完第一个答案后,请阅读此答案。
我们再次回答了“如何将递归算法转换为迭代算法”的问题,在本例中是Python。 如前所述,这是关于探索转变计划的总体思路 ; 这不是解决具体问题的“pythonic”方式。
在我的第一个回答中,我开始将程序重写为以下形式:
def f(n):
if is_base_case(n):
return base_case_value
argument = get_argument(n)
after = get_after(n)
return after(f(argument))
然后将其转换为以下形式:
def f(n):
# Let's make a stack of afters.
afters = [ ]
while not is_base_case(n) :
argument = get_argument(n)
after = get_after(n)
afters.append(after)
n = argument
# Now we have a stack of afters:
x = base_case_value
while len(afters) != 0:
after = afters.pop()
x = after(x)
return x
这里的技术是为特定输入构造一个“after”调用的显式堆栈,然后一旦我们拥有它,就运行整个堆栈。 我们基本上模拟了运行时已经做的事情:构造一堆“延续”,说明下一步该做什么。
一种不同的技术是让函数本身决定如何处理它的延续; 这被称为“延续传递风格”。 让我们来探索吧。
这次,我们将向递归方法f
添加参数c
。 c
是一个函数,它采用通常为f
的返回值,并且在调用f
之后执行任何假设。 也就是说,它明确地是f
的延续 。 然后方法f
变为“无效返回”。
基本案例很简单。 如果我们处于基本情况,我们该怎么办? 我们用我们将返回的值来调用continuation:
def f(n, c):
if is_base_case(n):
c(base_case_value)
return
十分简单。 非基本情况怎么样? 好吧,我们在原计划中要做什么? 我们打算(1)得到参数,(2)得到“after” - 递归调用的继续,(3)做递归调用,(4)调用“after”,它的继续,和(5) )将计算值返回到f
的延续。
我们将做所有相同的事情,除了当我们执行步骤(3)时, 现在我们需要传递执行步骤4和5的延续 :
argument = get_argument(n)
after = get_after(n)
f(argument, lambda x: c(after(x)))
嘿,这太容易了! 递归调用后我们该怎么办? 好了,我们称之为after
由递归调用的返回值。 但是现在该值将被传递给递归调用的延续函数,所以它只是进入x
。 之后会发生什么? 好吧, 接下来会发生什么 ,那就是c
,所以需要调用它,我们就完成了。
我们来试试吧。 以前我们会说
print(f(100))
但现在我们必须传递f(100)
之后发生的事情。 那么,会发生什么,价值被打印出来!
f(100, print)
我们完成了。
所以......很重要。 该函数仍然是递归的。 为什么这很有趣? 因为函数现在是尾递归的 ! 也就是说,它在非基本情况下做的最后一件事就是调用自身。 考虑一个愚蠢的案例:
def tailcall(x, sum):
if x <= 0:
return sum
return tailcall(x - 1, sum + x)
如果我们调用tailcall(10, 0)
则调用tailcall(9, 10)
,调用(8, 19)
tailcall(9, 10)
(8, 19)
,依此类推。 但是任何尾递归方法我们都可以非常非常容易地重写为循环:
def tailcall(x, sum):
while True:
if x <= 0:
return sum
x = x - 1
sum = sum + x
那么我们可以用我们的一般情况做同样的事情吗?
# This is wrong!
def f(n, c):
while True:
if is_base_case(n):
c(base_case_value)
return
argument = get_argument(n)
after = get_after(n)
n = argument
c = lambda x: c(after(x))
你看到了什么问题吗? lambda在c
和after
关闭,这意味着每个lambda将使用c
和after
的当前值,而不是lambda创建时的值 。 所以这已经破了,但我们可以通过创建一个每次调用时引入新变量的范围来轻松修复它:
def continuation_factory(c, after)
return lambda x: c(after(x))
def f(n, c):
while True:
if is_base_case(n):
c(base_case_value)
return
argument = get_argument(n)
after = get_after(n)
n = argument
c = continuation_factory(c, after)
我们完成了! 我们已将此递归算法转换为迭代算法。
或者......我们有吗?
在您阅读之前,请仔细考虑这一点。 你的蜘蛛意识应该告诉你这里出了点问题。
我们开始的问题是递归算法正在吹嘘堆栈。 我们把它变成了一个迭代算法 - 这里根本就没有递归调用! 我们只是坐在循环中更新局部变量。
但问题是 - 在基本情况下调用最后一个延续时会发生什么? 这种延续是做什么的? 好吧,它调用它后 ,然后它调用它的继续。 这种延续是做什么的? 一样。
我们在这里所做的就是将递归控制流移动到我们迭代构建的函数对象集合中 ,并且调用该东西仍然会破坏堆栈。 所以我们还没有真正解决这个问题。
或者......我们有吗?
我们在这里可以做的是增加一个间接层,这将解决问题。 (这解决了计算机编程中的每个问题,除了一个问题;你知道那个问题是什么吗?)
我们要做的是我们将改变f
的契约,使其不再是“我无效回归,并且在我完成后会调用我的继续”。 我们将它改为“我将返回一个函数,当它被调用时,称之为继续。此外, 我的继续也会这样做 。”
这听起来有点棘手,但事实并非如此。 再说一次,让我们理解它。 基本情况需要做什么? 它必须返回一个函数,当被调用时,它会调用我的继续。 但我的延续已经满足了这个要求:
def f(n, c):
if is_base_case(n):
return c(base_case_value)
递归案例怎么样? 我们需要返回一个函数 ,该函数在被调用时执行递归。 该呼叫的延续需要是一个函数,它的值,并返回一个函数 ,调用时执行该值延续 。 我们知道如何做到这一点:
argument = get_argument(n)
after = get_after(n)
return lambda : f(argument, lambda x: lambda: c(after(x)))
好的,这有什么用呢? 我们现在可以将循环移动到辅助函数中:
def trampoline(f, n, c):
t = f(n, c)
while t != None:
t = t()
并称之为:
trampoline(f, 3, print)
它的神圣善良有效。
跟随这里发生的事情。 这是调用序列,缩进显示堆栈深度:
trampoline(f, 3, print)
f(3, print)
这个电话回来了什么? 它有效地返回lambda : f(2, lambda x: lambda : print(min_distance(x))
,因此这是t
的新值。
这不是None
,所以我们调用t()
,它调用:
f(2, lambda x: lambda : print(min_distance(x))
那件事做什么? 它会立即返回
lambda : f(1,
lambda x:
lambda:
(lambda x: lambda : print(min_distance(x)))(add_one(x))
这就是t
的新价值。 它不是None
,所以我们调用它。 那叫:
f(1,
lambda x:
lambda:
(lambda x: lambda : print(min_distance(x)))(add_one(x))
现在我们处于基本情况,所以我们*调用continuation,用0代替x。 它返回:
lambda: (lambda x: lambda : print(min_distance(x)))(add_one(0))
这就是t
的新价值。 它不是None
,所以我们调用它。
这会调用add_one(0)
并获得1
。 然后它在中间lambda中传递1
表示x
。 那件事回来了:
lambda : print(min_distance(1))
这就是t
的新价值。 它不是无,所以我们调用它。 那个电话
print(min_distance(1))
打印出正确的答案, print
返回None
,循环停止。
注意那里发生的事情。 堆栈永远不会超过两个深度,因为每个调用返回一个函数,该函数说明循环旁边要做什么 ,而不是调用函数。
如果这听起来很熟悉,那应该是。 基本上我们在这里做的是制作一个非常简单的工作队列。 每当我们“排队”一份工作时,它就会立即出现,而这项工作所做的唯一工作就是将一个lambda返回到蹦床,然后将其固定在“队列”即变量t
,从而将下一个工作排入队列。
我们将问题分解成小块,并让每个部分负责说出下一部分是什么。
现在,你会注意到我们最终得到了任意深度嵌套的lambdas ,正如我们最后使用任意深度队列的技术一样。 基本上我们在这里所做的是将工作流描述从显式列表移动到嵌套lambda的网络中 ,但与以前不同,这次我们做了一个小技巧,以避免那些lambdas以增加的方式相互调用堆栈深度。
一旦你看到这种“将其分解成碎片并描述协调碎片执行的工作流程”的模式,你就会开始在任何地方看到它。 这就是Windows的工作方式; 每个窗口都有一个消息队列,消息可以表示工作流的一部分。 当工作流的一部分希望说明下一部分是什么时,它会将消息发布到队列,然后运行。 这就是async await
工作原理 - 再一次,我们将工作流分解为碎片,每个await
都是一块的边界。 它是生成器如何工作,每个yield
是边界,等等。 当然他们实际上并没有像这样使用蹦床,但他们可以 。
这里要理解的关键是延续的概念。 一旦您意识到可以将continuation视为可由程序操作的对象 , 任何控制流都可以实现。 想要实现自己的try-catch? try-catch只是一个工作流程,每个步骤都有两个延续:正常延续和特殊延续。 当存在异常时,您将分支到异常延续而不是常规延续。 等等。
这里的问题又是,我们如何消除一般深度递归引起的堆栈外。 我已经证明了表单的任何递归方法
def f(n):
if is_base_case(n):
return base_case_value
argument = get_argument(n)
after = get_after(n)
return after(f(argument))
...
print(f(10))
可以改写为:
def f(n, c):
if is_base_case(n):
return c(base_case_value)
argument = get_argument(n)
after = get_after(n)
return lambda : f(argument, lambda x: lambda: c(after(x)))
...
trampoline(f, 10, print)
而且“递归”方法现在只使用非常小的固定数量的堆栈。
首先你需要找到n
所有值,幸运的是你的序列是严格下降的,只取决于下一个距离:
values = []
while n > 1:
values.append(n)
n = n // 2 if n % 2 == 0 else n - 1
接下来,您需要计算每个值的距离。 要做到这一点,我们需要从buttom开始:
values.reverse()
现在,如果我们需要它来计算下一个距离,我们可以轻松跟踪之前的距离。
distance_so_far = 0
for v in values:
if v % 2 == 0:
distance_so_far += 1
else:
distance_so_far = min(distance(v), distance_so_far + 1)
return distance_so_far
坚持下去:
def finaldistance(n):
values = []
while n > 1:
values.append(n)
n = n // 2 if n % 2 == 0 else n - 1
values.reverse()
distance_so_far = 0
for v in values:
if v % 2 == 0:
distance_so_far += 1
else:
distance_so_far = min(distance(v), distance_so_far + 1)
return distance_so_far
而现在你正在使用内存而不是堆栈。
(我不用Python编程所以这可能不是惯用的Python)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.