【算法】算法学习三:递归算法 & 栈

一、递归的含义

递归算法是一种解决问题的方法,其中函数在执行过程中调用自身。它通过将一个大问题拆分成一个或多个相似的子问题,并逐步解决这些子问题来解决整个问题。递归算法通常包含两个关键组成部分:基本情况和递归调用。

基本情况是指可以直接求解或返回结果的简单情况。在递归算法中,当问题达到基本情况时,递归停止,避免无限循环。基本情况是算法中的出口条件。

递归调用是指函数在执行过程中调用自身来解决更小规模的子问题。通过不断调用自身,并将问题规模减小到基本情况的范围内,递归算法能够逐步解决原始问题。每次递归调用都在一个更小的子问题上进行操作,直到最终达到基本情况。

递归算法的实现通常包括以下几个步骤:

  1. 确定基本情况:确定问题的边界条件,即可以直接求解的简单情况。
  2. 定义递归函数:编写一个函数,其中包含对自身的调用来解决更小规模的子问题。
  3. 缩小问题规模:在递归函数中,通过传递更小规模的子问题来逐步缩小原始问题的规模。
  4. 递归调用:在递归函数内部调用自身来解决子问题。
  5. 结合子问题的解:利用子问题的解来解决原始问题。

需要注意的是,递归算法必须确保在每次递归调用时问题规模都能够缩小,否则可能导致无限递归和栈溢出等问题。此外,递归算法的效率可能较低,因为它涉及多次函数调用和重复计算。

递归算法在许多问题上都能提供简洁而优雅的解决方案,如树和图的遍历、排列组合、动态规划等。在编写递归算法时,正确定义基本情况和递归调用非常重要,以确保算法能够正确地终止并给出正确的结果。

二、基线条件和递归条件

对于递归算法,有两个关键概念需要考虑:基线条件(Base Case)和递归条件(Recursive Case)。

基线条件是指在递归算法中确定的一个或多个停止递归的条件。当满足基线条件时,递归将不再进行,而是直接返回一个结果或执行其他操作。基线条件通常是问题的最小规模或最简单情况。

递归条件是指在递归算法中定义的条件,用于决定是否需要进行递归调用。当问题的规模还没有达到基线条件时,递归条件将触发递归调用,将问题拆分为更小的子问题,并继续递归求解。

基线条件和递归条件共同构成了递归算法的终止条件和递归调用条件。

以下是一个计算阶乘的递归算法的示例:

def factorial(n):
    # 基线条件
    if n == 0:
        return 1
    
    # 递归条件
    else:
        return n * factorial(n-1)

在这个例子中,基线条件是当输入的数字 n 等于 0 时,直接返回结果 1。递归条件是当 n 不等于 0 时,通过调用自身传入较小规模的子问题 n-1,然后将子问题的结果乘以 n,最终得到整个问题的解。

基线条件确保递归能够终止,而递归条件则控制递归的执行过程,将问题逐步缩小直至达到基线条件。

在编写递归算法时,正确定义基线条件和递归条件非常重要。如果基线条件不正确或递归条件不满足,可能导致无限递归或无法获得正确的结果。同时,还要确保问题规模在每次递归调用时都能够减小,以避免无限递归和栈溢出等问题。

三、栈

3.1 什么是栈

栈(Stack)是一种常见的数据结构,它按照后进先出(Last In, First Out,LIFO)的原则工作。它类似于现实生活中的一叠盘子,只能在顶部进行插入和移除操作。

栈具有以下特点:

  1. 后进先出:最后插入的元素将首先被移除,而最先插入的元素将成为栈中的底部元素,只有在底部的元素才能被最后移除。
  2. 插入和移除操作:栈有两个主要操作,即入栈(Push)和出栈(Pop)。入栈将元素添加到栈的顶部,出栈将栈顶的元素移除。
  3. 只能访问栈顶元素:除了入栈和出栈操作外,栈还提供了一种访问栈顶元素的方法,通常称为Peek或Top操作。通过Peek操作,可以查看栈顶的元素而不对其进行移除。

栈的实现通常基于数组或链表。使用数组实现的栈被称为顺序栈,而使用链表实现的栈被称为链式栈。

栈在计算机科学中有广泛的应用,例如:

  1. 函数调用:在函数调用期间,计算机使用栈来存储函数的调用信息。每当一个函数被调用,其参数、返回地址和局部变量等信息将被推入栈中。当函数执行完成后,这些信息将被弹出栈。
  2. 表达式求值:在表达式求值过程中,栈可用于存储运算符和操作数。通过使用栈,可以按照正确的顺序计算表达式中的操作。
  3. 浏览器历史记录:浏览器使用栈来存储用户访问的不同网页,每当用户点击“后退”按钮时,最后访问的网页将被弹出栈。
  4. 撤销操作:许多编辑器和应用程序使用栈来实现撤销操作。每当用户执行一个操作时,相关的信息将被推入栈中,当用户执行撤销操作时,最后的操作将被弹出栈,以返回到之前的状态。

