Python 递归 深入理解递归 Python递归剖析,绝对让你看懂!

递归剖析

  • 递归真的很重要,之前学的时候,学的一知半解,以为真正了解,每次想到递归,就记得一句:返回给函数的调用者,嗯?函数调用者,你是说外部,还是内部啊?疑问太多了,还有就是被告知一句:递归能解决的问题,循环都能解决,所以就更加不重视递归了!直到接触算法后,在解决问题时,最快,最容易理解的解法就是递归,但是此时的递归却是看不太懂为什么要这样做!我先来说下,在算法中遇到可以用递归轻松完成的:希尔排序、归并排序、快速排序、反转链表及各种反转问题、二叉树的深度遍历、二叉树的各种题基本可以用、全排列…还有算法步骤里需要用到,太多了,所以,递归非常重要!“To iterate is human, to recurse divine 迭代是人,递归是神”,接下来,我也是看了很多算法书和视频,重点来整理下递归!
递归的两个过程
  • 首先“递归”包括两个过程:递“去”的过程,“归”回的过程!先从一个简单的递归函数讲起
def di_gui(n):
    print(n, "<===1====>")
    if n > 0:
        di_gui(n - 1)
    print(n, '<===2====>')


di_gui(5) # 外部调用后打印的结果是?
  • 递归的执行过程:首先,递归会执行“去”的过程,只需要满足终止条件,就会一直在函数内,带着更新的参数,调用函数自身,注意:到内部调用函数, 以下面的代码不会被执行,而是暂停阻塞;此时 随着函数每调用一次自身,还没有触发 返回值和到达终止条件,等同于在原来的基础上不断“向下/向内”开辟新的内存空间,记住,每次调用一次函数,就不是处在同一空间(想象下《盗梦空间》里的情景,梦中梦,都是处于不同的空间)
    在这里插入图片描述

  • 什么时候“递”去的过程结束?记住有两种情况>>> 一是:当前这层空间函数全部执行结束(终止条件),二是:执行到了return 返回值,直接返回

  • 重点来理解下,首先是一,看上面的列子,例子中没有return,但是不断的调用后,最终还是停止了,因为最后n=0时,di_gi(0)还是去调用,从上往下执行时,遇到if n>0 它被终止了,走不下去了,表明,自己能到达的这层空间已经全部执行完毕;接下来请原地返回吧,返回到哪里?返回到函数的调用者,好我们返回到 di_gui(0),把“到内部调用函数” 以下的代码全部执行完;执行完,看代码走到末尾,以为走出了最外层函数?注意了,此时它所处的空间并不是最外层哦,因为之前它被调用就在空间里面,所以回到的是 di_gui(1)的这一层空间,现在才是真正的开始“回”,所以继续把di_gui(1)的这一层空间,“到内部调用函数”以下的代码全部执行完,回到di_gui(2)的这一层空间…直到到达最开始 从外部调用,让参数5进入的最外层空间位置,才走出来!表示《盗梦空间》里,柯布醒了!
    在这里插入图片描述
    回来看下,代码输出的结果:5 4 3 2 1 00 1 2 3 4 5 ( 注意00这两个是在同一层空间哦)
    在这里插入图片描述

  • 从内存角度(本质)来分析:每调用一次函数,都会单独开辟一份栈帧空间,递归函数就是不停的开辟和释放栈帧空间的过程,具体来理解下:一个普通函数从执行到结束,就是一个开辟空间和释放空间的过程;而递归函数是在调用最外层函数时,先开辟一个最外层空间,每调用一次自身,就会在最外层空间内,再自己开辟本次的空间(所以递归耗内存)(还有一种说法是,不断的本次空间的基础上再开辟空间,等于是不断的嵌套,其实这两种说法本质上是一样的,因为信息都可以做到不共享),空间之间如果不通过参数传递或者用return 返回值,信息是不共享的,如下图↓↓↓

在这里插入图片描述
在这里插入图片描述

  • 递归的数据共享情况:递归每一层间的数据是独立的,不共享,但是可以通过参数或者返回值来形成共享,参数具体在传入的是“引用类型”(比如列表)

  • 递归 必须要有一个出口,如果没有出口就会重复调用,不断的开辟栈帧空间

