Python 为什么在退出递归调用时保留一些变量或列表,而在退出递归调用时不保留其他变量或列表?
我试图理解当您退出一个列表时,Python中的变量会发生什么变化。我认为传递给递归函数的所有变量都被推到堆栈上。当您退出堆栈时,这些变量会弹出。然而,我发现情况并非如此 我创建了一个简单的递归字符串函数来测试这一点。当我运行此代码时:Python 为什么在退出递归调用时保留一些变量或列表,而在退出递归调用时不保留其他变量或列表?,python,algorithm,list,recursion,data-structures,Python,Algorithm,List,Recursion,Data Structures,我试图理解当您退出一个列表时,Python中的变量会发生什么变化。我认为传递给递归函数的所有变量都被推到堆栈上。当您退出堆栈时,这些变量会弹出。然而,我发现情况并非如此 我创建了一个简单的递归字符串函数来测试这一点。当我运行此代码时: def recursiveArr(n, listB): if(n==0): return True; else: listB.append(n); listC = listB; re
def recursiveArr(n, listB):
if(n==0):
return True;
else:
listB.append(n);
listC = listB;
recursiveArr(n-1, listB[:]);
print(n)
print(listB)
我得到以下输出:
1
[4, 3, 2, 1]
2
[4, 3, 2]
3
[4, 3]
4
[4]
[4]
1
[4, 3, 2, 1]
[4, 3, 2, 1]
2
[4, 3, 2, 1]
[4, 3, 2, 1]
[4, 3, 2, 1]
[4, 3, 2, 1]
4
[4, 3, 2, 1]
[4, 3, 2, 1]
[4, 3, 2, 1]
这表明列表和变量n
都保留在堆栈上。然后,我修改了对递归arr(n-1,listB)
我得到了以下输出:
1
[4, 3, 2, 1]
2
[4, 3, 2]
3
[4, 3]
4
[4]
[4]
1
[4, 3, 2, 1]
[4, 3, 2, 1]
2
[4, 3, 2, 1]
[4, 3, 2, 1]
[4, 3, 2, 1]
[4, 3, 2, 1]
4
[4, 3, 2, 1]
[4, 3, 2, 1]
[4, 3, 2, 1]
从这两次运行中,我得出结论,简单整数变量总是被保留的,而列表只有在传入列表及其所有值的副本时才会被保留。这里是棘手的地方
我写了一些代码来打印字符串的排列。它可以工作,但我必须递减级别
,弹出结果数组的最后一个实例,并递减相应的计数值,以便在递归调用后获得变量,即使我只传入结果或计数数组的值
代码如下:
def stringify(theList):
theString = ''
return theString.join(theList)
def getPerm(charArray, countArray, result, level, lenOrigString):
if(level == lenOrigString):
print(stringify(result))
return
for i in range(len(charArray)):
if (countArray[i]!=0):
result.append(charArray[i])
countArray[i] = countArray[i]-1;
level = level + 1;
getPerm(charArray, countArray, result, level, lenOrigString)
result.pop(len(result)-1)
countArray[i] = countArray[i]+1
level = level - 1;
#print("Congratulations! You made it to the second layer!")
def printPermutations(toPerm):
toPerm = toPerm.lower()
lenOrigString = len(toPerm)
charArray = [ord(iChar)-97 for iChar in toPerm]
countArray = [];
for iChar in range(26):
countArray.append(0)
for i in range(len(charArray)):
countArray[charArray[i]] = countArray[charArray[i]] + 1
uniqueChar = [];
for iChar in charArray:
if iChar not in uniqueChar:
uniqueChar.append(iChar)
uniqueChar = [chr(iChar+97) for iChar in uniqueChar] #keeps track of the possible characters to use
countArray = list(filter(lambda x: x>0, countArray))#keep track of how many of each character has not been used
uniqueChar.sort()
result = [];
level = 0;
for i in range(len(uniqueChar)):
if (countArray[i]!=0):
result.append(uniqueChar[i])
countArray[i] = countArray[i]-1;
level = level + 1;
getPerm(uniqueChar, countArray, result, level, lenOrigString)#call next level
result.pop(len(result)-1)
countArray[i] = countArray[i]+1
level = level - 1;
printPermutations("ApPLe")
上面发布的代码运行良好。但是,当我删除调用后数组修改并尝试使用切片获得相同的效果时,我得到的结果比原始字符串长,使它们不是排列
下面是修改后的代码的样子:
for i in range(len(uniqueChar)):
if (countArray[i]!=0):
result.append(uniqueChar[i])
countArray[i] = countArray[i]-1;
level = level + 1;
getPerm(uniqueChar, countArray[:], result[:], level, lenOrigString)#call next level
level = level -1;
我的问题是:为什么我在基本字符串函数中找到的模式在字符串置换函数中不成立?
我马上就要面试了,我不想因为对递归退出呼叫感到困惑而让自己难堪
顺便说一句,我曾经激发过字符串排列代码。当你执行
listB[:]
时,它将创建该列表的副本。因此,在您的函数中,它不会从上面的一个堆栈中修改原始的listB
。当您仅将listB
作为参数传递时,您将引用传递给原始列表,因此每个修改的堆栈中的列表都是相同的
对于listB[:]
您的递归函数如下所示:
递归arr(4,[]
|
|追加(4)->listB现在是[4]
|
+--->recursiveArr(3,[4])这里提供的示例使您难以遵循您正在讨论的递归模式,但您对引用的直觉基本上是正确的 通常,当使用列表作为递归函数的参数遍历这样的调用树时,有两种方法。第一个是传入列表的副本,您的第一个示例对此进行了说明(使用语法
[:]
,这将生成整个列表的副本)
第二种方法是从不复制列表,并允许中的函数在整个遍历过程中引用相同的列表。这种方法需要在每次调用解析后恢复列表状态。我们必须这样做,以通过防止子节点的突变修改父状态来保留副本的逻辑
下面是您提供的示例函数的一个快速重写,用于坚持并删除无用的行(listC=listB;
):
输出:
n:1,lst:[4,3,2,1],id:14043907012752
n:2,lst:[4,3,2],id:140439070012832
编号:3,lst:[4,3],id:140439139990576
编号:4,lst:[4],id:140439072829760
请注意,打印发生在递归调用开始解析之后,因此我们首先在最终调用打印中看到一个完全填充的列表。如果这令人困惑,请重新安排函数以首先打印--位置无关。重要的是每个列表都有一个完全不同的id,所以我们总共创建了4个列表
现在,这里有一个等效的版本(就输出而言),它在整个过程中只使用一个列表,并使用上述“状态恢复”技术:
def bar(n, lst):
if n:
lst.append(n)
bar(n - 1, lst)
print(f"n: {n}, lst: {lst}, id: {id(lst)}")
lst.pop() # undo the append to restore state in this frame
if __name__ == "__main__":
bar(4, [])
输出:
n:1,lst:[4,3,2,1],id:139793013186800
n:2,lst:[4,3,2],id:139793013186800
编号:3,lst:[4,3],id:139793013186800
编号:4,lst:[4],id:139793013186800
注意一些事情。首先,逻辑输出是相同的。说服自己,每个append
都伴随着一个pop
,它完全恢复父调用的状态,撤消当前堆栈帧中执行的所有突变。其次,请注意,我们始终有一个列表,id为139793013186800
请记住,在递归调用堆栈中,父调用一直保持到所有子调用完全解析为止,因此我们只需要担心当前帧的状态
现在我们已经看到了理论,让我们看看置换方法的两个版本:
def print_permute_copy(lst, i=0):
if i == len(lst):
print("".join(lst))
else:
for j in range(i, len(lst)):
cpy = lst[:]
cpy[i], cpy[j] = cpy[j], cpy[i]
print_permute_copy(cpy, i + 1)
def print_permute_restore(lst, i=0):
if i == len(lst):
print("".join(lst))
else:
for j in range(i, len(lst)):
lst[i], lst[j] = lst[j], lst[i]
print_permute_restore(lst, i + 1)
lst[i], lst[j] = lst[j], lst[i]
if __name__ == "__main__":
print_permute_copy(list("abc"))
print()
print_permute_restore(list("abc"))
输出:
abc
acb
美国银行
bca
中国篮球协会
驾驶室
abc
acb
美国银行
bca
中国篮球协会
驾驶室
我们可以看出两者都是正确的,并产生了同等的输出。top函数创建列表的新副本以传递给子级。通过这样做,它不必担心从子级返回列表后恢复其状态。这种方法的缺点是每次调用都需要创建一个新的列表,这是低效的
另一方面,恢复版本只是传递一个列表,并对其执行所有交换。执行交换后,它会将变异列表传递给其子级,后者也会对其执行交换,但一旦其子级对列表进行操作(并最终撤消其交换),每个子级都会撤消其执行的任何交换
您显示的代码的状态要复杂得多,但让我们检查一下这个代码段:
for i in range(len(uniqueChar)):
if (countArray[i]!=0):
result.append(uniqueChar[i])
countArray[i] = countArray[i]-1;
level = level + 1;
getPerm(uniqueChar, countArray[:], result[:], level, lenOrigString)
level = level -1;
尝试使用切片失败,因为不是在副本上执行,而是在父列表上执行。与result.append(uniqueChar[i])
相同——这应该在我们准备复制的副本上执行
for i in range(len(uniqueChar)):
if countArray[i] != 0:
cpy = countArray[:]
cpy[i] = cpy[i] - 1;
level = level + 1;
getPerm(uniqueChar, cpy, result + [uniqueChar[i]], level, lenOrigString)
level = level -1;
countArray = [];
for iChar in range(26):
counts = []
for i in range(26):
def permute(lst, i=0):
if i == len(lst):
yield lst[:]
else:
for j in range(i, len(lst)):
lst[i], lst[j] = lst[j], lst[i]
yield from permute(lst, i + 1)
lst[i], lst[j] = lst[j], lst[i]
if __name__ == "__main__":
print(list(permute(list("abc"))))