20230511 递归
参考资料
基础
栈
选择数组尾部、链表头部作为栈顶
- 可以保证在操作栈时时间复杂度为 O(1)
- 选择数组头部、链表尾部作为栈顶,时间复杂度是 O(n)
函数调用系统栈
- 函数参数
- 局部变量
- 返回地址:函数执行完成后(本函数调用其他函数返回后),下一个指令的地址
示例:用递归的方式打印数组
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4};
print(arr, 0, arr.length - 1);
}
public static void print(int[] arr, int start, int end) {
// 退出条件
if (start > end) {
return;
}
System.out.println(arr[start]);
print(arr, start + 1, end);
}
理解递归
递归的本质就是函数调用,分为两个过程:递、归,两个过程都可以做一些操作
- 递的过程:划分子问题
- 归的过程:合并结果
递归需要更多的时间和空间开销
为什么使用递归:
- 对于线性数据结构问题,一般使用迭代解决
- 对于非线性数据结构(比如树、图等),使用递归来解决更加简单、更容易理解
什么场景下,可以使用递归?满足以下3个特点的问题可以使用递归
- 大问题可以拆解成若干个子问题
- 大问题和子问题的求解方法是一样的
- 存在结果已知的最小子问题
对于递归的代码,没必要一步步去跟踪它的微观执行步骤,只需要抓住宏观语义即可
宏观语义:递归函数是做什么的
尾递归
函数调用在最后一行,称为尾调用
尾调用可以在编译时优化,被尾调用的函数可以和调用函数使用相同栈帧
public static void main(String[] args) {
System.out.println(factorial(5));
System.out.println(factorialTailRecursive(5, 1));
}
/**
* 计算阶乘
*/
public static int factorial(int n) {
if (n == 1) {
return 1;
}
// 因为递归计算后,还有一步乘以n,所以不是尾递归
return n * factorial(n - 1);
}
/**
* 计算阶乘
* 尾递归
*/
public static int factorialTailRecursive(int n, int sum) {
if (n == 1) {
return sum;
}
return factorialTailRecursive(n - 1, n * sum);
}
在常见数据结构中使用递归
数组、链表
递归一般不用于处理线性结构,一般使用迭代,这里是因为线性结构简单,方便理解
示例:删除链表中的指定节点
/**
* 删除链表中的指定节点
*/
public class Test3 {
public static void main(String[] args) {
ListNode n76 = new ListNode(6);
ListNode n65 = new ListNode(5, n76);
ListNode n54 = new ListNode(4, n65);
ListNode n43 = new ListNode(3, n54);
ListNode n36 = new ListNode(6, n43);
ListNode n22 = new ListNode(2, n36);
ListNode n11 = new ListNode(1, n22);
ListNode head = deleteNode(n11, 6);
while (head != null) {
System.out.println(head.val);
head = head.next;
}
}
/**
* 删除链表中值为val的节点,并返回删除后新的头节点
*
* @param head
* @param val
* @return
*/
public static ListNode deleteNode(ListNode head, int val) {
if (head == null) {
return null;
}
// 递的过程中,分解成子问题
head.next = deleteNode(head.next, val);
// 归的过程中,进行操作
return head.val == val ? head.next : head;
}
private static class ListNode {
int val;
ListNode next;
public ListNode(int val) {
this.val = val;
}
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
}
示例:反转链表
/**
* 反转关系,并返回头节点
*/
public static ListNode reverse(ListNode head) {
if (head.next == null) {
return head;
}
ListNode newHead = reverse(head.next);
// 将next节点的next指针指向自身
head.next.next = head;
// 将自身的next节点设置为null
head.next = null;
return newHead;
}
二叉树
三种遍历方式:前、中、后
DFS 可以解决二叉树 98% 的问题,98% 的二叉树问题本质上都是遍历问题
图
掌握DFS,可以解决90%的图的问题
实际应用
- 快速排序:pivot,大小分区,两个指针,大小交换,在递的过程中进行排序
- 归并排序:二分分区,两个指针,左右比较,临时数组,在归的过程中进行排序
- 汉诺塔问题:讲解的非常好,p23
- 回溯算法思想:示例-找到二叉树的所有路径,
- 全排列:回溯代码框架,穷举,剪枝
- 动态规划:斐波那契数列p26(不是动态规划,只是为了引入记忆化搜索),自底而上的解法是动态规划的经典模板,不断优化斐波那契数列解法