# 获取最大递归层数:
import sys
res=sys.getrecursionlimit()
print(res) # 结果:1000
# sys.setrecursionlimit(800) 可以自己设置最大递归层数
  • 解释含有 return 返回值的递归,记住 return的返回流程是:先计算,再返回 ,来看下一段求阶乘的代码:
def jie_cheng(n):
    if n <= 1:
        return 1
    return n * jie_cheng(n - 1)
    
print(jie_cheng(5))

在这里插入图片描述

  • return 是返回到函数调用者,递归函数和普通函数也是一样的,所以递归最后一层空间走到尽头(一是:指向完毕,二是:遇到return,回顾一下而已)遇到return,就要开始返回了,返回到它的调用者(即是阻塞的位置),直到回到最外层空间
  • 如果上面的内容看懂了的话,试着解析下:下面这段代码的执行流程
def get_num(num):
    if num > 2:
        get_num(num - 1)
    print(num)


get_num(4) # 输出结果为:2 3 4

在这里插入图片描述

代码变化一下:

def get_num(num):
    if num > 2:
        get_num(num - 1)
    else:
        print(num)


get_num(4) # 输出结果为 2

'''
解析一下:加了else后,首先代码区有两个分支,
在num>2时,执行会有递归,当n=4 是开辟一层空间;
n=3时开辟一层空间,此时 get_num(2) 再开辟一个空间,
当它从上往下执行过程中,在他本层空间遇到if num>2 不成立,所以走分支 else,直接打印出来;
此时代码还没结束,回到上一层空间,num=3, num>2 已经进入了if 不会走else,
num=4 也不会走else,所以这两层空间没有值输出!
'''
return 返回值 详解
  • 上面这一大部分,就算是递归入门了,接下来才刚刚开始哦!还要继续讲 return ;先来看下这几段代码:求全排列的一部分递归代码,试着分别写出运行结果,并依次分析原因↓↓↓
# 例1:
def fullpermutation(list):
    if list is None:
        return None
    if len(list) == 1:
        return [list]
    res = []
    pivot = list[0]
    remain = fullpermutation(list[1:])
    print(remain)


print(fullpermutation([1, 2, 3]))  
'''
输出结果为:
[[3]]
None
None
'''
  • 递归只会在两种情况下触发“回”的过程,上述是在最后一层空间是碰到了return,所以给回到它的调用处(阻塞处),因为return会给调用者返回一个值,所以在本层空间,remain接收到了一个值:[[3]];接着执行下面的代码,即是打印remain,所以输出“[[3]]”,执行完print,等于回到了上一层空间,又到了调用处(阻塞处),那么这层空间还有返回值吗?答案是没有,所以“最后一层空间是碰到了return 给它返回的值 只会给最后一层使用”,所以接下来两层都是打印空!
# 例2:
def fullpermutation(list):
    if list is None:
        return None
    if len(list) == 1:
        return [list]
    res = []
    pivot = list[0]
    return fullpermutation(list[1:])


print(fullpermutation([1, 2, 3]))
'''
输出结果为:
[[3]]
'''
  • 这次是,在最后一层返回时,获得了一个返回值 [[3]] ,然后回到上一层时,前面又有return 表示需要把本层的返回值,返回到上层的接收处,重复,直到回到最外层,这个从底层传上来的返回值,一直传到了最外层,所以才打印出来的,只有最后一层,但是每次一层都获得了返回值,和例子1 后面两层没有返回值是不同的哦!
# 例3:
def fullpermutation(list):
    if list is None:
        return None
    if len(list) == 1:
        return [list]
    res = []
    pivot = list[0]
    remain = fullpermutation(list[1:])
    print(list)


print(fullpermutation([1, 2, 3]))
'''
输出结果为:
[2, 3]
[1, 2, 3]
None
'''
  • 最后一层碰到return,触发会的过程,回到调用处,执行阻塞处下面的代码,打印list,这个list是什么?它就是本层空间 参数的规模(因为代码写的是规模不断变小从[1,2,3]>>>[2,3]>>>[3]),显然,从最后一层回到上一层,此时规模是[2,3],所以打印list 就是[2, 3];接着继续回到上一层,即是最外层,规模是[1,2,3],所以打印[1, 2, 3],最后的None是因为最外层函数,没有返回值,所以才打印出None
