【问题标题】:Basics of recursion in PythonPython中的递归基础
【发布时间】:2015-07-24 17:21:56
【问题描述】:

“编写一个递归函数“listSum”,它接受一个整数列表并返回列表中所有整数的总和。

例子:

>>> listSum([1, 3, 4, 5, 6])
19

我知道如何以另一种方式做到这一点,但不是以递归方式。

def listSum(ls):
    i = 0
    s = 0
    while i < len(ls):
        s = s + ls[i]
        i = i + 1
    print(s)

我需要基本的方法来做到这一点,因为不允许使用特殊的内置函数。

【问题讨论】:

    标签: python list recursion


    【解决方案1】:

    每当遇到这样的问题时,尽量用同一个函数来表达函数的结果。

    在您的情况下,您可以通过将第一个数字与调用列表中其余元素的相同函数的结果相加来获得结果。

    例如,

    listSum([1, 3, 4, 5, 6]) = 1 + listSum([3, 4, 5, 6])
                             = 1 + (3 + listSum([4, 5, 6]))
                             = 1 + (3 + (4 + listSum([5, 6])))
                             = 1 + (3 + (4 + (5 + listSum([6]))))
                             = 1 + (3 + (4 + (5 + (6 + listSum([])))))
    

    现在,listSum([]) 的结果应该是什么?它应该是 0。这就是递归的基本条件。当满足基本条件时,递归将结束。现在,让我们尝试实现它。

    这里的主要内容是拆分列表。您可以使用slicing 来执行此操作。

    简单版

    >>> def listSum(ls):
    ...     # Base condition
    ...     if not ls:
    ...         return 0
    ...
    ...     # First element + result of calling `listsum` with rest of the elements
    ...     return ls[0] + listSum(ls[1:])
    >>> 
    >>> listSum([1, 3, 4, 5, 6])
    19
    

    尾调用递归

    一旦你理解了上述递归的工作原理,你就可以试着让它变得更好一点。现在,要找到实际结果,我们也依赖于前一个函数的值。 return 语句在递归调用返回结果之前不能立即返回值。我们可以通过将电流传递给函数参数来避免这种情况,就像这样

    >>> def listSum(ls, result):
    ...     if not ls:
    ...         return result
    ...     return listSum(ls[1:], result + ls[0])
    ... 
    >>> listSum([1, 3, 4, 5, 6], 0)
    19
    

    在这里,我们将总和的初始值传递给参数,在listSum([1, 3, 4, 5, 6], 0) 中为零。然后,当满足基本条件时,我们实际上是在 result 参数中累加和,所以我们返回它。现在,最后一个return 语句有listSum(ls[1:], result + ls[0]),我们将第一个元素添加到当前result 并再次将其传递给递归调用。

    这可能是了解Tail Call 的好时机。它与 Python 无关,因为它不进行尾调用优化。


    传递索引版本

    现在,您可能会认为我们正在创建如此多的中间列表。我可以避免吗?

    当然可以。您只需要接下来要处理的项目的索引。但是现在,基础条件会有所不同。由于我们要传递索引,我们将如何确定整个列表是如何处理的?好吧,如果索引等于列表的长度,那么我们已经处理了其中的所有元素。

    >>> def listSum(ls, index, result):
    ...     # Base condition
    ...     if index == len(ls):
    ...         return result
    ...
    ...     # Call with next index and add the current element to result
    ...     return listSum(ls, index + 1, result + ls[index])
    ... 
    >>> listSum([1, 3, 4, 5, 6], 0, 0)
    19
    

    内部函数版本

    如果您现在查看函数定义,您正在向它传递三个参数。假设您要将此函数作为 API 发布。当用户真正找到一个列表的总和时,传递三个值会不会很方便?

    不。我们对于它可以做些什么呢?我们可以创建另一个函数,它是实际 listSum 函数的本地函数,我们可以将所有与实现相关的参数传递给它,就像这样

    >>> def listSum(ls):
    ...
    ...     def recursion(index, result):
    ...         if index == len(ls):
    ...             return result
    ...         return recursion(index + 1, result + ls[index])
    ...
    ...     return recursion(0, 0)
    ... 
    >>> listSum([1, 3, 4, 5, 6])
    19
    

    现在,当调用listSum 时,它只返回recursion 内部函数的返回值,该函数接受indexresult 参数。现在我们只传递这些值,而不是listSum 的用户。他们只需要传递要处理的列表即可。

    在这种情况下,如果您观察参数,我们不会将ls 传递给recursion,而是在其中使用它。由于闭包属性,ls 可以在 recursion 内部访问。


    默认参数版本

    现在,如果你想保持简单,不创建内部函数,你可以使用默认参数,像这样

    >>> def listSum(ls, index=0, result=0):
    ...     # Base condition
    ...     if index == len(ls):
    ...         return result
    ...
    ...     # Call with next index and add the current element to result
    ...     return listSum(ls, index + 1, result + ls[index])
    ... 
    >>> listSum([1, 3, 4, 5, 6])
    19
    

    现在,如果调用者没有明确传递任何值,那么0 将被分配给indexresult


    递归幂问题

    现在,让我们将这些想法应用到不同的问题上。例如,让我们尝试实现power(base, exponent) 函数。它将返回 base 的值,以 exponent 的幂次方。

    power(2, 5) = 32
    power(5, 2) = 25
    power(3, 4) = 81
    

    现在,我们如何递归地做到这一点?让我们尝试了解这些结果是如何实现的。

    power(2, 5) = 2 * 2 * 2 * 2 * 2 = 32
    power(5, 2) = 5 * 5             = 25
    power(3, 4) = 3 * 3 * 3 * 3     = 81
    

    嗯,所以我们明白了。 base 乘以自身,exponent 乘以得到结果。好的,我们如何处理它。让我们尝试用相同的函数定义解。

    power(2, 5) = 2 * power(2, 4)
                = 2 * (2 * power(2, 3))
                = 2 * (2 * (2 * power(2, 2)))
                = 2 * (2 * (2 * (2 * power(2, 1))))
    

    如果任何事物的幂次方为 1,结果应该是什么?结果将是相同的数字,对吗?我们得到了递归的基本条件:-)

                = 2 * (2 * (2 * (2 * 2)))
                = 2 * (2 * (2 * 4))
                = 2 * (2 * 8)
                = 2 * 16
                = 32
    

    好的,让我们实现它。

    >>> def power(base, exponent):
    ...     # Base condition, if `exponent` is lesser than or equal to 1, return `base`
    ...     if exponent <= 1:
    ...         return base
    ...
    ...     return base * power(base, exponent - 1)
    ... 
    >>> power(2, 5)
    32
    >>> power(5, 2)
    25
    >>> power(3, 4)
    81
    

    好的,如何定义 Tail call 的优化版本呢?让我们将当前结果作为参数传递给函数本身,并在满足基本条件时返回结果。让我们保持简单,直接使用默认参数的方法。

    >>> def power(base, exponent, result=1):
    ...     # Since we start with `1`, base condition would be exponent reaching 0
    ...     if exponent <= 0:
    ...         return result
    ...
    ...     return power(base, exponent - 1, result * base)
    ... 
    >>> power(2, 5)
    32
    >>> power(5, 2)
    25
    >>> power(3, 4)
    81
    

    现在,我们在每个递归调用中减少exponent 的值,并使用base 减少多个result,并将其传递给递归power 调用。我们从值1 开始,因为我们正在反向处理问题。递归会这样发生

    power(2, 5, 1) = power(2, 4, 1 * 2)
                   = power(2, 4, 2)
                   = power(2, 3, 2 * 2)
                   = power(2, 3, 4)
                   = power(2, 2, 4 * 2)
                   = power(2, 2, 8)
                   = power(2, 1, 8 * 2)
                   = power(2, 1, 16)
                   = power(2, 0, 16 * 2)
                   = power(2, 0, 32)
    

    由于exponent 变为零,因此满足基本条件并返回result,因此我们得到32 :-)

    【讨论】:

    • 谢谢,这就是我要找的!但是为什么我需要 return ls[0] 部分?我不能只放 listSum(ls[0:])
    • @SebastianS ls[0:] 等价于ls(嗯,准确地说是ls 复制)。因此,listSum 将始终使用相同的参数调用,结果您将达到递归限制 [它将无限运行]。
    • 如果您查看显示的示例部分listSum([1, 3, 4, 5, 6]) = 1 + listSum([3, 4, 5, 6]),这里的1 是什么?列表的第一个元素传递给listSum。这就是我们为什么要做ls[0][3, 4, 5, 6] 是什么?来自ls 的其余元素(不包括第一个元素),这就是我们这样做的原因listSum(ls[1:])
    • 您可以考虑将index 和/或result 默认设置为0。它消除了对内部函数的需要。
    • 嗨@thefourtheye 非常感谢您详尽的回答。我对更简单的版本有两个问题,下面的if not ls: ... return 0 是什么意思。我为什么要这样做return ls[0] + listSum(ls[1:]) 而不是return listSum(ls[0:])。我试过后者,但它没有成功。非常感谢您的回答
    【解决方案2】:

    提前退出是递归函数的典型特征。 seq 为空时是假的(因此当没有数字可以求和时)。

    切片语法允许将序列传递给递归调用的函数,而不会在当前步骤中消耗整数。

    def listSum(seq):
        if not seq:
            return 0
        return seq[0] + listSum(seq[1:])
    
    print listSum([1,3,4,5,6])  # prints 19
    

    【讨论】:

    【解决方案3】:
    def listSum(L):
        """Returns a sum of integers for a list containing
        integers.
        input: list of integers
        output: listSum returns a sum of all the integers
        in L.
        """
        if L == []:
            return []
        if len(L) == 1:
            return L[0]
        else:
            return L[0] + listSum(L[1:])
    print listSum([1, 3, 4, 5, 6])
    print listSum([])
    print listSum([8])
    

    【讨论】:

      【解决方案4】:

      另一个版本:

      def listSum(ls):
          ls_len = len(ls)
      
          # Base condition
          if ls_len==1:
              return ls[0]
          if ls_len==0:
              return None
          # ls = listSum(ls[0:i]) + listSum(ls[i:])
          elif ls_len%2==0:
                  i = int(ls_len/2)
                  return listSum(ls[0:i]) + listSum(ls[i:])
          else:
              i = int((ls_len-1)/2)
              return listSum(ls[0:i]) + listSum(ls[i:])
      

      按照@thefourtheye 的例子,我们可以说:

      listSum([1, 3, 4, 5, 6]) = listSum([1, 3]) + listSum([4, 5, 6])
                               = (listSum([1]) + listSum([3])) + (listSum([4]) + listSum([5, 6]))
                               = (listSum([1]) + listSum([3])) + (listSum([4]) + (listSum([5]) + listSum([6])))
      

      基本条件:当ls只有一个元素时,返回这个值。

      【讨论】:

      • 为什么这个替代版本比公认的答案更好?它更复杂,这是一个很大的缺点。这个分而治之的版本预计会运行得更快吗?
      • @msm1089 你很高兴看到这个方法是divide and conquer 版本!我提供的不是更快的版本,而是更好的版本来解释这个算法的本质。在我看来,Divide and conquer 是最重要的算法之一。
      【解决方案5】:
      def listsum(list):
          if len(list) == 1:
              return list[0]
          else:
              return list[0] + listsum(list[1:])
      
      print(listsum([1,5,9,10,20]))
      

      这个递归函数背后的基本思想是我们想要检查我们是否有一个显示为if len(list) == 1:的基本情况。对于基本情况,我们只返回列表return list[0] 中的值,否则,列表中仍然有多个元素。在else: 语句中,我们将列表中的第一个元素list[0] 添加到列表中的其余元素中。这可以通过递归调用函数来显示,其中列表短了1 个元素——该元素位于index 0-- listsum(list[1:]),这个过程会随着列表变小而重复,直到你到达基本情况——长度为 1 的列表,然后你将得到最终结果。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多