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

递归

基本结构

    • 递归
      • 什么是递归Recursion?
      • 递归最简单的实例:数列求和
      • 递归“三定律”
      • 递归算法的实现
      • python中的递归深度限制
      • 递归可视化
      • 汉诺塔问题
      • 分治策略
      • 优化问题
      • 贪心策略Greedy Method
      • 找零兑换问题:递归解法
      • 动态规划
      • tips
      • tips2 良好的代码风格
        • 可读性
        • 可维护性
        • 可扩展性
      • 背包问题和伪多项式时间复杂度

什么是递归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二进制

你可能感兴趣的:(数据结构python版课堂笔记)