# 例4:
def fullpermutation(list):
    if list is None:
        return None
    if len(list) == 1:
        return [list]
    res = []
    pivot = list[0]
    remain = fullpermutation(list[1:])
    return list


print(fullpermutation([1, 2, 3]))
'''
输出结果为:
[1, 2, 3]
'''
  • 依照上面的步骤,触发回的过程,只要没有到达最外层,return list 返回本层的规模(参数规模),那么这个返回值就会给本层的接收者 remain 不可能给最外层的接收者,虽然这里没有打印 remain的值,但是这里的remain和第一个列子中的remain,后面层数是有返回值的哦(下面例子就会体现),本例,返回到最外层时,list的本层规模为 [1,2,3] 最外层接收者接收到,然后打印出来,所以是[1,2,3]
# 例5:
def fullpermutation(list):
    if list is None:
        return None
    if len(list) == 1:
        return [list]
    res = []
    pivot = list[0]
    remain = fullpermutation(list[1:])
    print(remain)
    return list


print(fullpermutation([1, 2, 3]))
'''
输出结果为:
[[3]]
[2, 3]
[1, 2, 3]
'''
  • 看上面的例子,这次我们是打印了 remain,因为返回的过程中,指向阻塞处(调用处)下面的代码,每次return list 即是 在本层返回 当前的参数规模,所以 remain 是能接收本层的返回值的,所以会打印 [[3]] ,[2, 3] ;最后 [1, 2, 3] 是最外层打印的
# 例6:
def fullpermutation(list):
    if list is None:
        return None
    if len(list) == 1:
        return [list]
    res = []
    pivot = list[0]
    remain = fullpermutation(list[1:])
    print(remain)
    print(list)
    return list


print(fullpermutation([1, 2, 3]))
'''
输出结果为:
[[3]]
[2, 3]
[2, 3]
[1, 2, 3]
[1, 2, 3]
'''
  • 这个,就是所有的融合;通过上面的代码我们来总结下return的返回值:最后一层遇到的return 需要执行 回的过程,此时的返回值 值会返回给最后的调用处的接收者;然后执行 阻塞处下面的代码时,如果又遇到return 这里的返回值,如果没有到达最外层,都是给本层的接收者!为什么要大费周章的讲这个,是因为我们在写递归时,往往不清楚return 要怎么写,已及它的返回值是什么?接下来就要看下return 如何解决问题的

  • 请用递归完成一个字符串的反转,输入字符串“abcd”,完成翻转成“dcba”
    除了递归,我们可以直接用字符串的切片来完成,如下,但是这里是要讲递归的思想,不体现方法优劣!

s="abcd"
print(s[::-1])

想一下递归的思路该怎么做?
思路一:按照上面的例子,不断的划分子规模,我们选的是划分原字符串的规模,都是往后截取的,比如第一次参数s=“abcd” 我们取参数s[1:] ,不断调用函数,每次传入参数为:‘bcd’ ‘cd’ ‘d’ ,这样最后一层返回d 我们能拿到最后一个值,然后最后一个返回值d 加上 当前规模的第一个值,就完成了反转,具体代码如下:

def str_reverse(s):
    if len(s) <= 1: # 递归出口
        return s
    # last = str_reverse(s[1:])
    # return last + s[0]
    return str_reverse(s[1:]) + s[0] # 每次返回最后一层的值,加上当前规模的第一个值

思路二:改变下子规模,我们这次不划分原字符串,而是从它的索引下手,传入最后一个元素的索引,不断去递归索引的值,这时,变的是end的值,从 3 到 2 1 0,到0时触发回的过程,返回当前索引的值,即是 a (不断向前取值),这时我们再加上当前层的s,因为没有划分s所以s每一层都是等于‘abcd’的,我们每次取当前end索引指向的字符串的值,等于从前往后遍历字符串 ‘abcd’ ,两部分链接起来,就完成了反转:每次的返回值为 a, ba , cba, dcba,

def reve(str, end):
    if end == 0:
        return "" + str[0]
    last = str[end] + reve(str, end - 1)
    return last


