【问题标题】:Recursion puzzle with key in the box盒子里有钥匙的递归谜题
【发布时间】:2022-01-15 09:05:59
【问题描述】:

我目前正在尝试了解组成示例的递归。想象一下,你有一个公文包,可以用钥匙打开。钥匙在大盒子里,大盒子里可以放其他小盒子,钥匙可能在里面。

在我的示例中,框是列表。当我们找到较小的框时,就会出现递归——我们搜索它的键。问题是我的函数可以找到钥匙,如果它真的在盒子里,如果没有像“钥匙”这样的东西就不能回去。

不幸的是,如果小盒子里没有钥匙,我无法理解如何返回。你能帮我解决这个难题吗?顺便说一句,祝你有美好的一天!以下是代码(大框包含了找到并返回密钥的方式):

box = ['socks', 'papers', ['jewelry', 'flashlight', 'key'], 'dishes', 'souvernirs', 'posters']

def look_for_key(box):
    for item in box:
        if isinstance(item, list) == True:
            look_for_key(item)
        elif item == 'key':
            print('found the key')
    key = item
    return key

print(look_for_key(box))

【问题讨论】:

  • 确保从递归调用中返回值:return look_for_key(item)
  • 没关系:prnt.sc/22mv16s
  • @AbbeGijly 你不想只返回递归调用的值而不先检查它是否是None。否则,您可能会错过在其他地方找到该项目的机会。
  • 您也可以只返回TrueFalse,因为该函数被硬编码以查找值'key';调用者知道会找到什么项目(如果有的话)。

标签: python recursion


【解决方案1】:

迭代

我能找到的最接近你但可读的解决方案是:

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']

为了明确起见,我使用 treesub_treeleft_branchright_branch 作为变量名,而不是 boxinner_box 等,如您的示例所示。请注意函数look_for_key 是如何映射到treesub_tree 的每个left_branchright_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)正确组合你的结果,这里使用+,然后递归调用将处理它并帮助返回到顶层。

原生递归和 ma​​p-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:]) )。当它穿越 在完整的子树(或分支)上并到达该子树中的最后一个离开(返回值或 [] 时的基本条件),它开始返回上层 - 山谷 在曲线中。如果您有多个子/巢列表,则会有多个山谷。最终遍历整棵树并返回结果

您可以玩不同巢结构的盒子(或树),以更好地了解它的工作原理。希望这些可以为您提供足够的信息和对树递归的更全面的理解。

【讨论】:

  • 非常感谢您的绝对惊人的回答。老实说,我没想到会在我的帖子中出现类似的内容。由于我的技能以及我不是以英语为母语的事实,我需要一段时间才能理解上述所有内容,但我会尽力而为。再次感谢,祝你圣诞假期愉快,当然如果你有的话;)
【解决方案2】:

整合上述cmets:

box = ['socks', 'papers', ['jewelry', 'flashlight', 'key'], 'dishes', 'souvernirs', 'posters']

def look_for_key(box):
    for item in box:
        if isinstance(item, list) == True:
            in_box = look_for_key(item)
            if in_box is not None:
                return in_box
        elif item == 'key':
            print('found the key')
            return item
    # not found
    return None

print(look_for_key(box))

哪个打印:

found the key
key

如果从框中删除密钥,则执行代码打印:

None

【讨论】:

  • 这不是一个糟糕的解决方案。但是,由于在基本条件之一中使用了赋值,并且有条件地返回了递归调用的结果,因此阅读和解释有点困难。这种复杂性破坏了递归解决方案的简单性和美观性。也许你可以简化它?此外,此解决方案不处理多个键 box = ['a','key','c', ['e', ['f','key']]] 的情况。例如,请参阅其他解决方案。
  • 谢谢 - 我正在寻找一种解决方案,对您的原件进行最少的编辑以使其运行
猜你喜欢
  • 1970-01-01
  • 2021-06-09
  • 2019-01-15
  • 1970-01-01
  • 2015-12-28
  • 2019-06-05
  • 1970-01-01
  • 1970-01-01
  • 2023-03-09
相关资源
最近更新 更多