解决递归问题的四部曲-java面试题

Posted 福建沙县小吃

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解决递归问题的四部曲-java面试题相关的知识,希望对你有一定的参考价值。

什么是递归
简单地说,就是如果在函数中存在着调用函数本身的情况,这种现象就叫递归。
以阶乘函数为例,如下, 在 recursion函数中存在着 recursion(n - 1) 的调用,所以此函数是递归函数

public int  recursion(int n) 
    if (n < =1) 
        return1;
    
    return n *  recursion(n - 1)

递归算法通用解决思路
我们在上一节仔细剖析了什么是递归,可以发现递归有以下两个特点

  1. 一个问题可以分解成具有相同解决思路的子问题,子子问题,换句话说这些问题都能调用同一个函数
  2. 经过层层分解的子问题最后一定是有一个不能再分解的固定值的(即终止条件),如果没有的话,就无穷无尽地分解子问题了,问题显然是无解的。

所以解递归题的关键在于我们首先需要根据以上递归的两个特点判断题目是否可以用递归来解。

经过判断可以用递归后,接下来我们就来看看用递归解题的基本套路(四步曲):

  1. 先定义一个函数,明确这个函数的功能,由于递归的特点是问题和子问题都会调用函数自身,所以这个函数的功能一旦确定了,之后只要找寻问题与子问题的递归关系即可
  2. 接下来寻找问题与子问题间的关系(即递推公式),这样由于问题与子问题具有相同解决思路,只要子问题调用步骤 1定义好的函数,问题即可解决。所谓的关系最好能用一个公式表示出来,比如 f(n) = n * f(n-)
    这样,如果暂时无法得出明确的公式,用伪代码表示也是可以的,发现递推关系后,要寻找最终不可再分解的子问题的解,即(临界条件),确保子问题不会无限分解下去。由于第一步我们已经定义了这个函数的功能,所以当问题拆分成子问题时,子问题可以调用步骤 1 定义的函数,符合递归的条件(函数里调用自身)
  3. 将第二步的递推公式用代码表示出来补充到步骤 1 定义的函数中
  4. 最后也是很关键的一步,根据问题与子问题的关系,推导出时间复杂度,如果发现递归时间复杂度不可接受,则需转换思路对其进行改造,看下是否有更靠谱的解法

听起来是不是很简单,接下来我们就由浅入深地来看几道递归题,看下怎么用上面的几个步骤来套
热身赛

输入一个正整数n,输出n!的值。其中n!=123*…*n,即求阶乘

套用上一节我们说的递归四步解题套路来看看怎么解

  1. 定义这个函数,明确这个函数的功能,我们知道这个函数的功能是求 n 的阶乘, 之后求 n-1, n-2 的阶乘就可以调用此函数了
/**
 * 求 n 的阶乘
 */
public int factorial(int n) 

2.寻找问题与子问题的关系 阶乘的关系比较简单, 我们以 f(n) 来表示 n 的阶乘, 显然 f(n) = n * f(n - 1), 同时临界条件是 f(1) = 1,即

3.将第二步的递推公式用代码表示出来补充到步骤 1 定义的函数中

/**
 * 求 n 的阶乘
 */
public int factorial(int n) 
    // 第二步的临界条件
    if (n < =1) 
        return1;
    

    // 第二步的递推公式
    return n * factorial(n-1)

4.求时间复杂度 由于 f(n) = n * f(n-1) = n * (n-1) * … * f(1),总共作了 n 次乘法,所以时间复杂度为 n。

看起来是不是有这么点眉目, 当然这道题确实太过简单,很容易套路,那我们再来看进阶一点的题
入门题

一只青蛙可以一次跳 1 级台阶或一次跳 2 级台阶,例如: 跳上第 1 级台阶只有一种跳法:直接跳 1 级即可。跳上第 2 级台阶
有两种跳法:每次跳 1 级,跳两次;或者一次跳 2 级。 问要跳上第 n 级台阶有多少种跳法?

我们继续来按四步曲来看怎么套路

1.定义一个函数,这个函数代表了跳上 n 级台阶的跳法

/**
 * 跳 n 极台阶的跳法
 */
public int f(int n) 

2.寻找问题与子问题之前的关系 这两者之前的关系初看确实看不出什么头绪,但仔细看题目,一只青蛙只能跳一步或两步台阶,自上而下地思考,也就是说如果要跳到 n 级台阶只能从 从 n-1 或 n-2 级跳, 所以问题就转化为跳上 n-1 和 n-2 级台阶的跳法了,如果 f(n) 代表跳到 n 级台阶的跳法,那么从以上分析可得 f(n) = f(n-1) + f(n-2),显然这就是我们要找的问题与子问题的关系,而显然当 n = 1, n = 2, 即跳一二级台阶是问题的最终解,于是递推公式系为

3.将第二步的递推公式用代码表示出来补充到步骤 1 定义的函数中 补充后的函数如下

/**
 * 跳 n 极台阶的跳法
 */
public int f(int n) 
    if (n == 1)
     return1;
    if (n == 2)
     return2;
    return f(n-1) + f(n-2)

中级题

接下来我们看一下大学时学过的汉诺塔问题:  如下图所示,从左到右有A、B、C三根柱子,其中A柱子上面有从小叠到大的n个圆盘,现要求将A柱子上的圆盘移到C柱子上去,期间只有一个原则:一次只能移到一个盘子且大盘子不能在小盘子上面,求移动的步骤和移动的次数

这里直接给大家答案,大家自己好好研究吧

// 将 n 个圆盘从 a 经由 b 移动到 c 上
public void hanoid(int n, char a, char b, char c) 
    if (n <= 0) 
        return;
    
    // 将上面的  n-1 个圆盘经由 C 移到 B
    hanoid(n-1, a, c, b);
    // 此时将 A 底下的那块最大的圆盘移到 C
    move(a, c);
    // 再将 B 上的 n-1 个圆盘经由A移到 C上
    hanoid(n-1, b, a, c);


public void move(char a, char b) 
    printf("%c->%c\\n", a, b);

进阶题

细胞分裂 有一个细胞 每一个小时分裂一次,一次分裂一个子细胞,第三个小时后会死亡。那么n个小时候有多少细胞?

public int allCells(int n) 
    return aCell(n) + bCell(n) + cCell(n);


/**
 * 第 n 小时 a 状态的细胞数
 */
public int aCell(int n) 
    if(n==1)
        return1;
    else
        return aCell(n-1)+bCell(n-1)+cCell(n-1);
    


/**
 * 第 n 小时 b 状态的细胞数
 */
public int bCell(int n) 
    if(n==1)
        return0;
    else
        return aCell(n-1);
    


/**
 * 第 n 小时 c 状态的细胞数
 */
public int cCell(int n) 
    if(n==1 || n==2)
        return0;
    else
        return bCell(n-1);
    

以上是关于解决递归问题的四部曲-java面试题的主要内容,如果未能解决你的问题,请参考以下文章

用C程序解决汉诺塔问题与青蛙跳台阶问题(递归)

剑指Offer-循环和递归面试题10.2:青蛙跳台阶

Leetcode练习(Python):递归类:面试题 08.06. 汉诺塔问题:在经典汉诺塔问题中,有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序

面试题 08.06. 汉诺塔问题(非递归实现汉诺塔问题)

一些经典的递归题

一些经典的递归题