s = "abcd"
print(reve(s, len(s) - 1))
  • 两种做法都用到return,而是还有重要的递归思路,下面就来看看递归要用什么思路来解

  • 来试下“反转链表”如何用递归做,首先对于一个单链表,要对它反转,我们的思路依旧可以通过不断向后划分子规模,找到它的最后一个结点,然后从后往前依次改变每个结点的指向,让最初的头结点变成尾结点,让它最后指向None;那么具体步骤是:用递归找到最后一个结点,我们可以通过前一个结点的next区域,不断递归,找到下一个结点,直到当前结点的next是None,说明它就是最后一个结点,这也是我们递归的出口,那么依次让它返回,同时改变指向,就完成了,具体看下面的图解:

class ListNode: 
    def __init__(self, x):
        self.val = x
        self.next = None


class Solution:
    def ReverseList(self, pHead):
        if pHead is None: # 判断传入的头结点是否为空
            return None
        if pHead.next is None: # 如果只有一个结点,直接返回结点;同时也是递归的出口
            return pHead
        last_node = self.ReverseList(pHead.next) # last_node永远只接收到了最后一个结点
        pHead.next.next = pHead # 后一个结点,指向前一个结点
        pHead.next = None # 前一个结点,在不同的层,先指向None,如果到达最外层也会指向None
        return last_node # 最后返回最后一个结点,表示反转成功
  • 到达最后一层,触发回的过程,返回 last_node 结点给最后第四层,执行阻塞处下面的代码,因为pHead.next.next 是None,它不用改变也行,直接到 return last_node 把 last_node给第三层用…
    在这里插入图片描述
  • 第三层获得 last_node ,执行阻塞处下面的代码,改变指向,pHead.next.next 是“5结点”,它的指针指向 pHead,即是上一个结点,然后上一个结点指向None;因为这里并不需要用一个temp 来保存前一个指针信息,表面上是断开链接,会丢掉数据,其实不会丢掉,因为他们不再同一层;可以用temp先保存前一个结点的数,这属于递归的写,在我的文章>>>反转链表多种解法 有提到,这里不细说!
    在这里插入图片描述
    在这里插入图片描述
  • 直到 回到最外层,起初的头结点,自然会指向None,最后返回 last_node即可
    在这里插入图片描述
  • 这才是 链表反转 递归的详细过程,果然和我开始理解的不一样,当初怎么都无法理解,直到用断点调试,才发现真正的过程,是这样,大家可以用断点调试下,看下代码的具体过程,下面是测试代码:
class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None


class Solution:
    def ReverseList(self, pHead):
        if pHead is None:
            return None
        if pHead.next is None:
            return pHead
        last_node = self.ReverseList(pHead.next)
        print(last_node.val)
        pHead.next.next = pHead
        pHead.next = None
        return last_node

    def print_list(self, node):  # 打印测试
        if node is None:
            return None
        while node:
            print(node.val, end="")
            node = node.next
        print()


if __name__ == '__main__':
    n1 = ListNode(1)  # 依次实例5个结点
    n2 = ListNode(2)
    n3 = ListNode(3)
    n4 = ListNode(4)
    n5 = ListNode(5)
    n1.next = n2  # 依次将结点链接起来,形成一个链表
    n2.next = n3
    n3.next = n4
    n4.next = n5
    n5.next = None

    obj = Solution()
    print(obj.ReverseList(n1).val)
    # obj.print_list(n1) # 1 2 3 4 5
    # obj.print_list(obj.ReverseList(n1))  # 5 4 3 2 1

递归思路

  • 思想:
    1.找到当前这个值与上一个值的关系
    2.找到程序的出口 有个明确的结束条件
    3.假设当前功能已经完成 每次进入更深一层递归时,问题规模相比上次递归都应有所减少

  • 递归思路:
    (1)找重复:看哪一部分是 实现函数的变化;每次进入更深一层递归时,问题规模相比上次递归都应有所减少
    (2)找变化:变化的量应该作为参数
    (3)找边界(出口):终止条件

  • 递归可以分为:
    (1)直接量+小规模子问题
    (2)多个小规模子问题
    (3) “切蛋糕”思维
    (4)找递推公式,等价交换公式