栈的简单性和实用性使其成为解决各种问题的重要工具。它提供了一种有序且高效的数据存储和访问方式,能够满足许多算法和应用程序的需求。

3.2 调用栈

调用栈(Call Stack)是一种用于跟踪程序中函数调用和返回的机制。它是一个栈结构,用于存储函数调用时的相关信息,包括函数的参数、局部变量和返回地址等。调用栈的操作基于后进先出(LIFO)原则。

当一个函数被调用时,它的相关信息会被推入调用栈中,形成一个栈帧(Stack Frame)或称为活动记录(Activation Record)。栈帧包含了函数的参数、局部变量以及调用该函数后需要返回的地址。然后,程序执行被调用的函数。

如果被调用的函数内部再次调用其他函数,新的栈帧将被创建并推入调用栈中。这样,栈中的顶部栈帧始终代表当前正在执行的函数。

当函数执行完成后,对应的栈帧将从调用栈中弹出,程序将返回到调用该函数的位置继续执行。被弹出的栈帧被丢弃,其内存空间可以被重新利用。

调用栈在程序执行期间起着重要的作用,它管理了函数调用的顺序和相关信息,确保函数的正确执行和返回。如果函数调用的嵌套层级很深或者递归调用的次数很多,调用栈可能会变得很大,有时会导致栈溢出的错误。

调用栈也可以用于调试程序。通过查看调用栈中的栈帧信息,可以了解程序执行过程中函数的调用顺序和参数值,有助于定位错误和理解代码的执行流程。

总而言之,调用栈是一种用于管理函数调用和返回的数据结构,它提供了一种跟踪程序执行流程的方式,对于函数的嵌套调用和程序的执行顺序至关重要。

def greet2(name):
    print("How are you? " + name + " !")

def bye():
    print("OK, bye!")

def greet(name):
    print("Hello, " + name + " !")
    greet2('Mike')
    bye()
    print('Bye!')

greet('Tom')

输出结果为:

Hello, Tom !
How are you? Mike !
OK, bye!
Bye!

调用栈的执行顺序如下:

  1. 主程序开始执行,调用了 greet(‘Tom’)。
  2. greet 函数被推入调用栈。
  3. 在 greet 函数内部,调用了 greet2(‘Mike’)。
  4. greet2 函数被推入调用栈。
  5. greet2 函数执行完成后,从调用栈中弹出。
  6. 控制权返回到 greet 函数,继续执行。
  7. 在 greet 函数内部,调用了 bye()。
  8. bye 函数被推入调用栈。
  9. bye 函数执行完成后,从调用栈中弹出。
  10. 控制权返回到 greet 函数,继续执行。
  11. greet 函数执行完成后,从调用栈中弹出。
  12. 主程序执行结束。

通过调用栈,我们可以跟踪和管理函数的嵌套调用和返回过程,确保函数按照正确的顺序执行。调用栈在函数调用和返回的过程中起到了重要的作用。

3.3 递归调用栈

递归调用栈是指在函数内部调用自身的过程中形成的调用栈结构。当一个函数在执行过程中再次调用自身,新的函数调用会被推入调用栈,形成一系列嵌套的函数调用。

让我们以一个简单的递归函数示例来说明递归调用栈的工作原理:

def countdown(n):
    if n <= 0:
        print("Go!")
    else:
        print(n)
        countdown(n - 1)

countdown(5)

在这个例子中,我们定义了一个名为 countdown 的递归函数。该函数接收一个参数 n,并按照从 n 倒数到 1 的顺序进行打印,直到达到基线条件 n <= 0,然后打印 “Go!”。

当我们调用 countdown(5) 时,以下是递归调用栈的执行过程:

  1. countdown(5) 被推入调用栈。
  2. countdown(5) 执行,并打印 5。
  3. countdown(4) 被推入调用栈。
  4. countdown(4) 执行,并打印 4。
  5. countdown(3) 被推入调用栈。
  6. countdown(3) 执行,并打印 3。
  7. countdown(2) 被推入调用栈。
  8. countdown(2) 执行,并打印 2。
  9. countdown(1) 被推入调用栈。
  10. countdown(1) 执行,并打印 1。
  11. countdown(0) 被推入调用栈。
  12. countdown(0) 执行,满足基线条件,打印 “Go!”。
  13. countdown(0) 从调用栈中弹出。
  14. 控制权返回到上一层的 countdown(1)。
  15. countdown(1) 从调用栈中弹出。
  16. 控制权返回到上一层的 countdown(2)。
  17. 依此类推,直到 countdown(5) 从调用栈中弹出,程序执行结束。

递归调用栈在递归函数的执行过程中起到关键作用。它管理了递归函数的嵌套调用顺序和相关信息,确保函数按照正确的顺序被调用和返回。如果递归调用层级过深,调用栈可能会变得很大,有时会导致栈溢出的错误。因此,在编写递归函数时,需要注意控制递归的终止条件,以避免无限递归和栈溢出的问题。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

旅途中的宽~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值