在函数执行过程中调用函数自身的程序设计称为递归。
举个例子:
public void fib(int n){
if(n==1){
return;
}
fib(n-1);
}
在这个例子中,fib(int n)方法在自己的方法体中调用了自身,这就是递归调用。
递归的大体思路是自顶向下将规模较大的原问题不断拆分为规模相对较小的小问题,然后自底向上逐个解决小问题,最后得到原始大问题的解。
将递归的完整过程可分为“递”和“归”两步:
递:不断拆分问题
归:解决小问题并返回结果
递归函数依据自己不断调用自身的过程,将原始问题拆分为有限个小问题,拆分行为一直进行到递归终止条件,转而处理求解各个小问题并向上返回,最终得到原问题的解。
由上面的理解我们可以总结出递归的条件,当一个问题满足以下全部条件时,我们即可尝试使用递归的方式去求解:
那么我们确定了求解某问题需要使用递归时,如何快速确定递归函数呢?答案是:站在宏观的角度去思考问题,不要纠结于递归内部如何进行。
如何快速写出递归函数:站在宏观的角度,始终把握该函数的语义。
什么是函数的语义:该函数的定义是为了干什么,也就是该函数的目的是什么?
我们在编写递归函数的具体实现时,假设该函数是已经内置好的API,我们需要时直接调用即可。
由于递归这种程序设计技巧,巧妙的将大问题不断拆分为小问题,化整为零的特点,其在数据结构相关的很多方面都有着广泛的应用,比如简单的数字序列、数组、链表、二叉树等。下面我针对这些应用分别列出几个示例,以此验证“三板斧”的作用以及“宏观”角度的精妙:
问题描述:给定一个整数n,计算得到n!,即数字n的阶乘。
三板斧:
代码:
int getFactorial(int num) {
// base case:递归中止条件
if (num==1){
return 1;
}
// n!=n*(n-1)!,而刚好有一个函数getFactorial可以计算某个整数的阶乘,我们直接调用即可
return num*getFactorial(num-1);
}
问题描述:给定一个整数n,从最高位到最低位逐个输出对应位的数字,比如n=129,输出1 2 9。
三板斧: 假设有一个函数printNum用于逐个打印数字num的每一位
代码:
void printNum(int num) {
// base case:输入整数为个位数时,可直接输出
if (num<10){
System.out.print(num+" ");
return;
}
// 逐个打印num的个位之外的所有数字
printNum(num/10);
// 打印num的个位数
System.out.print(num%10+" ");
}
问题描述:给定一个整数数组arr,计算并返回该数组的所有元素之和。
三板斧: 设数组长度为n,该问题可表示为sum(arr,n-1)
代码:
int sum(int[] arr, int lastIndex) {
if (lastIndex==0){
return arr[0];
}
return sum(arr,lastIndex-1)+arr[lastIndex];
}
问题描述:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
原题链接https://leetcode.cn/problems/reverse-linked-list/
三板斧: 该问题可表示为 reverseList(ListNode head)
代码:
public ListNode reverseList(ListNode head) {
// 递归
// 终止条件
if(head==null){
return null;
}
if(head.next==null){
return head;
}
// 其他情况
ListNode second=head.next;
head.next=null;
ListNode newHead=reverseList(second);
second.next=head;
return newHead;
}
问题描述:输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
原题链接https://leetcode.cn/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof/
三板斧: 该问题可表示为 reversePrint(ListNode head)
代码:
public int[] reversePrint(ListNode head) {
// 递归,语义:输入链表头节点,逆序打印链表元素
if(head==null){ // 空链表
return new int[0];
}
if(head.next==null){ // 单节点链表
return new int[]{head.val};
}
ListNode second=head.next;
head.next=null;
int[] last=reversePrint(second);
int[] res=Arrays.copyOf(last,last.length+1);
res[res.length-1]=head.val;
return res;
}
测试结果:
注意此题使用递归并不是最优解法,这里只是表达递归的广泛应用。
问题描述:给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。说明: 叶子节点是指没有子节点的节点。
原题链接https://leetcode.cn/problems/maximum-depth-of-binary-tree/
三板斧: 该问题可表示为 maxDepth(TreeNode root)
代码:
public int maxDepth(TreeNode root) {
// 递归终止条件
if(root==null){
return 0;
}
// 其他情况
return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
}
问题描述:给定一个二叉树,判断它是否是高度平衡的二叉树。本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 ,如下图所示为一棵合法的平衡二叉树。
原题链接https://leetcode.cn/problems/balanced-binary-tree/
三板斧: 该问题可表示为 isBalanced(TreeNode root),先来分析一下,如何确定以root为根节点的二叉树为一棵平衡树呢?我们需要去计算其左右子树的高度,当高度差的绝对值超过1时说明该树不是平衡二叉树,反之如果所有子树都满足其左右子树高度差不超过1,则说明以root为根节点的整棵树为平衡二叉树。所以这里需要借助到上一题求解二叉树高度的函数 maxDepth(TreeNode root)。
代码:
public boolean isBalanced(TreeNode root) {
// 递归终止条件:空树天然为一棵平衡树
if(root==null){
return true;
}
// 分别计算左右子树的高度
int leftHeight=maxDepth(root.left);
int rightHeight=maxDepth(root.right);
// 如果root的左右子树高度差绝对值超过1则说明root树不是平衡树
if(Math.abs(leftHeight-rightHeight)>1){
return false;
}
// 否则判断左右子树是否都是平衡树
return isBalanced(root.left)&&isBalanced(root.right);
}
public int maxDepth(TreeNode root) {
if(root==null){
return 0;
}
return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
}