二分法和递归

# 二分法一定是在排序好的数据里使用

lst = [33, 22, 44, 55, 66, 88, 77, 99, 101, 238, 345, 456, 567, 678, 789]
n = 76
lst.sort()
left = 0
right = len(lst) - 1

while left <= right:  # 条件是 开头<=结尾
    middle = (left + right) // 2
    if lst[middle] > n:  # 每次用对折后,中间的数和 查找对象比较
        right = middle - 1
    elif lst[middle] < n:
        left = middle + 1
    elif lst[middle] == n:
        print("找到了")
        break
else:
    print("这个数不在列表中")

# 递归函数来做
lst = [22, 33, 44, 55, 66, 77, 88, 99, 101, 238, 345, 456, 567, 678, 789]


def func(n, left, right):
    if left <= right:  # 边界
        mid = (left + right) // 2
        if n > lst[mid]:
            left = mid + 1
            func(n, left, right)  # 递归的入口,目的是再确定一次中间的位置
        elif n < lst[mid]:
            right = mid - 1
            func(n, left, right)
        elif n == lst[mid]:
            print("找到了")
            return  # 递归出口
    else:
        print("没有这个数")
        return  # 递归的出口


func(66, 0, len(lst) - 1)

# 升级:如果找到了要求的数,请返回它的索引

lst = [22, 33, 44, 55, 66, 77, 88, 99, 101, 238, 345, 456, 567, 678, 789]


def func(n, left, right):
    if left <= right:  # 边界
        mid = (left + right) // 2
        if n > lst[mid]:
            left = mid + 1
            return func(n, left, right)  # 每一层都要返回给上一层的调用者
        elif n < lst[mid]:
            right = mid - 1
            return func(n, left, right)
        elif n == lst[mid]:
            print("找到了")
            return mid  # 多层函数只会将返回值返回给上一层的调用者
    else:
        print("没有这个数")
        return -1


print(func(66, 0, len(lst) - 1))

尾递归

尾递归(自己调用自己,且非表达式:把值放到参数中算)

在这里插入图片描述

>>>求斐波那契数列第n位是几?(尾递归做法)

def feibo(num, res, temp):
     #使用尾递归法求解斐波那契数量的第num个数字
     if num == 0:
          return res
     else:
          return feibo(num - 1, temp, res + temp)

print(feibo(10,0,1))
# 直接递归  尾递归 与 循环 的对比

import time


def Fib_recursion(num):
    '''
    直接使用递归法求解斐波那契数量的第num个数字
    '''
    if num < 2:
        return num
    return Fib_recursion(num - 1) + Fib_recursion(num - 2)


def Fib_tail_recursion(num, res, temp):
    '''
    使用尾递归法求解斐波那契数量的第num个数字
    '''
    if num == 0:
        return res
    else:
        return Fib_tail_recursion(num - 1, temp, res + temp)


def Fib_circle(num):
    '''
    直接使用循环来求解
    '''
    a = 0
    b = 1
    for i in range(1, num):
        c = a + b
        a = b
        b = c
    return c


if __name__ == '__main__':
    num_list = [5, 10, 20, 30, 40, 50]
    for num in num_list:
        start_time = time.time()
        print(Fib_recursion(num))
        end_time = time.time()
        print(Fib_tail_recursion(num, 0, 1))
        end_time2 = time.time()
        print(Fib_circle(num))
        end_time3 = time.time()
        print('正在求解的斐波那契数字下标为%s' % num)
        print('直接递归耗时为 :', end_time - start_time)
        print('尾递归调用耗时为:', end_time2 - end_time)
        print('直接使用循环耗时为:', end_time3 - end_time2)

递归练习题

>>>打印f-e的数:

def pri(f, e):
    if f > e:
        return
    else:
        print(f)
        return pri(f + 1, e)


pri(1, 6)


>>>求一个列表的和:
def sum_lis(li, f):  # 如果单独是传入一个列表,它体现不了变化
    """
    :param li: 传入一个列表
    :param f: 列表起始位置
    :return: 列表和
    """
    if f == len(li) - 1:  # 表示起始位置也是结束位置,即只有一个元素
        return li[f]

    return li[f] + sum_lis(li, f + 1)


