递归

  • 思想
  • 什么是递归
  • 递归与循环
  • 区别
  • 优缺点
  • 使用
  • 步骤
  • 优化
  • 哈希表
  • 迭代
  • 例题
  • 斐波那契数列
  • 爬楼梯
  • 合并两个有序链表


思想

什么是递归

  • 递归算法用一句话概括就是:自己不停调用自己,直到问题解决。
  • 它适用于可以将问题不断简化,且子问题和原问题的求解过程类似的问题。

递归与循环

区别

从递归的概念看起来,递归很像循环,都是对一段代码的复用,那么他们的区别在哪里呢?

  1. 递归由“归”过程。递归由return实现将结果“归”到上一层,像一个回旋镖,扔出去,再飞回来;而循环更像是射箭,扔出去,直到达到终止条件。
  2. 递归往往是自顶向下,而循环既可以自顶向下,也可以自底向上。

优缺点

  1. 递归运行效率低,循环运行效率高。因为递归是对函数的复用,而函数放在栈中,所以递归会造成不断的函数入栈,出栈。
  2. 递归易于理解。

使用

那么我们在使用的时候应该选择哪种呢?
递归:

  1. 数据的结构形式是按照递归定义的,比如单链表,二叉树,斐波那契数列等;
  2. 数据的结构形式不是按照递归定义的,但是用递归求解比用循环求解更加简单,比如汉诺塔问题,四重及以上循环问题。

循环:

  1. 数据的结构形式不是按照递归定义的,使用循环就能够轻松解决的问题,比如一重循环、二重循环、三重循环。

步骤

使用递归要从以下两方面着手:

  1. 边界(结束条件)。
    意义:递归会在函数内部代码中,调用这个函数本身。所以,必须要找出递归的结束条件,否则会无限调用。
    实施:我们需要找出当参数为何值时,递归结束,之后直接把结果返回。此时们必须达到根据这个参数的值,可以直接知道函数的结果。
  2. 状态转移方程式。
    意义:每个阶段和下个阶段的关系。
    实施:原函数的一个等价关系式,例如:f(n) = n+f(n-1), f(n) =n * f(n-1)等等。

优化

在递归的使用中,经常会出现重复运算,所以可以进行优化操作。

如:现在有一个问题,结果研究得出状态转移方程式是:F(n)=F(n-1)+F(n-2)。

此时我们的运算是以下情况:

java递归return为什么不是直接结束整个方法 递归用return_递归


其中,F(n-2),F(n-3)等等,都被重复计算了多次,所以我们需要优化。

哈希表

我们可以将第一次运算结果存放进哈希表,在下次运算时直接取出,这样就会避免重复运算。也就是用空间换时间。

迭代

迭代就是知道了一组小规模问题的答案后,就可以用状态转移方程组装成大一点规模问题的答案的做法。也就是自底向上的求解问题。

  • 如上一个图,我们从底部向上移动,可以直接避免重复运算。
  • 自底向上的计算方式还可以优化存储。如利用变量,滚动数组。

例题

斐波那契数列

  • 题目出处
  • 写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。

//优化:
//1.迭代:由下向上,优化掉二叉树的多余节点
//2.滚动数组:优化内存空间
if(n==0){return 0}else if(n==1){return 1}else{          //优化:if(n<2){return n}else{
    var count = Math.floor(n/3)                           //优化:三个三个跳,一个一个跳(各有优劣)
    var myarr = []                                      //滚动数组不是说必须用数组,三个数字就可以
    myarr[0]=0
    myarr[1]=1
    myarr[2]=myarr[0]+myarr[1]
    if(count==0){return myarr[2]}
    for(var i=1;i<=count;i++){     
        myarr[0]=(myarr[1]+myarr[2])%1000000007
        myarr[1]=(myarr[2]+myarr[0])%1000000007
        myarr[2]=(myarr[0]+myarr[1])%1000000007
    }
    if(n%3==0){
        return myarr[0]
    }else if(n%3==1){
        return myarr[1]
    }else{
        return myarr[2]
    }
}
};

爬楼梯

  • 题目出处
  • 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

//看到n,想到与n-1的关系:错,没有理解精髓
//看到n,想到子问题=>此题应为少走一步=>f(n-1)与f(n-2)
//也不应该是看到n就想n-1,而是思考n问题从逻辑上是否与子问题有联系
/*if(n==1){
    return 1
}else if(n==2){
    return 2
}else{
    return climbStairs(n-1)+climbStairs(n-2)
}*/
//时间复杂度:O(2的n)递归本质上是没有性能问题的,之所以出现问题是因为重复递归导致的,这里需要去避免重复递归的问题。
//根据运算过程,写出每一次的运算结果,这是一颗大量重复的二叉树
var climbStairs = function(n) {
	const dp = [];
    dp[0] = 1;
    dp[1] = 1;
    for(let i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
};
//时间复杂度:O(n):对于迭代自下而上的构建二叉树,去掉了所有重复情况,是最简单的二叉树
//「滚动数组思想」可以用来优化空间复杂度

合并两个有序链表

  • 题目出处
  • 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var mergeTwoLists = function(l1, l2) {
    if (l1 === null) {
        return l2;
    } else if (l2 === null) {
        return l1;
    } else if (l1.val < l2.val) {
        l1.next = mergeTwoLists(l1.next, l2);
        return l1;
    } else {
        l2.next = mergeTwoLists(l1, l2.next);
        return l2;
    }
};