本文为作者参加线上课程的学习笔记。
递归定义
- 直接或间接调用自身
- 算法思想:
- 原问题可分解为子问题之和(必要条件)
- 原问题与分解后的子问题相似(递归方程)
- 分解次数有限(子问题有穷)
- 最终问题可直接解决(递归边界)
递归奥义
- 递归 = 递 + 归
- 会想还要会写 -> 实践出真知
- 递归奥义:复制自己
递:往深处走,往远处走
归:走回来
//递归框架
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)
-
思想:
-
找任意一个元素作为中间值m
-
比m小的放在数组前部,大的放后部
-
前部和后部分别排序(递归)
-
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;
}
};
二叉树
为什么二叉树存在三种遍历顺序:为了可以从三种序中正确恢复出唯一确定的二叉树。