[数据结构与算法] 七月在线-第5课-递归

本文为作者参加线上课程的学习笔记。

递归定义

  • 直接或间接调用自身
  • 算法思想:
    • 原问题可分解为子问题之和(必要条件)
    • 原问题与分解后的子问题相似(递归方程)
    • 分解次数有限(子问题有穷)
    • 最终问题可直接解决(递归边界)

递归奥义

  • 递归 = 递 + 归
  • 会想还要会写 -> 实践出真知
  • 递归奥义:复制自己

递:往深处走,往远处走

归:走回来

//递归框架
int robot(int x, int y) { // 机器人的输入
  if (边界条件) {	// 什么时候不用造了(自己就能干完)
    return 0;
  }
  int a = robot(x1, y1); // 造一个小的自己帮忙干活
  int b = robot(x2, y2);	// 再造一个小的自己帮忙干活
  return a + b; // 自己要做的就是把别人的成果组装起来
}

斐波那契数列

  • 递归方程
    • F(n) = F(n - 1) + F(n - 2) // 可分解,有穷性
  • 递归边界
    • F(0) = 1, F(1) = 1
  • 经典例子
    • 兔子生兔子
    • 爬楼梯
public class Solution {
  public static int ClimbStairs(int n) {
    if (n == 0) {
      return 1;
    }
    
    if (n == 1) {
      return 1;
    }
    
    int a = ClimbStairs(n - 1);
    int b = ClimbStairs(n - 2);
    return a + b;
  }
  public static void main(String[] args) throws Exception {
    System.out.println(ClimbStairs(50));
  } 
}

电脑每秒可以计算一亿次?

  • 直接递归

    时间复杂度预估

  • 递归优化

    不做重复的事(记忆化搜索)

    优化方式:将重复的地方记录下来。

  • 递归转递推

    递归式?递推式?仅仅是方向不一样。

  • 为什么用递归?

    方便而已!!特别好用

// 原始递归的优化(记忆化递归)
public class Solution {
  public static long arr[] = new long[100]; 
  
  public static long ClimbStairs(int n) {
    if (n == 0) {
      return 1;
    }
    
    if (n == 1) {
      return 1;
    }
    
    if (arr[n] > 0) {
      return arr[n];
    }
 
    long a = ClimbStairs(n - 1);
    long b = ClimbStairs(n - 2);
    f[n] = a + b;
    return a + b;
  }
  public static void main(String[] args) throws Exception {
    System.out.println(ClimbStairs(50));
  } 
}
// cpp版本的记忆化递归
class Solution {
public:
    int help(int n, std::vector<int>& dp) { // 这里要注意:dp为传引用
        if (n == 1 || n ==0) {
            return 1;
        }
        
        if (dp[n] != 0) {
            return dp[n];
        }
        
        int one_step = help(n - 1, dp);
        int two_step = help(n - 2, dp);
        dp[n] = one_step + two_step;
        
        return dp[n];        
    }
    
    int climbStairs(int n) {
        std::vector<int> dp(n + 1, 0);
        return help(n, dp); 
    }
};

从小到大:递推(bottom up)

从大到小:递归(top down)

注意:递归需要消耗栈空间,递归太深,会把递归栈爆掉。

幂运算

  • 求a^n
  • 递推公式:F(n) = aF(n-1), F(0) = 1
public class Solution {  
  public static double pow(double x, int n) {
    if (n == 0) {
      return 1;
    }
    
    double sub = pow(x, n >> 1);
    if (n & 1) {
      return sub * sub * x;
    } else {
      return sub * sub;
    }
  }
  public static void main(String[] args) throws Exception {
    System.out.println(pow(1, 50));
  } 
}

汉诺塔

三根柱子A、B、C。要求将A上的所有圆盘放在C上。

问:最短移动次数,以及移动方案(哪个盘子从哪个柱子移动到哪个柱子)

本质:需要将A中最下方的大盘子,移动到C柱。

归纳法如下:

1:

A C:直接从A移到C

2:

A B:A的一块,移到B

A C:A最下面一块,移到C

B C:B上一块移到C。

关注:

  • 最后一片盘子在什么时候移动到最后一根柱子?

  • 那最后盘子上面的盘子在哪里?

最终状态:

A只有一个盘子,C是空的。而其他盘子一定在B上。并且在B上的盘子一定是从大到小摆放的。

然后再将B的所有盘子移动到C。

如何让所有的盘子都在B上?让A-1都移到B上,然后A的1移到C上,然后B上所有移到C(和从A移到C本质相同,即为子问题)。

3:

A B:A最上面n-1块,移到B

A C:A最下面1块,移到C

B C:B的所有块移到C。

总结:三根柱子:起点,终点,辅助。从起点出发,利用辅助,到达终点。

递归和数学归纳法,是一致的。

// 汉诺塔问题-递推公式
// f(n)= 2f(n-1) + 1

class Solution1 {
 public:
  void hanota(int n, char A, char B, char C) {
    if (n == 0) {
      return; // 盘子为空,直接return
    }
    hanota(n - 1, A, C, B); // A通过C,移到B(需要多步,所以递归移动)
    printf("%d \t %c \t %c\n", n, A, C); // 从A移动到C(只要一步,所以无需递归)
    hanota(n - 1, B, A, C); // B通过A,移动到C(需要多步,所以递归移动)
  }
};


class Solution2 {
 public:
  void help(int n, char A, char B, char C, int& count) {
    if (n == 0) {
      return; // 盘子为空,直接return
    }
    help(n - 1, A, C, B, count); // A 通过C,移到B
    count++; // 计数器
    help(n - 1, B, A, C, count); // B通过A,移动到C
  }

  void hanota(int n, char A, char B, char C) {
    int count = 0;
    help(n, A, B, C, count);
    printf("count = %d", count); // 打印移动次数
  }
};

难点

递归时,如何记录状态??

递归时,不可以使用全局变量记录参数,必须把参数传进去。因为不同的递归,内部的参数一定是不一样的,因为它们需要完成不用的任务。

快速排序

  • 期望时间复杂度O(NlogN)

  • 思想:

    1. 找任意一个元素作为中间值m

    2. 比m小的放在数组前部,大的放后部

    3. 前部和后部分别排序(递归)

class Solution {
 public:
  static int partition(int left , int right, vector<int>& arr) { // 注意:传引用
    int mid = arr[left];
    while (left < right) {
      while (left < right && arr[right] >= mid) {
        right--;
      }
      // 此时right指向的元素的值小于mid,交换即可
      if (left < right) { // 注意这里的if条件,必须要有
        arr[left] = arr[right];
        left++;
      }
			
      while (left < right && arr[left] <= mid) {
        left++;
      }
      // 此时left指向的元素的值大于mid,交换即可
      if (left < right) {
        arr[right] = arr[left];
        right--;
      }
    }
    arr[left] = mid;

    return left;
  }

  // recursion
  void quickSort(int left, int right, vector<int>& arr) {

    // recursion terminator
    if (left >= right) {
      return;
    }

    // 一次partition + 两次quickSort
    int pivot = partition(left, right, arr);
    quickSort(left, pivot - 1, arr);
    quickSort(pivot + 1, right, arr);
  }

  vector<int> sortArray(vector<int>& nums) {
    int left = 0;
    int right = nums.size() - 1;
    quickSort(left, right, nums);
    return nums;
  }
};

二叉树

为什么二叉树存在三种遍历顺序:为了可以从三种序中正确恢复出唯一确定的二叉树。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值