python数据结构课堂笔记4:递归与动规

递归基本结构递归什么是递归Recursion?递归最简单的实例:数列求和递归“三定律”递归算法的实现python中的递归深度限制递归可视化汉诺塔问题分治策略优化问题贪心策略Greedy Method找零兑换问题:递归解法动态规划tipstips2 良好的代码风格可读性可维护性可扩展性背包问题和伪多项式时间复杂度什么是递归Recursion?递归是一种解决问题的方法,其精髓在:将问题分解...
摘要由CSDN通过智能技术生成

递归

什么是递归Recursion?

  • 递归是一种解决问题的方法,其精髓在:
    将问题分解为规模更小的相同问题
    持续分解,直到问题规模小到可以用非常简单直接的方式解决
    递归的问题分解方式非常独特,其算法方面的明显特征就是:在算法流程中调用自身

  • 递归为我们提供了一种对复杂问题的优雅解决方案,精妙的递归算法常会出奇简单,令人赞叹。

递归最简单的实例:数列求和

  • 问题:给定一个列表,返回所有数的和,列表中数的个数不确定
  • 如果不用for循环和while循环,就可以用递归来求解
  • 我们认识到求和实际上最终是由一次次的加法实现的,而加法恰有两个操作数,是确定的。
  • 我们的目的是将规模较大的列表求和,分解为规模较小且固定的两个数求和
  • 换方式来表达数列求和:全括号表达式(1+(3+(5+(7+9))))
  • 则加法实现为:
    total = (1+(3+(5+(7+9))))
    total = (1+(3+(5+16)))
    total = (1+(3+21))
    total = (1+24)
    total = 25
  • 求和问题可归纳为:数列的和 = “首个数” + “余下数列”的和
  • 如果数列包含的数少到只有1个的话,它的和就是这个数了
    lenSum(numList) = first(numList) + listSum(rest(numList))
    问题 分解 相同问题,规模更小
def listsum(numList):
    if len(numList) == 1:
        return numList[0]#更小规模
    else:
        return numList[0] + listsum(numList[1: ])#调用自身

print(listsum[1,3,5,7,9])  

问题分解为更小规模的相同问题,并表现为"调用自身";对更小规模问题的解决:简单直接

递归“三定律”

  1. 递归算法必须有一个基本结束条件(最小规模问题的直接解决)
  2. 递归算法必须减小规模,改变状态,向基本结束条件演进(减小问题规模)
  3. 递归算法必须调用自身(解决减少了规模的相同问题)
    调用自身可以理解为“问题分解成了规模更小的相同问题”

递归算法的实现

  • 当一个函数被调用的时候,系统会把调用时的现场数据压入到系统调用栈
    现场数据:包括要返回函数的名称、调用函数时函数包括的参数、局部变量等。
    每次调用,压入栈的现场数据称为栈帧
    当函数返回时,要从调用栈的栈顶取得返回地址,恢复现场,弹出栈帧,按地址返回。
    在递归时,会产生一些局部变量,并将现场数据压入系统调用栈中,当递归结束后,将局部变量销毁,栈顶数据按地址返回。

python中的递归深度限制

  • 调用递归时常出现的错误:RecursionError
    递归的层数太多,系统调用栈容量有限

  • 这时候要检查程序中是否忘记设置基本结束条件,导致无限递归
    或者向基本结束条件演进太慢,导致递归层数太多,调用栈溢出

  • 在python内置的sys模块可以获取和调整最大递归深度

import sys
sys.getrecursionlimit()#获取递归深度
sys.setrecursionlimit(3000)#修改递归深度

递归可视化

  • python海龟作图系统turtle moudle
    python内置,随时可见,以LOGO语言的创意为基础
    其意象为模拟海龟在沙滩上爬行留下的足迹
    爬行:forward(n);backward(n)
    缩写:fd(n)、bk(n)
    转向:left(a);right(a)
    缩写:lt(a)、rt(a)
    抬笔放笔:抬笔penup();落笔pendown()
    缩写:pu()、pd()
    笔属性:笔画粗细pensize(s);
    笔画颜色pencolor©
    画圆:circle(半径,角度)
    画点:dot(大小,颜色)
    填充:
    设定填充颜色:fillcolor(“color”)
    开始填充:begin_fill()
    结束填充:end_fill()
    坐标控制:
    直接到达:goto(x,y)
    获取坐标:position()
    计算距离:distance(x,y)
    turtle.tracer(0)去掉动画直接显示最后图形

作图模板

#1.导入海龟模块
import turtle

#2.生成一只海龟,做一些设定
t = turtle.Turtle()
t.color("blue")
t.pensize(3)#宽度

#3.用海龟作图
t.forward(100)
t.right(60)
t.pensize(5)
t.backward(150)
t.left(90)
t.color("brown")
t.forward(150)

#4.结束作图
t.hideturtle()#隐藏海龟,可选
turtle.done()
  • 分形:“一个粗糙或零散的几何形状,可以分成数个部分,且每一部分都(至少近似地)是整体缩小后的形状”,即具有自相似的性质。
    分形树:自相似递归图形

