《Hello算法》之迭代与递归

算法效率评估

在能够解决问题的前提下,算法效率已成为衡量算法优劣的主要评价指标。主要包括以下两个维度。
时间效率:算法运行速度的快慢(即算法运行时间的长短)。
空间效率:算法占用内存空间的大小。
目标是设计 “既快又省” 的数据结构与算法。

效率评估方法

1、实际测试
给定两种算法,在计算机上分别运行它们,记录时间和空间使用情况。
缺陷

  • 难以排除测试环境的干扰因素。不同的测试环境可能会得到相反的结果。
  • 展开完整测试非常消耗资源。完整测试需要在不同数据量下开展,非常浪费资源。
    2、理论估算
    复杂度分析(complexity analysis) 描述了随着输入数据大小的增加,算法执行所需时间和空间的增长趋势。
  • “时间和空间资源”分别对应时间复杂度(time complexity)空间复杂度(space complexity)
  • “随着输入数据大小的增加”意味着复杂度反映了算法运行效率与输入数据体量之间的关系
  • “时间和空间的增长趋势”表示复杂度分析关注的不是运行时间或占用空间的具体值,而是时间或空间增长的快慢
    复杂度分析相对于实际测试的优点
  • 仅需理论分析,绿色节能。
  • 独立于运行环境,适用于各类平台。
  • 能够体现不同数据量下的算法效率。

迭代

迭代(iteration):在满足特定条件下重复执行某段代码,直到该条件不再满足。

迭代实现方式

  • for循环
    for循环适合预先知道迭代次数时使用。例求解1到n之和:
def for_loop(n:int)->int:
   """ for循环"""
   res = 0
   # 循环求和1,2,...,n-1,n
   for i in range(1, n+1): # range(a,b)是左闭右开区间[a,b),故此处是n+1.
       res += i
   return res
# 输出
res_for = for_loop(5)
print("for_loop:", res_for)
# for_loop: 15
  • while循环
    while循环与for循环类似,在while循环中,程序每轮都会先检查条件,如果条件为真则继续执行,否则就结束循环。求和代码如下
def while_loop(n:int)->int:
   """ while循环"""
   res = 0
   i = 1  # 初始化条件变量
   while i <= n:
       res += i  # 累加
       i += 1  # 迭代 更新条件变量
   return res
# 输出
res_while = while_loop(5)
print("while_loop:", res_while)
# while_loop: 15
  • 求和函数的流程框图如图
    while循环求和函数
    由于初始化和更新条件变量的步骤是独立在循环结构之外的,while比for循环的自由度更高(for循环更紧凑,while循环更灵活)。例如条件变量每轮进行了多次更新,这种情况while循环比for循环方便多了。
def while_loop_ii(n:int)->int:
    """while循环(两次更新)"""
    res = 0
    i = 1 # 初始化条件变量
    # 循环求和1,4,10,22,...
    while i <= n:
        res += i  # 累加
        # 更新条件变量
        i += 1  
        i *= 2  
        # print("res:", res)
        # print("i:", i)
    return res
# 输出
res_while_2 = while_loop_ii(10)
print("while_loop_ii:", res_while_2)
# while_loop_ii: 15

while适合无法确定循环次数的情况,如:

s = ""
while s != "quit":
    s = input("请输入: ")
print("已输入 quit")
  • 嵌套循环
    可以在一个循环结构内嵌套另一个循环结构,以for循环为例:生成一个包含数字对的字符串,其中第一个数字在1到n之间循环,第二个数字也在1到n之间循环,总循环了n2次,即算法运行时间和输入数据大小n成“平方关系”。
def nested_for_loop(n:int)->int:
    """双层for循环"""
    res = "" # 初始化结果字符串为空
    # 循环i=1,2,...,n-1,n
    for i in range(1, n+1):
        # 循环j=1,2,...,n-1,n
        for j in range(1, n+1):
            # 将生成的数字对添加到结果字符串中
            res += f"({i}, {j})"
    return res
# 输出
res_nested_for_loop = nested_for_loop(3)
print("res_nested_for_loop:", res_nested_for_loop)
# res_nested_for_loop: (1, 1)(1, 2)(1, 3)(2, 1)(2, 2)(2, 3)(3, 1)(3, 2)(3, 3)
  • 嵌套循环的流程框图如图
    嵌套循环

递归

递归(recursion)通过函数调用自身来解决问题,包含两个阶段:
1.递: 函数不断深入地调用自身,通常传入更小或更简化的参数,直到达到“终止条件”。
2.归: 触发“终止条件”后,程序从最深层的递归函数开始逐层返回,汇聚每一层的结果。
递归代码主要包含三个要素:
1.终止条件: 用于决定什么时候由“递”转“归”。
2.递归调用: 对应“递”,函数调用自身,通常输入更小或更简化的参数。
3.返回结果: 对应“归”,将当前递归层级的结果返回至上一层。

  • 例:求和问题:输入n,求和1+2+3+…+n-1+n
