我编写了以下递归函数,但是由于最大递归深度而导致运行时错误。我想知道是否可以编写一个迭代函数来克服这个问题:
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
答案 0 :(得分:12)
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”。我将使用 curry 这些功能来消除该问题。 这一步很棘手。确保您了解它!
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))
您是说after
是对f
的呼叫的 continuation (紧随其后的 )。通常,我们通过将有关调用者代码中位置的信息放入“调用堆栈”中来实现延续。在消除递归中,我们所做的只是将连续性信息从调用堆栈移到堆栈数据结构中。但是信息完全一样。
这里要意识到的重要一点是,我们通常将调用堆栈视为“过去让我来到这里的事情是什么?”。 那完全是倒退。调用堆栈会告诉您此调用结束后您必须做什么!!这就是我们在显式堆栈中编码的信息。在我们“展开堆栈”时,我们在任何地方都没有编码之前做过的事情,因为我们不需要该信息。
正如我在最初的评论中所说:总有一种方法可以将递归算法转换为迭代算法,但这并不总是那么容易。我在这里向您展示了如何做到这一点:仔细重构递归方法,直到极其简单。通过重构将其简化为 single 递归。然后,直到那时,才应用此转换将其转换为显式堆栈形式。 实践,直到您对此程序转换感到满意为止。然后,您可以继续使用更高级的技术来删除递归。
请注意,当然,这几乎肯定不是解决此问题的“ pythonic”方法。您可能会使用延迟评估的列表理解来构建一种更紧凑,更易理解的方法。此答案旨在回答所提出的特定问题:通常,我们如何将递归方法转换为迭代方法?
我在评论中提到,删除递归的标准技术是将显式列表构建为堆栈。这说明了该技术。还有其他技术:尾巴递归,连续通过样式和蹦床。这个答案已经太长了,因此我将在后续答案中介绍。
答案 1 :(得分:4)
阅读完我的第一个答案后,请阅读此答案。
同样,在这种情况下,我们通常回答“如何将递归算法转换为迭代算法”的问题。如前所述,这是关于探索转换程序的总体思路;这不是解决特定问题的“ 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”调用堆栈,然后在有了它之后,耗尽整个堆栈。我们本质上是在模拟运行时已经完成的工作:构造一堆“继续”,说明下一步该做什么。
另一种技术是让函数自身决定如何对其进行延续;这就是所谓的“持续传球风格”。让我们来探索它。
这一次,我们将向递归方法c
添加参数f
。 c
是一个函数,该函数通常使用f
的返回值,并且执行在调用f
之后可能发生的任何事情。也就是说,它明确是f
的 continuation 。然后方法f
变为“无效返回”。
基本情况很容易。如果我们处于基本情况下该怎么办?我们用将要返回的值来调用延续:
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)
我们完成了。
所以...很重要。该函数仍然是递归的。为什么这很有趣? 因为该函数现在为尾递归!也就是说,它在非基本情况下所做的 last 就是调用自身。考虑一个愚蠢的情况:
def tailcall(x, sum):
if x <= 0:
return sum
return tailcall(x - 1, sum + x)
如果我们调用tailcall(10, 0)
,它将调用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已创建。因此,这是无效的,但是我们可以通过创建一个在每次调用时引入 new 变量的作用域来轻松修复它:
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)
我们完成了!我们已经将这种递归算法变成了迭代算法。
或者...有吗?
在继续阅读之前,请仔细考虑以下事项。您的蜘蛛意识应该告诉您这里有问题。
我们开始遇到的问题是递归算法正在使栈崩溃。我们已经将其转换为迭代算法-此处根本没有递归调用!我们只是坐在循环中更新局部变量。
问题是-在基本情况下,调用 final 延续时会发生什么?这种延续是做什么的?好吧,它先调用其 after ,然后再调用其延续。这种延续是做什么的?一样。
我们在这里所做的所有工作都是将递归控制流移到我们迭代构建的函数对象的集合中,然后调用该东西仍在进行中炸掉烟囱。因此,我们尚未真正解决问题。
或者...有吗?
我们在这里可以做的是再增加一个间接级别,这将解决问题。 (这解决了计算机编程中的每一个问题,除了一个问题;您知道那个问题是什么吗?)
我们要做的是更改f
的合同,以使它不再是“我将返回空位,并在完成后称呼我的延续”。我们将其更改为“我将返回一个函数,该函数在被调用时将调用我的延续。此外,我的延续将执行相同的操作。”
这听起来有些棘手,但实际上并非如此。再次,让我们进行推理。基本情况需要做什么?它必须返回一个函数,该函数在调用时调用我的延续。但是我的延续已经满足了这个要求:
def f(n, c):
if is_base_case(n):
return c(base_case_value)
递归情况如何?我们需要返回一个 function ,该函数在被调用时将执行递归。 that 调用的延续必须是一个带有值的函数,并且返回一个函数,该函数在被调用时将执行该值的延续。我们知道该怎么做:
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))
现在,我们处于基本情况下,因此我们*称为延续,用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
的新值。它不是None,所以我们调用它。然后调用
print(min_distance(1))
打印出正确答案,print
返回None
,循环停止。
注意那里发生了什么。 堆栈的深度永远不会超过两层,因为每次调用都会返回一个函数,该函数说出循环旁边的操作,而不是调用
如果听起来很熟悉,应该这样做。基本上,我们在这里要做的是使工作队列非常简单。每次我们“排队”工作时,该工作都会立即出队,而工作要做的唯一事情是通过将lambda返回到蹦床中来排队下一个工作,从而将其粘贴在“排队”中,变量t
。
我们将问题分解成小块,并使每一块负责说明下一部分。
现在,您会注意到我们以任意深度的嵌套lambdas 结尾,就像我们在先前的技术中以任意深度的队列结束一样。本质上,我们在这里所做的是将工作流程描述从一个显式列表移到嵌套的lambda网络中。,但是与以前不同,这次我们做了一些技巧,避免了这些lambda每次都调用其他以增加堆叠深度的方式。
一旦您看到这种“分解成碎片并描述协调碎片执行的工作流程”的模式,您就会开始在各处看到它。 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)
并且“递归”方法现在将仅使用非常小的固定数量的堆栈。
答案 2 :(得分:2)
首先,您需要找到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
将它们粘在一起:
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)