算法-递归

核心分析

递归很简单:

  • 递:向下传递
  • 归:向上收束

可以看做是到达边界后反弹,因此,递归边界是不可或缺的。

组合思想

递归的结构并不复杂,但是可谓威力强大的。

不仅是因为神奇的结构性质,和许多巧妙的思想是分不开的。

闭包拆包

闭包是编程中的一大利器,思想层面上的影响也更加深远。

正如日常的电子产品、自定向下的设计方式、程序代码中的接口设计等,我们都无需关心内部的实现,我们依然得心应手。

思维上面,我倾向于将它看做:不同领域的环境变量控制


在递归过程中,每一层的结构可以这样划分

  • 当前层的结果计算
  • 其他层的计算结果

一步一步追根究底,将明晰的都化为结果的一部分,直到全部明确。

def sum(t):
    if t < 0:
        return -1
    return t + sum(t-1)

在每一次的传递过程中,计算部分明晰的结果,稀释整个问题,收缩发散的空间。

正如二分法,不断的对查找空间进行分割,到极限处,都是直接的相等的判断。


将庞大的问题,逐渐解剖,逐层进行剥离,在边界处全部明晰,然后组合全部的细节。

  • 递:拆包
  • 归:闭包

问题等价

对于闭包拆包而言,两方面太过于平均了,而且对于的过程,还存在计算的聚合,正好响应了核心主旨

  • 递:拆
  • 归:闭

有没有一种更好的办法,能够减少传递的损耗呢。


尾递归为人所熟知,因为它可以直接的计算结果,本质是消除了的计算归纳,将这部分计算融入了的过程,保证的一路畅通。

def sum(t,res):
    if t < 0:
        return -1
    if t == 0:
        return res
    return sum(t-1, res + t)

这个方法里面有一个特殊的点:一个贯穿全文的上下文。

使用上下文的好处是显而易见的,但是也存在一定的弊端:并非所有递归都可化为上下文。


这种方式,对于更加深化的内容,大家都称为状态转移

问题等价的核心本质是等量代换,使用更加简单的,可逼近的等同问题求解,直到边界直接计算。

不过这个时候的重心,更多的是在于转移关系的剖析和建立,而不是那么简单的表述了。

思维发散

递归的结构颇具趣味,一眼到头,却是深陷其中无法自拔。

因为子结构和自身的等同性,你无法确定它是在传递还是在计算,它是递还是归,它是过程还是结果。

没错,它都是;它既作为结果,同样也是过程。

因此,除了在计算过程的拆解有不菲的战绩,在结果的组合、世界线收束的场景下,也占据举足轻重的地位。


还是青蛙跳台阶,每次1或2

def jump(n):
    if n < 2:
        return 1
    return jump(n-1) + jump(n-2)

抛开边界条件,我们主体看递归部分jump(n-1) + jump(n-2)

但是,还需要抛弃的,是错误的计算性思维,正确的应该叫做结果构建,或者可能性汇聚。

可以作为计算过程,但是这里应该解读为:从n-1跳一步和n-2跳两步。

这里是作为两个结果之和,虽然同样是拆包,但是拆的是组成,而不是计算。

思想小结

递归的多样化,都来源于基础领域的相互组合。

  • 内容
    • 过程
    • 结果
  • 结构

它自身的结构可同时包含以上全部内容,从而构建复杂的递归方法。

外部形态

问题传递

从名字就可以看出来,这是对应的问题等价一类的一般性结构,使用新问题去解答替换老问题。
f ( x ) = f ( g ( x ) ) f(x) = f(g(x)) f(x)=f(g(x))

结果拆解

结果拆解这里,属于十分混乱的区域,一致的模板应该为
f ( x ) = ( a f ( p ( x ) ) ⊗ b f ( q ( x ) ) ) ⊙ c g ( x ) f(x) = (af(p(x)) \otimes bf(q(x)) ) \odot cg(x) f(x)=(af(p(x))bf(q(x)))cg(x)
这是因为内容为结果还是计算带来的差异,如果是纯净的结果拆解,应该是
f ( x ) = f ( p ( x ) ) + f ( q ( x ) ) + ⋯ f(x) = f(p(x)) + f(q(x)) + \cdots f(x)=f(p(x))+f(q(x))+
就是思维发散中举的例子,但是更多的关于计算内容的结果组合,对于特有的系数和常数项,以及子结果的组合方式千奇百怪。

因为拆解必定设计聚合,这部分是在无法一言以蔽之。

使用方式

独立递归

独立递归没有特殊的使用方式,本质上并没有脱离上述总结的内容。

递归爆破

在常用的DFSBFS中,我们的目的总是明确的,但是范围比较广。

对于n数和、八皇后问题和背包问题这种,都相当于是在一个大型集合中去筛选出特定的数据集。

因此会涉及到遍历递归,针对不同的变化条件进行爆破,然后对结果进行收集。


针对收集的结果,可以出现两种有趣的分支

  • 结果丢弃

    对于特定形式的数据收集,呈现尾递归似的对过程的偏爱,在边界下进行数据集的校验,收集或丢弃

  • 操作回溯

    由于底层实现带来的便利,对于某时刻的环境的快照,可以让程序回退时候还原当初的状态。

    不过和结果丢弃不太相同,这一类经常判断的是递归条件,对过程进行检查,判断是否能继续递归。

操作回溯算是对于结果丢弃中的递归条件的检查,让确定错误的递归无以为继。

不过对于结果的收集,可以直接收集或者条件收集。

先果后因

递归除了可以作为过程,还可以作为结果,这里就十分挑战我们的大脑了。

leetcode887

    public int superEggDrop(int k, int n) {
        if(k == 1 || n < 2){
            return n;
        }
        int times = Integer.MAX_VALUE;
        for(int level = 1; level < n+1; level++){
            times = Math.min(times, Math.max(superEggDrop(k-1, level-1), superEggDrop(k, n-level)) + 1);
        }
        return times;
    }

其中的times,到底是怎么来的?

准确而无用的回答:递归到边界的直接返回设定值,每一层基于子结果的聚合和比较,计算当前层数值,并逐层向上传递。


这里明显的看到递归爆破的痕迹,但是对于结果的筛选居然是基于结果的筛选。。。

需要先明确的是,常规爆破都是单程票,在边界处直接进行的直接过滤筛选。

但是这道题中包含

  • 遍历式递归
  • 递、归问题拆解和结果筛选
  • 基于子结果筛选出结果

这个递归单元,完全的发挥出了它作为结果的计算过程。

因此,对于这种非简单结果聚合和问题拆解的问题类型,总需要有一个独立的结果寄存器。

递归一个还未出现的结果,让层级的递归去进行调优,然后返回调优后的结果作为结果。


这里所谓的结果调优,对比于一般的深度爆破有一个特殊的点:局部寄存器而非全局寄存器。

在每个递归环节,都需要计算出自己的结果,然后上级根据子层级的结果进行筛选;而非全局各结果收集。

使用树结构来作比,深度爆破关注的是递归到边界的叶子节点的筛选;而这种方式是各节点根据子节点的筛选逐级上升到根节点。

有一种堆排序的感觉。

相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页