【不一样的递归大法】

递归

  • 递归
    • 定义
  • 何时用递归:递归三板斧
    • 递归=递+归
    • 递归大法:三板斧
  • 如何快速写出递归函数:宏观的角度
  • 解题突破
    • 整数序列相关
      • 求阶乘
      • 正序打印数字的所有位
    • 数组相关
      • 数组求和
    • 单链表相关
      • 链表反转
      • 从尾到头打印链表
    • 二叉树相关
      • 二叉树高度
      • 平衡二叉树

递归

定义

在函数执行过程中调用函数自身的程序设计称为递归

举个例子:

public void fib(int n){
	if(n==1){
		return;
	}
	fib(n-1);
}

在这个例子中,fib(int n)方法在自己的方法体中调用了自身,这就是递归调用。

何时用递归:递归三板斧

递归的大体思路是自顶向下将规模较大的原问题不断拆分为规模相对较小的小问题,然后自底向上逐个解决小问题,最后得到原始大问题的解。

递归=递+归

将递归的完整过程可分为“递”和“归”两步:

递:不断拆分问题
归:解决小问题并返回结果

递归函数依据自己不断调用自身的过程,将原始问题拆分为有限个小问题,拆分行为一直进行到递归终止条件,转而处理求解各个小问题并向上返回,最终得到原问题的解。

递归大法:三板斧

由上面的理解我们可以总结出递归的条件,当一个问题满足以下全部条件时,我们即可尝试使用递归的方式去求解:

  • 原始大问题可拆分为多个小问题;
  • 拆分得到的小问题,除了数据规模与原始问题的数据规模不同之外,求解问题的思路完全一致;
  • 子问题拆分是有限的,必须存在递归终止条件:其中,递归终止条件指的是,不借助任何其他条件或函数,就可立即得到该问题的计算结果。

如何快速写出递归函数:宏观的角度

那么我们确定了求解某问题需要使用递归时,如何快速确定递归函数呢?答案是:站在宏观的角度去思考问题,不要纠结于递归内部如何进行。

如何快速写出递归函数:站在宏观的角度,始终把握该函数的语义。
什么是函数的语义:该函数的定义是为了干什么,也就是该函数的目的是什么?
我们在编写递归函数的具体实现时,假设该函数是已经内置好的API,我们需要时直接调用即可。

解题突破

由于递归这种程序设计技巧,巧妙的将大问题不断拆分为小问题,化整为零的特点,其在数据结构相关的很多方面都有着广泛的应用,比如简单的数字序列、数组、链表、二叉树等。下面我针对这些应用分别列出几个示例,以此验证“三板斧”的作用以及“宏观”角度的精妙:

整数序列相关

求阶乘

问题描述:给定一个整数n,计算得到n!,即数字n的阶乘。

三板斧:

  • 原问题可拆解:n!=n*(n-1)!;
  • n!与(n-1)!求解思路完全一致,只不过二者规模大小不同;
  • 拆分是有限的,当n=1时,n!=1,我们可直接得到计算结果;

代码:

int getFactorial(int num) {
	// base case:递归中止条件
    if (num==1){
        return 1;
    }
    // n!=n*(n-1)!,而刚好有一个函数getFactorial可以计算某个整数的阶乘,我们直接调用即可
    return num*getFactorial(num-1);
}

测试结果:
n=3,
在这里插入图片描述

正序打印数字的所有位

问题描述:给定一个整数n,从最高位到最低位逐个输出对应位的数字,比如n=129,输出1 2 9。

三板斧: 假设有一个函数printNum用于逐个打印数字num的每一位

  • 原问题可拆解,比如n=129,可先输出数字12的每一位,然后打印9;
  • 子问题与原问题求解思路完全一致,只不过二者规模大小不同;
  • 拆分是有限的,当n<10时,也就是n为个位数时,可直接输出n;