print(sum_lis([1, 2, 3, 4, 5], 0))

# 多引入一个 变化的参数,可以想到第一个元素加上剩下的元素,规模不断变小
# 需求:求 1+2+3+4........100 的和
num = 1
count = 0
while num <= 100:
    count += num
    num += 1
print(count)

'''
思路:
sum(1) + 2 +3.....100
sum(2) + 3........100
sum(3) + 4........100
...
sum(98)+99+100
sum(99) + 100
sum(100)

sum(100) = sum(99) + 100
sum(99) = sum(98) + 99
sum(98) = sum(97) + 98
.....

sum(2) = sum(1) + 2

sum(1) = 1
'''
# 用递归函数解决
def sum(num):
    if num == 1:  # 出口
        return 1
    return num + sum(num - 1)  # 一直返回sum(num-1)+num,每次递归调用,有return才能有返回值


print(sum(100))
>>>需求:打印斐波那契数列
def fibo(num):  # 参数是表示第n个斐波那契数,函数整体表示获取斐波那契数列中第n个数字的值
    if num == 1 or num == 2:  # 第一个和第二个都是1
        return 1  # 返回1,出口
    return fibo(num - 1) + fibo(num - 2)  # 规律,后一项加上后两项,就等于斐波那契数列第n个数字的值


if __name__ == '__main__':
    list_num = []  # 创建一个空列表,开始
    for i in range(1, 21):  # 遍历1-20
        list_num.append(fibo(i))  # 注意这里开始调用函数,获得一个斐波那契数字,将获取到的值填充到list_num
    print(list_num)


# 最佳解法:
def fei_bo(n):
    if n <= 1:
        return (n, 0)
    else:
        (a, b) = fei_bo(n - 1)
        return (a + b, a)


print(fei_bo(5))
# 这里是线性的解法,不再重复计算前一项已知的数
# 求最大公约数
def maxg(m, n):
    if n == 0:
        return m
    return maxg(n, m % n)


print(maxg(6, 0))

# 最大公约数:
# 如果m%n=0 则n是m的最大公约数;例如4%2=0 则2是最大公约数 
# 如果m%n=K 则 n%k=?0 >>> f(m,n)=f(n,m%n)

>>>用递归实现列表排序:
def ins(li, k):
    if k == 0:
        return
    # 对前K个元素进行排序
    ins(li, k - 1)
    # 把位置K的元素插入到前面的部分
    x = li[k]
    index = k - 1
    while x < li[index]:
        li[index + 1] = li[index]
        index -= 1
    li[index + 1] = x
    print(li)


ins([1, 4, 3, 2], 3)
>>>需求:递归实现遍历目录

import os


def get_alldirfile(source_path):  # 定义一个函数获取用户输入的路径名下所有目录和文件
    if not os.path.exists(source_path):  # 判断用户输入的目录是否存在
        return  # 不存在,直接返回,结束该函数,找到一个出口
    list_name = os.listdir(source_path)  # lisdir获取所有目录,并全部放到一个列表中
    for flie_dirname in list_name:  # 遍历下所有的文件目录名
        abs_path = os.path.join(source_path, flie_dirname)  # 拼接成绝对路径
        # 判断下一级是否是目录还是文件,是文件结束,是目录继续深入,直到是文件结束
        if os.path.isfile(abs_path):  # 是文件
            print("file_path:%s" % (abs_path))
        # 也可进行复制操作,open(abs_path,"w",encoding="utf-8")
        if os.path.isdir(abs_path):
            get_alldirfile(abs_path)  # 递归函数


if __name__ == '__main__':
    path = r"F:\日语\快乐50音"
    get_alldirfile(path)

# 优化
import os


def file_get(file_path, n):
    list_file = os.listdir(file_path)
    for file in list_file:
        abs_file = os.path.join(file_path, file)
        if os.path.isdir(abs_file):
            print("\t" * n, file)
            file_get(abs_file, n + 1)
        else:
            print("\t" * n, file)


file_get("D:\KuGou", 1)
  • 324
    点赞
  • 1294
    收藏
    觉得还不错? 一键收藏
  • 50
    评论
评论 50
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值