Python 接下来的问题是递归函数,它解决了在游戏中获得一定点数的所有可能方法
原始问题(已经解决):假设一个简化的足球计分系统,你可以得到{2,3,7}分,那么在给定分数的情况下,所有可能的得分方式是什么 链接到该问题 我正试图了解这些递归函数在下面的代码中维护score和results变量的方式 代码解决方案(由ggorlen编写): 这很好地解决了我最初的问题。但是,在阅读了编程面试元素(Python)中的一些问题之后,我决定尝试一下传递结果和分数列表的方式。我模仿了作者的一种技术,每次递归调用函数时不传递结果列表,而是为空列表指定一个默认值:Python 接下来的问题是递归函数,它解决了在游戏中获得一定点数的所有可能方法,python,recursion,Python,Recursion,原始问题(已经解决):假设一个简化的足球计分系统,你可以得到{2,3,7}分,那么在给定分数的情况下,所有可能的得分方式是什么 链接到该问题 我正试图了解这些递归函数在下面的代码中维护score和results变量的方式 代码解决方案(由ggorlen编写): 这很好地解决了我最初的问题。但是,在阅读了编程面试元素(Python)中的一些问题之后,我决定尝试一下传递结果和分数列表的方式。我模仿了作者的一种技术,每次递归调用函数时不传递结果列表,而是为空列表指定一个默认值: def find_sc
def find_scoring2(points, ways_to_score=[2, 3, 7]):
def score_finder(points, scores, result = []):
if points == 0:
result.append(scores[:])
elif points > 0:
for val in ways_to_score:
scores.append(val)
score_finder(points - val, scores)
scores.pop()
return result
return score_finder(points, [])
这产生了相同的结果,这个想法来自EPI第235页(生成平衡括号)
然后,我决定改变分数变量的创建方式,以消除从列表中弹出的需要。我不理解代码,但我还是从同一本书中复制了这个想法
def find_scoring3(points, ways_to_score=[2, 3, 7]):
def score_finder(points, scores, result = []):
if points == 0:
result.append(scores[:])
elif points > 0:
for val in ways_to_score:
#scores.append(val)
score_finder(points - val, scores + [val])
#scores.pop()
return result
return score_finder(points, [])
因此,我的问题是:
1.我如何在每次都将results变量设置为空列表的情况下,仍然产生正确的解决方案?是否有一些视频或参考资料可以帮助我了解这是如何工作的?
2.为什么当我从追加更改为仅向列表中添加值时,分数变量的行为会发生变化?第二和第三个版本会意外地工作基于: 发生了什么事?嗯,当定义了
foo
时,就创建了bar=[]
。未来的调用不会像预期的那样将bar
设置为新的空列表,而是将bar
分配给原始列表的别名,该别名会不断累积42
s
这与直觉相悖,因为我们通常认为这样的函数是无状态的,工作方式如下:
>>> def foo(bar=""):
... bar += "42"
... return bar
...
>>> foo()
'42'
>>> foo()
'42'
>>> foo()
'42'
现在让我们移动foo
到baz
的内部函数:
>>> def baz():
... def foo(bar=[]):
... bar.append(42)
... return bar
... return foo()
...
>>> baz()
[42]
>>> baz()
[42]
>>> baz()
[42]
事实证明,在这种情况下,foo
是在每次调用baz
时从零开始创建的,同时还有它的bar=[]
列表。当baz
返回时,foo
被销毁,因为它只是baz
的堆栈帧中的局部变量,以及bar
为了让这一点更加明显,这里有一个实验:
>>> def foo(bar=print("bar defined")): pass
...
bar defined
>>> foo()
>>> foo()
>>> foo()
指纹应该很清楚发生了什么。但如果不是,这里有一个递归版本,与您的函数类似,它通过在默认参数上“滥用”别名跨堆栈帧构建结果列表:
>>> def foo(i=4, bar=[]):
... if i:
... bar.append(i)
... return foo(i - 1)
... return bar
...
>>> foo()
[4, 3, 2, 1]
但是它失败了,比如说,JS:
> const foo = (i=4, bar=[]) => {
... if (i) {
..... bar.push(i);
..... return foo(i - 1);
..... }
... return bar;
... };
undefined
> foo();
[]
和Ruby:
irb(main):023:0> def foo(i=4, bar=[])
irb(main):024:1> if i > 0
irb(main):025:2> bar.push(i)
irb(main):026:2> return foo(i - 1)
irb(main):027:2> end
irb(main):028:1> return bar
irb(main):029:1> end
=> :foo
irb(main):030:0> foo
=> []
其中,每次调用foo()
时,如果缺少bar
参数,则会将一个新数组(列表)分配给bar
。我可以展示其他语言示例,但我相信Python在将对象作为默认参数处理方面非常独特
考虑到这一切,“正确”的递归调用应该是score\u finder(points-val,scores,result)
(类似于return foo(i-1,bar)
——如果需要,可以在JS/Ruby版本上进行尝试),它将在函数调用一次后填充result
的默认参数
这就解释了我编写原始代码的动机——我只是完全避免了默认的def-foo(bar=[])
模式,从而跳过了所有的奇怪之处(不过对于def-foo(bar=42)
这样的原语也没问题)
至于你的第二个问题,
scores+[val]
看起来更干净,因为没有append
/pop
/[:]
,但它实际上更慢,内存更密集。与其创建一个通过引用传递给调用堆栈的列表(并且仅当结果准备好时才从开始复制到结束),find\u scoring3
为每个递归调用帧创建一个全新的列表(大量额外工作)。这也意味着find_scoring3
正在进行不必要的复制(scores[:]
——尝试删除该片段)
Python的一个缺点是它提供了许多易于使用的O(n)列表操作,如
[:]
,中的,列表+[]
,如果使用不当,可能会增加时间复杂度。谢谢,先生。非常有启发性的东西。你有教授编程的博客或youtube频道吗?没有,就在这里(虽然学的比教的多)。很高兴看到你深入阅读这本书并提出有趣的问题!另外,我忘了回答你的第二个问题--请参阅更新。
>>> def foo(i=4, bar=[]):
... if i:
... bar.append(i)
... return foo(i - 1)
... return bar
...
>>> foo()
[4, 3, 2, 1]
> const foo = (i=4, bar=[]) => {
... if (i) {
..... bar.push(i);
..... return foo(i - 1);
..... }
... return bar;
... };
undefined
> foo();
[]
irb(main):023:0> def foo(i=4, bar=[])
irb(main):024:1> if i > 0
irb(main):025:2> bar.push(i)
irb(main):026:2> return foo(i - 1)
irb(main):027:2> end
irb(main):028:1> return bar
irb(main):029:1> end
=> :foo
irb(main):030:0> foo
=> []