欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

DSA_07:递归算法

程序员文章站 2022-03-25 16:29:48
前面讲解了复杂度分析、数组、链表、栈、队列,今天讲解递归算法。 递归,可以说同前面这些基础数据结构一般用途广泛,在诸多算法中,这是非常基础,常用的。 简单举几个例子: 1. 二叉树前序、中序、后续遍历可用递归。 2. 深度优先算法可以递归,如迷宫寻路、迷宫生成等。 3. 快排、归并排序可用递归。 4 ......

前面讲解了复杂度分析、数组、链表、栈、队列,今天讲解递归算法。

 

递归,可以说同前面这些基础数据结构一般用途广泛,在诸多算法中,这是非常基础,常用的。

简单举几个例子:

  1. 二叉树前序、中序、后续遍历可用递归。

  2. 深度优先算法可以递归,如迷宫寻路、迷宫生成等。

  3. 快排、归并排序可用递归。

  4. 二分查找可用递归。

  5. 解决实际问题时可用递归。

同样,递归的用途也是多不胜数。想要正确、灵活去应用该算法,需要掌握其一些特性。

 

本文通过一个基础的例子学习该算法,并给出该算法的各种特性技巧。

 

例:这里有 n 阶台阶,你一次可以跨越 1阶、2阶 或 3阶,那么,你有多少种不同的走法走完这 n 阶台阶呢?

如果你曾遇到过该题,你会觉得非常简单。但是如果你不会递归,你会觉得非常困难,用人脑几乎不可能想清楚,后面会给出解释。

 

这里直接给出方法:

  走完这 n 阶有多少种走法?然后你一次可以跨越 1~3 阶,令走完这 n 阶有 f(n) 种方法。

  则走到 n 阶的路径是否等于:走到 n-1 阶的方法 + 走到 n-2 阶的方法 + 走到 n-3 阶的方法?

  当然是的,这里或许需要花费时间细细品味一下。

  你可以理解:走到 n 阶 可以从 n-1 阶上去,也可从 n-2 阶上去,还可以从 n-3 阶上去,这样说是否理解了呢?

 

因此有 f(n) = f(n-1) + f(n-2) + f(n-3)。

终止条件呢:f(1) = 1,f(2) = 2,f(3) = 4。相信这能够想清楚。

因此有下面代码:

int fn(int n)
{
    if (n == 1) return 1;
    if (n == 2) return 2;
    if (n == 3) return 4;
    if (n <= 0) return -1;
    return fn(n - 1) + fn(n - 2) + fn(n - 3);
}

现在,让我们看看该算法的分解图:

    DSA_07:递归算法

你是否发现问题了呢?

    f(4) 被重复计算了!!!

随着 n 的增加,被重复计算的会越来越多,性能开销太大!!!

回过头看,为何人脑无法处理过来呢?你可以看出,其复杂是 o(3^n),仅次于阶乘复杂度,你的大脑能承受这样的指数爆炸吗?

 

递归会占用栈内存,对空间的开销非常大,小心爆栈。

递归算法如何优化呢?一方面是将递归用迭代法写,另外可用尾递归优化,这里不再细说。

在力扣等地方刷题时有时间限制,它会给出大量的测试数据,我们有什么办法使得运行时间非常小呢?

就本题而言,如果告诉你,台阶最多 30 阶,请看代码:

class solution
{
public:
    solution() { gen(); }
    int fn(int n)
    {
        if (n <= 0 || n > 30) return -1;
        return val[n - 1];
    }

private:
    int val[30];
    void gen()
    {
        val[0] = 1;
        val[1] = 2;
        val[2] = 4;
        for (int i = 3; i < 30; ++i)
            val[i] = val[i - 1] + val[i - 2] + val[i - 3];
    }
};

这样一来,原本需要递归才能解决的问题,直接变成查表法了,时间空间复杂度一下子均变为 o(1)。

我们看看 30 阶台阶时有多少种走法呢?

  fn(30) = 53798080

超过 5千万,这更加验证了人是无法完成这样庞大的计算的。

 

爬楼梯:力扣第 70 题。

利用查表法,让我们看看战绩:

    DSA_07:递归算法

 

现在总结递归算法及一些需要注意的地方:

  1. 递归需要满足的三个条件:

    a. 一个问题的解可以分解为几个子问题的解。

    b. 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样。

    c. 存在递归终止条件。

  2. 如何编写递归代码:

    a. 找到如何将大问题分解为小问题的规律。

    b. 基于此写出递推公式。

    c. 再推敲终止条件。

    d. 最后将递推公式和终止条件翻译成代码。

  3. 递归代码要警惕堆栈溢出

  4. 怎么将递归代码改写为非递归代码 或 如何优化递归代码

  5. 递归有利有弊:

    是递归代码的表达力很强,写起来非常简洁。

    就是空间复杂度高有堆栈溢出的风险存在重复计算过多的函数调用会耗时较多等问题。

 

关于递归算法到此暂时结束,关于其优化以及将递归转化为非递归本文并未深入讲解,这在以后会多次讲解到。