汉诺塔问题

  • 要求
    一次只能搬一个盘子;大盘子不能叠在小盘子上
    搬完64个盘片需要移动:2**64 - 1次
  • 解决思路
    递归三定律分析汉诺塔问题(基本结束条件(最小规模);如何减小规模;调用自身)
    分解递归:
    假如有5个盘子,穿在1号柱上,需要挪到3号柱:
    1.如果能有办法把最上面的一摞4个盘子统统挪到2号柱上;
    2.把剩下最大号盘子直接从1号柱挪到3号柱;
    3.再用同样的办法把2号柱上的一摞4个盘子挪到3号柱即可。
    一摞4个盘子把上面3个盘子挪到3号柱,把剩下最大号盘子从1号柱挪到2号柱,\
    再用同样的办法把一摞3个盘子从3号柱挪到2号柱
    3个以下盘子同样移法

分治策略

解决问题的典型策略:分而治之
将问题分为若干更小规模的部分
通过解决每一个小规模部分问题,并将结果汇总得到原问题的解
分治策略和递归算法有着天然的联系

优化问题

  • 计算机科学中许多算法都是为了找到某些问题的最优解
    例如,两点之间的最短距离;
    能最好匹配一系列点的直线;
    或者满足一定条件的最小集合
  • 经典案例:兑换最小个数的硬币问题
    假设为一家自动售货机厂家编程序,自动售货机要每次找给顾客最少数量硬币
    人们会采用各种策略来解决这些问题,例如最直接的“贪心策略
    一般会这么做:从最大面值的硬币开始,用尽量多的数量;
    有余额的,再到下一最大面值的硬币,还用尽量多的数量,直到最小面值硬币为止

贪心策略Greedy Method

每次都试图解决问题的尽量大的一部分
对应到兑换硬币的问题,就是每次以最多数量的最大面值硬币来迅速减少找零面值
贪心策略适用于“局部最优等同于总体最优”的问题求解。
即第一步分解的问题可解决的方法尽量接近总体目标,不存在回溯的问题。

找零兑换问题:递归解法

  • 首先确定基本结束条件,兑换硬币这个问题最简单直接的情况就是,需要兑换找零,其面值正好等于某种硬币

  • 其次减小问题的规模,我们对每种硬币尝试一次

  • 对递归解法进行改进的关键在于消除重复计算:可以用一个表将计算过的中间结果保存起来,在计算之前查表看看是否已经计算过

  • 这个算法的中间结果就是部分找零的最优解,在递归调用过程中已经得到的最优解被记录下来
    在递归调用前,先查找表中是否已有部分找零的最优解
    如果有,直接返回最优解而不进行递归调用
    如果没有,则进行递归调用
    这种改进叫做记忆化或者函数值存储技术
    可用来解决递归重复计算的问题

动态规划

  • 动态规划算法采用了一种更有条理的方式来得到问题的解
  • 找零兑换的动态规划解法从最简单的“1分钱找零”的最优解开始,逐步递加上去,直到我们需要的找零钱数
  • 在找零递加过程中,设法保持每一分钱的递加都是最优解,一直加到求解找零钱数,自然得到最优解
  • 递加过程中保持最优解的关键是,其依赖于更少钱数最优解的简单计算,而更少钱数的最优解已经得到了
  • 问题的最优解包含了更小规模相同问题的最优解”,这是一个最优化问题能够用动态规划策略解决的必要条件
    包括:仅依赖于规模小的最优解
  • 动态规划并不是递归函数
    动态规划中最主要的思想是:
    从最简单情况开始到达所需找零的循环
    其每一步都依靠以前的最优解来得到本步骤的最优解,直到得到答案。
    把小规模相同问题的最优解先算出来。

tips

递归与动态规划不同点:
递归+函数值缓存技术:要什么就算什么,从n(最后,即最大规模)开始,递归调用,所以缓存中的函数值什么时候被算出来未知
动态规划:直接找到最简单的情况,从最简单的情况向目标迭代。
使用动态规划解决问题的要求:大规模问题的解包含了其部分问题的解

背包问题和单词最短编辑距离,如果使用递归来求解,就使用了分治策略;如果使用动态规划则不属于。
列表适用于密集数据集;字典适用于稀疏数据集;集合中的数据项无次序关系,其数据项需要去重复。
记忆化递归求解效率一般高于动态规划,但是递归主要问题是递归会使用系统调用栈,受系统资源影响较大

可迭代对象是可以对包含数据项逐个枚举的对象
集合是“简版”字典,只有key没有value的字典,都是通过散列表来实现

tips2 良好的代码风格

可读性

变量、函数命名:必要的注释(关键点、函数参数返回值)
代码格式(PEP8自动格式化)

可维护性

尽量不用全局变量;
不依赖于某些特定缺省值;
不硬编码某些特定位置、特定值;

可扩展性

一些变量用符号代替,并集中放在文件头部
不要import *
用类的接口方法不是类变量

背包问题和伪多项式时间复杂度

0-1背包问题的复杂度O(nW)是个多项式时间复杂度
但是在图灵机等价的计算模型下
背包问题的规模取决于最大重量W的二进制位数logW
当logW增加1,计算时间就是原来的2倍,这是指数增长(2**logW)
背包问题是个NP问题,目前尚未找到真正的多项式时间复杂度问题

另一个具有伪多项式时间复杂度算法的NP问题是判断素数
其输入规模为数N的二进制位数logN
当logN增加1,计算时间就是原来的2倍
实际的计算机整数是固定长度,已经预留了至少32bits二进制

当logW增加1,计算时间就是原来的2倍,这是指数增长(2**logW)
背包问题是个NP问题,目前尚未找到真正的多项式时间复杂度问题

另一个具有伪多项式时间复杂度算法的NP问题是判断素数
其输入规模为数N的二进制位数logN
当logN增加1,计算时间就是原来的2倍
实际的计算机整数是固定长度,已经预留了至少32bits二进制

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值