代码:

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)

  • 原问题可拆解,比如sum(arr,n-1)=arr[n-1]+sum(arr,n-2),即数组元素之和为数组前[0,n-2]元素之和+元素arr[n-1];
  • 子问题与原问题求解思路完全一致,只不过二者规模大小不同;
  • 拆分是有限的,当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)

  • 原问题可拆解,比如reverseList(ListNode head)过程可拆分为两个子过程,1)首先反转以head.next节点为头节点的子链表,即调用reverseList(head.next),返回反转后的子链表的头节点;2)然后将head节点链接在1)中得到的子链表链尾即可即可;
  • 子问题与原问题求解思路完全一致,只不过二者规模大小不同;
  • 拆分是有限的,这里有两个终止条件,满足二者中的一个即可停止递归:1)当head=null,即传入链表为空时,直接返回null;2)head.next=null,即此时传入链表为单节点链表,无论正序还是逆序,该链表都为此节点本身,所以直接返回该节点即可;

代码:

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;
}

测试结果:
【不一样的递归大法】_第1张图片

从尾到头打印链表

问题描述:输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
原题链接https://leetcode.cn/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof/

三板斧: 该问题可表示为 reversePrint(ListNode head)

  • 原问题可拆解,比如reversePrint(ListNode head)过程可拆分为两个子过程,1)首先逆序打印以head.next节点为头节点的子链表,即调用reversePrint(headnext);2)然后打印head节点即可;
  • 子问题与原问题求解思路完全一致,只不过二者规模大小不同;
  • 拆分是有限的,这里有两个终止条件,满足二者中的一个即可停止递归:1)当head=null,即传入链表为空时,直接返回空数组;2)head.next=null,即此时传入链表为单节点链表,无论正序还是逆序打印都为该节点本身,所以直接返回该节点组成的数组即可;

代码:

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)

  • 原问题可拆解,比如maxDepth(TreeNode root) 过程可拆分为三个子过程,1)首先分别计算得到左右子树的高度,即分别调用maxDepth(root.left) 、maxDepth(root.left) ;2)然后取左右子树高度中的最大值,即Max(leftHight,rightHight);3)最后将2)得到的结果+1,即加上root根节点的高度
  • 子问题与原问题求解思路完全一致,只不过二者规模大小不同;
  • 拆分是有限的,这里的递归终止条件为root==null,即传入空树时,则说明高度为0,直接返回0即可;

代码:

public int maxDepth(TreeNode root) {
	// 递归终止条件
    if(root==null){
        return 0;
    }
    // 其他情况
    return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
}

测试结果:
【不一样的递归大法】_第2张图片

平衡二叉树

问题描述:给定一个二叉树,判断它是否是高度平衡的二叉树。本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 ,如下图所示为一棵合法的平衡二叉树。
原题链接https://leetcode.cn/problems/balanced-binary-tree/
【不一样的递归大法】_第3张图片

三板斧: 该问题可表示为 isBalanced(TreeNode root),先来分析一下,如何确定以root为根节点的二叉树为一棵平衡树呢?我们需要去计算其左右子树的高度,当高度差的绝对值超过1时说明该树不是平衡二叉树,反之如果所有子树都满足其左右子树高度差不超过1,则说明以root为根节点的整棵树为平衡二叉树。所以这里需要借助到上一题求解二叉树高度的函数 maxDepth(TreeNode root)。

  • 原问题可拆解,比如isBalanced(TreeNode root)过程可拆分为两个子过程,1)首先判断root为根节点的二叉树是否满足平衡二叉树左右子树高度差绝对值不超过1的限制;2)如果1)满足,则判断root的左右子树是否都满足平衡二叉树的要求;
  • 子问题与原问题求解思路完全一致,只不过二者规模大小不同;
  • 拆分是有限的,这里的递归终止条件为root==null,即传入空树,我们知道空树天然是一棵平衡二叉树,故直接返回true即可;

代码:

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;
}

测试结果:
【不一样的递归大法】_第4张图片

你可能感兴趣的:(Java开发,算法,java,数据结构,链表)