迭代
我能找到的最接近你但可读的解决方案是:
def look_for_key(box):
for item in box:
if item == 'key':
return item
elif isinstance(item, list) and look_for_key(item) is not None:
return look_for_key(item)
else:
pass
box = [['sock','papers'],['jewelry','key']]
look_for_key(box)
# ==> 'key'
我不喜欢它,因为它的推导条件包括一个难以解释的递归调用。如果将look_for_key(item) 分配给一个变量并在之后检查not None,这无助于提高可解释性。它同样难以解释。一个等效但更易于解释的解决方案是:
def look_for_key(box):
def inner(item, remained):
if item == [] and remained == []:
return None
elif isinstance(item, list) and item != []:
return inner(item[0], [item[1:], remained])
elif item == [] or item != 'key':
return inner(remained[0], remained[1:])
elif item == 'key':
return item
return inner(box[0], box[1:])
box = [['sock','papers'],['jewelry','key']]
look_for_key(box)
# ==> 'key'
它使用return inner(item[0], [item[1:], remained]) 和return inner(remained[0], remained[1:]) 显式地将树拆分为分支(见下文),而不是在推论期间有条件地重用递归调用 - if look_for_key(item) is not None: return look_for_key(item) - 使用这行代码很难看到一张图,了解递归的方向。
第二个解决方案还可以更轻松地使用树形图推断复杂性,因为您可以明确地看到分支,例如 remained[0] 与 remained[1:]。
由于inner 只是以函数方式编写的迭代,而for 循环是形成迭代的语法糖,因此两种解决方案原则上应该具有相似的复杂性。
由于您不仅想要一个解决方案,还想要更好地理解递归,我会尝试以下方法。
树上的映射(Map-Reduce)
这是一个典型的教科书树递归问题。您想要的是遍历称为树的分层数据结构。一个典型的解决方案是在树上映射一个函数:
from functools import reduce
def look_for_key(tree):
def look_inner(sub_tree):
if isinstance(sub_tree, list):
return look_for_key(sub_tree)
elif sub_tree == 'key':
return [sub_tree]
else:
return []
return reduce(lambda left_branch, right_branch: look_inner(left_branch) + look_inner(right_branch), tree, [])
box = ['socks', 'papers', ['jewelry', 'flashlight', 'key'], 'dishes', 'souvernirs', 'posters']
look_for_key(box)
# ==> ['key']
为了明确起见,我使用 tree、sub_tree、left_branch、right_branch 作为变量名,而不是 box、inner_box 等,如您的示例所示。请注意函数look_for_key 是如何映射到tree 中sub_tree 的每个left_branch 和right_branch 上的。然后使用reduce(经典的map-reduce 过程)对结果进行汇总。
为了更清楚,您可以省略reduce 部分,只保留map 部分:
def look_for_key(tree):
def look_inner(sub_tree):
if isinstance(sub_tree, list):
return look_for_key(sub_tree)
elif sub_tree == 'key':
return sub_tree
else:
return None
return list(map(look_inner, tree))
look_for_key(box)
# ==> [None, None, [None, None, 'key'], None, None, None]
这不会生成您想要的结果格式。但它有助于理解递归是如何工作的。 map 只是添加了一个抽象层,用于递归查找子树中的键,相当于 python 提供的 for 循环的语法糖。那不重要。最重要的是正确分解树(演绎)并设置适当的基础条件以返回结果。
原生树递归
如果还不够清楚,你可以去掉所有的抽象和语法糖,从头开始构建一个原生递归:
def look_for_key(box):
if box == []:
return []
elif not isinstance(box, list) and box == 'key':
print('found the key')
return [box]
elif not isinstance(box, list) and box != 'key':
return []
else:
return look_for_key(box[0]) + look_for_key(box[1:])
look_for_key(box)
# ==> found the key
# ==> ['key']
这里是递归的所有三个基本元素:
显式显示。从这个例子中你也可以清楚地看到,走出内箱(或子树)并没有奇迹。要查看盒子(或树)内的每个可能角落,您只需在每个较小的盒子(或子树)中重复地将其拆分为两个部分。然后你在每个级别(so called fold or reduce or accumulate)正确组合你的结果,这里使用+,然后递归调用将处理它并帮助返回到顶层。
原生递归和 map-reduce 方法都能够找出多个键,因为它们遍历整个树并累积所有匹配项:
box = ['a','key','c', ['e', ['f','key']]]
look_for_key(box)
# ==> found the key
# ==> found the key
# ==> ['key', 'key']
递归可视化
最后,要完全了解树递归的情况,您可以绘制递归深度并可视化调用如何移动到更深层次然后返回。
import functools
import matplotlib.pyplot as plt
# ignore the error of unhashable data type
def ignore_unhashable(func):
uncached = func.__wrapped__
attributes = functools.WRAPPER_ASSIGNMENTS + ('cache_info', 'cache_clear')
@functools.wraps(func, assigned=attributes)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except TypeError as error:
if 'unhashable type' in str(error):
return uncached(*args, **kwargs)
raise
wrapper.__uncached__ = uncached
return wrapper
# rewrite the native recursion and cache the recursive calls
@ignore_unhashable
@functools.lru_cache(None)
def look_for_key(box):
global depth, depths
depth += 1
depths.append(depth)
result = ([] if box == [] else
[box] if not isinstance(box, list) and box == 'key' else
[] if not isinstance(box, list) and box != 'key' else
look_for_key(box[0]) + look_for_key(box[1:]))
depth -= 1
return result
# function to plot recursion depth
def plot_depths(f, *args, show=slice(None), **kwargs):
"""Plot the call depths for a cached recursive function"""
global depth, depths
depth, depths = 0, []
f.cache_clear()
f(*args, **kwargs)
plt.figure(figsize=(12, 6))
plt.xlabel('Recursive Call Number'); plt.ylabel('Recursion Depth')
X, Y = range(1, len(depths) + 1), depths
plt.plot(X[show], Y[show], '.-')
plt.grid(True); plt.gca().invert_yaxis()
box = ['socks', 'papers', ['jewelry', 'flashlight', 'key'], 'dishes', 'souvernirs']
plot_depths(look_for_key, box)
每当函数被递归调用时,曲线就会进入更深的层次 - 向下斜线。当树/子树被拆分为左右分支时,两个调用发生在同一级别 - 连接两个点的水平线(两个调用look_for_key(box[0]) + look_for_key(box[1:]) )。当它穿越
在完整的子树(或分支)上并到达该子树中的最后一个离开(返回值或 [] 时的基本条件),它开始返回上层 - 山谷 在曲线中。如果您有多个子/巢列表,则会有多个山谷。最终遍历整棵树并返回结果
您可以玩不同巢结构的盒子(或树),以更好地了解它的工作原理。希望这些可以为您提供足够的信息和对树递归的更全面的理解。