Python实现数据结构与算法——递归小结

关于递归

什么是递归?

一句话解释就是函数调用函数自身。

递归的典型算法结构

如果当前问题只是一个简单问题——解决它。
如果不是——将其分为可以使用相同策略的子问题,直到分解为可以解决的简单问题。这个过程就是函数调用自身的过程。

解题思路(什么时候用递归)

  1. 找出问题与子问题之间的递推关系
  2. 定义递归函数,至少包括两点内容:
    (1)Base case:可解决的简单问题的解,这是分解问题的临界点
    (2)Recursive case:步骤1中的递推关系。这个递推关系的实现一定包含了函数的自我调用。

补充说明:递归并非问题的最优解,可以根据时间复杂度和空间复杂度分析决定是否选用递归解答。

递归和迭代有啥不一样?

递归和迭代在分析问题的思路上可能会有相似的地方,但是迭代算法是基于显示循环构造的。

递归中的堆栈:

因为递归函数中一定包含了函数的自我调用,这就意味着,在实际解题过程中,递归函数将持续重复调用自身直到满足某些条件(比如达到简单问题)才能返回结果。
所以在这个持续自我调用过程中,每次调用,都会将当前函数作为一个对象保存在堆栈空间中,一直到到达Basic case。到达Basic case后,开始依次返回结果,同时释放堆栈空间。在这个过程中,堆栈的LIFO的特点很好的发挥作用。

对象的状态保持

在递归调用过程中,每次调用,函数都有其自身的上下文。所以,如果需要使某一种对象贯穿整个递归过程,有两种方法:
(1)将该对象作为递归函数的参数
(2)将该对象定义为全局变量。

举例说明:累加计算1-10,当前数current_number和累加值accumulated_sum需要贯穿整个递归过程。

将该对象作为递归函数的参数

#方法1,将sum作为递归函数的参数
def sum_recursive(current_number, accumulated_sum):
    # Base case
    # Return the final state
    if current_number == 11:
        return accumulated_sum

    # Recursive case
    else:
        return sum_recursive(current_number + 1, accumulated_sum + current_number)
>>> sum_recursive(1, 0)
55

将该对象定义为全局变量。

#方法2:全局变量
current_number = 1
accumulated_sum = 0

def sum_recursive():
    global current_number
    global accumulated_sum
    # Base case
    if current_number == 11:
        return accumulated_sum
    # Recursive case
    else:
        accumulated_sum = accumulated_sum + current_number
        current_number = current_number + 1
        return sum_recursive()

两种方法的运行结果都是:

>>> sum_recursive()
55

利用记忆化改进递归

在递归过程中,可能会出现重复计算的问题,可以使用记忆化的方式来避免。
来看一个例子:Fibonacci数

def fibonacci_recursive(n):
    print("Calculating F", "(", n, ")", sep="", end=", ")

    # Base case
    if n == 0:
        return 0
    elif n == 1:
        return 1

    # Recursive case
    else:
        return fibonacci_recursive(n-1) + fibonacci_recursive(n-2)

res = fibonacci_recursive(5)
print("\n----")
print(res)

运行结果:

Calculating F(5), Calculating F(4), Calculating F(3), Calculating F(2), Calculating F(1), Calculating F(0), Calculating F(1), Calculating F(2), Calculating F(1), Calculating F(0), Calculating F(3), Calculating F(2), Calculating F(1), Calculating F(0), Calculating F(1),
----

5

从运行结果中可以看到,递归函数确实存在重复计算的问题。

辅助哈希表实现记忆化

加入辅助记忆化的哈希表可以解决重复计算的问题:

def fib(n):
    cache = {}
    def fibonacci_recursive(n):
        if n in cache:
            return cache[n]
        print("Calculating F", "(", n, ")", sep="", end=", ")

        if n < 2:
            result = n
        else:
            result = fibonacci_recursive(n-1) + fibonacci_recursive(n-2)

        # put result in cache for later reference.
        cache[n] = result
        return result

    return fibonacci_recursive(n)

res = fib(5)
print("\n----")
print(res)

运行结果:

Calculating F(5), Calculating F(4), Calculating F(3), Calculating F(2), Calculating F(1), Calculating F(0),
----
5

从结果看,每一个n只计算了一次。所以,这种方法实际上就是牺牲了多余的内存空间,来达到提高运算的目的。

lru_cache实现记忆化

除了上述使用哈希表,类似的,可以使用装饰器来达到相同效果,这里介绍一个很好用的装饰器:lru_cache。
lru_cache是一个为函数提供缓存功能的装饰器,在下次以相同参数调用时直接返回上一次的结果。如果 maxsize 设置为 None ,LRU功能将被禁用且缓存数量无上限。
代码如下:

from functools import lru_cache

@lru_cache(maxsize=None)
def fibonacci_recursive(n):
    print("Calculating F", "(", n, ")", sep="", end=", ")

    # Base case
    if n == 0:
        return 0
    elif n == 1:
        return 1

    # Recursive case
    else:
        return fibonacci_recursive(n-1) + fibonacci_recursive(n-2)

res = fibonacci_recursive(5)
print("\n----")
print(res)

运行结果:

Calculating F(5), Calculating F(4), Calculating F(3), Calculating F(2), Calculating F(1), Calculating F(0),
----
5

注意事项

递归中的数据结构通常都为可递归的可变数据结构。比如list,所以在递归过程中如果频繁操作list,如进行切片操作,则会造成每次切片都会有一个新的变量产生,极大的浪费资源。

递归练习

以下是递归的一些练习题,有原题地址,部分练习题我在以前的文章里发过,点击链接均可以查看。这些题都可以尝试使用递归来进行解题,但并不代表递归一定是最优的选择。练习的时候可以尝试其他解题思路。

题目博客链接
反转字符串链接
两两交换链表中的节点链接
反转链表链接
斐波那契数链接
二叉树的最大深度链接
Pow(x,n)链接
合并两个有序链表链接
第k个语法符号链接
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值