迭代(iteration):
迭代是一种基于循环结构的重复执行过程,通过反复执行相同的操作来解决问题或遍历数据集。在迭代中,一系列操作会被多次执行,直到满足某个条件为止,通常使用for
循环或while
循环来实现。
迭代的关键特点:
- 循环控制条件:迭代中通常有一个循环控制条件,用于确定何时终止循环。这个条件可以是某个变量的值、某个状态是否满足或其他条件。
- 循环体:循环体是迭代中要执行的一组操作。这些操作可以是任意类型的代码块,可以包含条件语句、函数调用、变量更新等。
- 迭代变量:迭代过程中通常会使用一个或多个变量来跟踪迭代状态,并根据条件的结果来更新这些变量。这些变量的值可能在每次迭代中变化。
以下函数基于for循环实现的功能
# python
def for_loop(n: int) -> int:
res = 0
for i in range(1,n+1):
res += i
return res
递归(recursion):
递归是一种在函数定义中使用函数自身的方法。它是一种解决问题的方法,其中问题被分解为规模更小的子问题,并通过递归调用来解决这些子问题。
递归两个关键要素:
- 基本情况(Base Case):确定递归何时结束的条件。当问题达到某个最小规模时,不再需要递归调用,函数直接返回结果。基本情况通常是递归算法中的出口条件。
- 递归步骤(Recursive Step):将原始问题划分为一个或多个规模较小但相同结构的子问题,然后通过递归调用解决这些子问题。递归步骤将问题分解为更小的部分,直到达到基本情况。
以下函数利用递归计算阶乘
def factorial(n):
# 基本情况:n为0或1时,直接返回1
if n == 0 or n == 1:
return 1
# 递归步骤:调用自身计算n-1的阶乘,并将结果与n相乘
else:
return n * factorial(n-1)
调用栈
递归调用栈是在程序执行递归函数时所使用的一种数据结构,它用于跟踪函数调用和返回的顺序。每当一个函数被调用时,相关的信息(如参数值、局部变量等)会被保存在栈帧中,并将此栈帧推入调用栈的顶部。当函数执行完毕并返回结果时,对应的栈帧将从调用栈中弹出。
下面是一个简单的例子来说明递归调用栈的工作原理:
def recursive_function(n):
if n <= 0:
return
print(n)
recursive_function(n - 1)
recursive_function(3)
'''
当我们调用recursive_function(3)时,以下是递归调用栈的状态:
初始调用: recursive_function(3)
调用栈: [recursive_function(3)]
第一次递归调用: recursive_function(2)
调用栈: [recursive_function(3), recursive_function(2)]
第二次递归调用: recursive_function(1)
调用栈: [recursive_function(3), recursive_function(2), recursive_function(1)]
第三次递归调用: recursive_function(0)
调用栈: [recursive_function(3), recursive_function(2), recursive_function(1), recursive_function(0)]
递归结束,开始返回:
调用栈: [recursive_function(3), recursive_function(2), recursive_function(1)]
返回到第二次调用:
调用栈: [recursive_function(3), recursive_function(2)]
返回到第一次调用:
调用栈: [recursive_function(3)]
返回到初始调用,递归结束。
'''
递归调用栈的大小取决于递归函数被调用的次数。对于每个函数调用,会将新的栈帧推入调用栈中,并在函数返回后从栈中弹出。如果递归的深度过大,可能会导致栈溢出的错误。因此,在编写递归函数时,要确保递归能够收敛到基本情况,以避免无限递归和栈溢出问题。
递归函数每次调用自身时,系统都会为新开启的函数分配内存,以存储局部变量、调用地址和其他信息等。这将导致两方面的结果:
- 函数的上下文数据都存储在称为“栈帧空间”的内存区域中,直至函数返回后才会被释放。因此,递归通常比迭代更加耗费内存空间。
- 递归调用函数会产生额外的开销。因此递归通常比循环的时间效率更低。
尾递归
尾递归是一种特殊类型的递归,其特点是递归调用发生在函数的最后,并且递归调用的返回值直接作为当前函数的返回值,没有其他操作。
在传统的递归中,每次递归调用都会生成新的栈帧,在调用栈中占用额外的内存空间。当递归深度较大时,可能导致栈溢出的问题。而尾递归则通过优化,使得递归调用不再占用额外的栈空间。
下面是一个使用尾递归的例子来计算阶乘:
def factronial(n,result = 1):
if n == 0:
return result
else:
return factronial(n - 1, n*result)
尾递归和递归都是一种函数调用自身的技术,但它们之间有一些关键的区别。
-
调用位置: 在递归中,递归调用通常发生在函数的内部,并且可能会出现在其他操作之前或之后。而在尾递归中,递归调用必须是函数的最后一个操作。
-
返回值: 在递归中,每个递归调用返回时,需要将其结果与其他操作一起处理,并在最后进行汇总。而在尾递归中,递归调用的结果直接作为当前调用的结果返回,无需进一步操作。
-
栈帧: 递归通常会产生多个栈帧,每个调用都会将一个新的栈帧推入栈中,用于保存局部变量和返回地址。因此,递归的深度受限于栈的大小。而尾递归由于满足特定条件,可以通过尾递归优化来消除不必要的栈帧推入,从而避免栈溢出问题。
-
性能: 由于尾递归消除了不必要的栈帧推入,尾递归往往比普通递归更高效。普通递归的性能可能会受到栈的大小限制,并且在处理大规模数据时可能导致栈溢出。而尾递归由于使用循环来实现,不会产生额外的栈帧,因此能够更有效地处理大量数据。
尾递归是一种可以通过优化技术减少内存使用和提高性能的递归形式。然而,并非所有的递归算法都可以很容易地转化为尾递归形式。在设计递归函数时,如果遇到可能导致栈溢出或性能问题的情况,可以考虑使用尾递归来改进代码。
递归树
递归树是一种用于可视化理解递归算法执行过程的图形结构。它展示了递归函数在调用自身时所形成的树状结构,可以帮助我们分析和优化递归算法,并更好地理解递归的本质。
递归树的根节点对应于初始调用,而每个子节点对应于每次递归调用。子节点可能会有自己的子节点,以此类推,形成递归树的分支。
通过观察递归树,可以帮助我们理解递归函数的执行过程、递归深度以及重复计算等情况。递归树也有助于分析算法的时间复杂度,例如递归调用的次数以及每次调用所需的时间。
以下是一个计算斐波那契数列的递归树示例:
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \
fib(2) fib(1)
通过观察递归树,我们可以看到重复计算的情况,例如 fib(3)
被计算了两次。这种情况下,可以使用一些优化技术(如记忆化)来避免重复计算,提高算法的效率。
本篇笔记参考Hello 算法