def recur(n:int)->int:
    """递归"""
    # 终止条件
    if n == 1:
        return 1
    # 递: 递归调用
    res =  recur(n-1)  # 递归
    # 归:返回结果
    return n + res  # 求和操作
 # 输出
recur0 = recur(5)
print("recur:", recur0)
# recur: 15
  • 递归过程图,每层返回后,都要执行一次求和操作。
    递归过程图

迭代与递归的区别

迭代与递归可以得到相同的结果,但是它们代表了两种不同的思考和解决问题的范式。
迭代:“自下而上” 地解决问题。从最基础的步骤开始,然后不断重复或累加这些步骤,直到任务完成。
递归:“自上而下” 地解决问题。将原问题分解为更小的子问题,这些子问题和原问题具有相同的形式。接下来将子问题继续分解为更小的子问题,知道基本情况时停止(进本情况的解是已知的)。

  • 以上述求和函数为例,设问题f(n)=1+2+3+…+n-1+n
    迭代: 在循环中模拟求和过程,从1遍历到n,每轮执行求和操作,依次解决f(1),f(2),…,f(n-1),f(n)
    递归: 将问题分解为子问题f(n)=n+f(n-1),不断(递归地)分解下去,直至基本情况f(1)=1时终止。

递归调用栈

递归函数每次调用自身时,系统都会为新开启的函数分配内存,以存储局部变量、调用地址和其他信息等。

  • 函数的上下文数据都存储在称为“栈帧空间”的内存区域中,直至函数返回后才会被释放。因此,递归
    通常比迭代更加耗费内存空间
  • 递归调用函数会产生额外的开销。因此递归通常比循环的时间效率更低
    递归深度: 在触发终止条件前,同时存在n个未返回的递归函数,递归深度为n,如图。
    递归深度
    实际中编程语言允许的递归深度通常是有限的,过深的递归深度可能导致栈溢出错误。

尾递归

尾递归(tailrecursion): 如果函数在返回前的最后一步才进行递归调用,则该函数可以被编译器或解释器优化,使其在空间效率上与迭代相当。

  • 普通递归: 当函数返回到上一层级的函数后,需要继续执行代码,因此系统需要保存上一层调用的上下文。
  • 尾递归: 递归调用是函数返回前的最后一个操作,这意味着函数返回到上一层级后,无须继续执行其他操作,因此系统无须保存上一层函数的上下文。
def tail_recur(n:int, res:int)->int:
    """尾递归"""
    # 终止条件
    if n == 0:
        return res
    # 尾递归调用
    return tail_recur(n - 1, n + res)
 # 输出
tail_recur0 = tail_recur(5, 0)
print("tail_recur:", tail_recur0)
# tail_recur: 15

尾递归的执行过程如图所示,与普通递归相比,两者的求和操作的执行点是不同的。

  • 普通递归:求和操作是在“归”的过程中执行的,每层返回后都要再执行一次求和操作。
  • 尾递归:求和操作是在“递”的过程中执行的,“归”的过程只需层层返回。
    尾递归过程
    许多编译器或解释器并不支持尾递归优化,如python默认不支持尾递归优化,因此即使函数是尾递归形式,仍然可能会遇到栈溢出问题。

递归树(recursion tree)

在处理与“分治”相关的算法问题时,递归往往比迭代的思路更加直观、代码更加易读。

  • 求斐波那契数列。给定一个斐波那契数列0,1,1,2,3,5,8,13,…,求该数列的第n个数字。
    解题思路: 设第n个数字为f(n),已知数列的前两个数字为f(1)=0和f(2)=1,且数列中每个数字是前两个数字的和,即f(n)=f(n-1)+f(n-2)。
    按照递推关系进行递归调用,将前两个数字作为终止条件,便可写出递归代码。
def fib(n:int)->int:
    """斐波那契数列:递归"""
    # 终止条件 f(1)=0, f(2)=1
    if n == 1 or n == 2:
        return n - 1
    # 递归调用 f(n) = f(n-1) + f(n-2)
    res = fib(n - 1) + fib(n - 2)
    # 返回结果 f(n)
    return res
 # 输出
fib0 = fib(8)
print("fib:", fib0)
# fib: 13

代码中函数内递归调用了两个函数,即从一个调用产生了两个调用分支。如图所示,不断递归调用,最终产生一颗层数为n的递归树。
递归树

迭代vs.递归

特点迭代递归
实现方式循环结构函数调用自身
时间效率效率通常较高每次函数调用都会产生开销
内存使用通常使用固定大小的内存空间累积函数调用可能使用大量的栈帧空间
代码风格代码直观、可读性好代码结构简洁、清晰。
适用问题适用于简单循环任务适用于子问题分解,如树、图、分治、回溯等
  • 49
    点赞
  • 58
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值