算法入门学习——第三章:递归、栈、调用栈(Python实现)

本文深入探讨了递归的概念,通过两种不同的方法寻找钥匙的示例解释了递归的工作原理。递归条件和基线条件是编写递归函数的关键,而调用栈在递归过程中起着至关重要的作用,它存储了未完成的函数调用。在计算5的阶乘的递归函数示例中,详细展示了调用栈的变化过程。尽管递归使代码更易理解,但可能会占用大量内存,有时需要考虑使用循环或尾递归来优化。递归在算法和数据结构如栈中广泛应用,理解其工作原理对于程序员至关重要。
摘要由CSDN通过智能技术生成

第三章 递归

3.1 递归

引例:
假设你发现了一个上锁的箱子和一个盒子。钥匙可能在盒子里面,这个盒子里还有盒子,而盒子里的盒子又有盒子…(钥匙就在某个盒子中)为了找到钥匙,你将使用什么算法?
第1种方法:
1.创建一个要查找的盒子堆。
2.从盒子堆取出一个盒子,在里面找。
3.如果找到的是盒子,就会将其加入盒子堆中,以便以后再查找。
4.如果找到钥匙,则大功告成。
5.回到第2步。
伪代码:

def look_for_key(main_box):
    pile=main_box.make_a_pile_to_look_through()
    while pile is not empty:#盒子堆
        box=pile.grab_a_box()#取一个盒子
        for item in box:#遍历盒子
            if item.is_a_box():#是盒子就添加在盒子堆中
                pile.append(item)
            elif item.is_a_key():#找到钥匙结束
                print:"find key!"

第2种方法:
1.检查盒子中的每样东西。
2.如果是盒子就回到第1步。
3.如果是钥匙就大功告成。
伪代码:

def look_for_key(box):
    for item in box:
        if item.is_a_box():
            look_for_key(item)#递归函数调用自己
        elif item.is_a_key():
            print: "find key!"

在你看来哪一种方法更容易呢?
第1种方法是使用while循环,只要盒子堆不空就从中取出一个盒子,并在其中仔细查找,如下:
第2种方法是使用递归函数调用自己这种方法的伪代码,如下:

总结:这两种方法的作用相同。递归只是让解决方案更清晰,并没有性能上的优势,实际上在有些情况下使用循环的性能更好。使用循环程序的性能可能更高,使用递归程序可能更容易理解。
PS:很多算法都使用了递归,因此理解这种概念很重要。

3.2 基线条件和递归条件

编写递归函数时必须告诉它何时停止递归。正因如此,每个递归函数都有两部分:基线条件和递归条件
递归条件是指函数调用自己。
基线条件则指函数不再调用自己,从而避免形成无限循环。

3.3 栈

引例:
假设你去野外烧烤,并为此创建了一个待办事项清单——一叠便条(在讨论数组和链表时,也有一个待办事项清单,你可将待办事项添加到清单的任何地方,还可以删除任何一个待办事项)
一叠便条要简单的多——插入的待办事项放在清单的最前面;读取待办事项时,你只读取最上面那个,并将其删除。
因此这个待办事项清单只有两种操作:压入(插入)和弹出(删除并读取)

这种数据结构称为(请结合上述例子具象理解)。栈是一种简单的数据结构,刚才我们一直在使用它,却没有意识到。

栈的定义:
栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

3.4 调用栈

引例:
计算机在内部使用被称为调用栈的栈,为了理解调用栈,来看一个函数:

def greet(name):
    print:"hello," + name
    greet2(name)
    print:"getting ready to say bye..."
    bye()
def greet2(name):
    print:"how are you ?"+name
def bye():
    print:"ok bye!"

下面详细介绍调用函数时发生的情况:
假如你调用greet(“Marry”)计算机将首先为该函数调用分配一块内存。

然后,我们来使用这些内存变量name被设置为Marry,这需要储存到内存中。

每当你调用函数时,计算机都像这样将函数调用涉及的所有变量的值储存到内存中。接下来你打印:hello Marry!

再调用greet2(“Marry”) 同样,计算机也为这个函数调用分配一块内存。

计算机使用一个栈来表示这些内存块,其中第2个内存块(greet2)位于第1个内存块(greet1)上面,你打印:how are you,Marry?**

然后从函数调用返回。此时站顶的内存块被弹出,现在占点的内存块是函数greet的,这意味着你返回到了函数greet。当你调用函数greet2时 ,函数greet指只执行了的一部分。

这是一个重要的概念!调用另一个函数时,当前函数暂停并处于未完成状态,该函数所有变量的值都还在内存中

执行完函数greet2后,你回到函数greet并从离开的地方接着往下执行,首先打印:getting ready to say bye…

再调用函数bye(),在栈顶添加了函数bye()的内存块,然后你打印:OK,bye!并从这个函数返回

现在你又回到了函数greet,由于没有别的事情要做,你就从函数greet返回这个栈。
因此,用于储存多个函数的变量成为调用栈。

3.5 递归调用栈

递归函数也使用调用栈。一起来看看递归函数f的调用栈。
注:f(5)写作5!(其实就是5的阶乘)下面是计算阶乘的递归函数:

def f(x):
    if x==1:
        return 1
    else:
        return x*f(x-1)

下面来详细分析调用f(3)时调用栈是如何变化的(别忘了栈顶的方框指出了当前执行到了什么地方):

注意:每一个f调用都有自己的x变量。在一个函数调用中不能访问另一个x变量——只能访问当前处在栈顶位置的变量。
请添加图片描述

总结:栈在递归中扮演着重要的角色,在本章开头的事例中,有两种寻找钥匙的办法。
使用第1种方法时,你创建一个带查找的盒子堆,因此你始终知道还有哪些盒子是需要查找的。
使用第2种方法及递归的方法时,没有盒子堆。既然没有盒子堆,那么算法怎么知道还有哪些盒子需要查找呢?原来盒子堆储存在了栈中,这个栈包含未完成的函数调用,每个函数调用都包括还未检查完的盒子。
使用栈很方便,因为你无需跟踪盒子堆栈替你这样做了。
最后,使用栈虽然很方便,但是也要付出代价,储存详尽的信息,可能占用大量的内存,每个函数调用都要占用一定的内存,如果占很高就意味着计算机储存了大量函数调用的信息。
在这种情况下你有两种选择:
一、重新编写代码,转而使用循环。
二、使用尾递归,这是一个高级的递归主题,不在我们讨论的范围内。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值