套路解决递归问题
原文链接:套路解决递归问题
递归解题三部曲
何为递归?程序反复调用自身即是递归。
我自己在刚开始解决递归问题的时候,总是会去纠结这一层函数做了什么,它调用自身后的下一层函数又做了什么…然后就会觉得实现一个递归解法十分复杂,根本就无从下手。
相信很多初学者和我一样,这是一个思维误区,一定要走出来。既然递归是一个反复调用自身的过程,这就说明它每一级的功能都是一样的,因此我们只需要关注一级递归的解决过程即可。
如上图所示,我们需要关心的主要是以下三点:
- 整个递归的终止条件。
- 一级递归需要做什么?
- 应该返回给上一级的返回值是什么?
因此,也就有了我们解递归题的三部曲:
- 找整个递归的终止条件:递归应该在什么时候结束?
- 找返回值:应该给上一级返回什么信息?
- 本级递归应该做什么:在这一级递归中,应该完成什么任务?
一定要理解这3步,这就是以后递归秒杀算法题的依据和思路。
但这么说好像很空,我们来以题目作为例子,看看怎么套这个模版,相信3道题下来,你就能慢慢理解这个模版。之后再解这种套路递归题都能直接秒了。
例1:求二叉树的最大深度
先看一道简单的Leetcode题目: Leetcode 104. 二叉树的最大深度
题目很简单,求二叉树的最大深度,那么直接套递归解题三部曲模版:
- 找终止条件。 什么情况下递归结束?当然是树为空的时候,此时树的深度为0,递归就结束了。
- 找返回值。 应该返回什么?题目求的是树的最大深度,我们需要从每一级得到的信息自然是当前这一级对应的树的最大深度,因此我们的返回值应该是当前树的最大深度,这一步可以结合第三步来看。
- 本级递归应该做什么。 首先,还是强调要走出之前的思维误区,递归后我们眼里的树一定是这个样子的,看下图。此时就三个节点:root、root.left、root.right,其中根据第二步,root.left和root.right分别记录的是root的左右子树的最大深度。那么本级递归应该做什么就很明确了,自然就是在root的左右子树中选择较大的一个,再加上1就是以root为根的子树的最大深度了,然后再返回这个深度即可。
具体Java代码如下:
1 class Solution { 2 public int maxDepth(TreeNode root) { 3 //终止条件:当树为空时结束递归,并返回当前深度0 4 if(root == null){ 5 return 0; 6 } 7 //root的左、右子树的最大深度 8 int leftDepth = maxDepth(root.left); 9 int rightDepth = maxDepth(root.right); 10 //返回的是左右子树的最大深度+1 11 return Math.max(leftDepth, rightDepth) + 1; 12 } 13 }
当足够熟练后,也可以和Leetcode评论区一样,很骚的几行代码搞定问题,让之后的新手看的一脸懵逼(这道题也是我第一次一行代码搞定一道Leetcode题):
1 class Solution { 2 public int maxDepth(TreeNode root) { 3 return root == null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; 4 } 5 }
例2:两两交换链表中的节点
看了一道递归套路解决二叉树的问题后,有点套路搞定递归的感觉了吗?我们再来看一道Leetcode中等难度的链表的问题,掌握套路后这种中等难度的问题真的就是秒:Leetcode 24. 两两交换链表中的节点
直接上三部曲模版:
- 找终止条件。 什么情况下递归终止?没得交换的时候,递归就终止了呗。因此当链表只剩一个节点或者没有节点的时候,自然递归就终止了。
- 找返回值。 我们希望向上一级递归返回什么信息?由于我们的目的是两两交换链表中相邻的节点,因此自然希望交换给上一级递归的是已经完成交换处理,即已经处理好的链表。
- 本级递归应该做什么。 结合第二步,看下图!由于只考虑本级递归,所以这个链表在我们眼里其实也就三个节点:head、head.next、已处理完的链表部分。而本级递归的任务也就是交换这3个节点中的前两个节点,就很easy了。
附上Java代码:
1 class Solution { 2 public ListNode swapPairs(ListNode head) { 3 //终止条件:链表只剩一个节点或者没节点了,没得交换了。返回的是已经处理好的链表 4 if(head == null || head.next == null){ 5 return head; 6 } 7 //一共三个节点:head, next, swapPairs(next.next) 8 //下面的任务便是交换这3个节点中的前两个节点 9 ListNode next = head.next; 10 head.next = swapPairs(next.next); 11 next.next = head; 12 //根据第二步:返回给上一级的是当前已经完成交换后,即处理好了的链表部分 13 return next; 14 } 15 }
相信经过以上2道题,你已经大概理解了这个模版的解题流程了。
例3:平衡二叉树
那么请你先不看以下部分,尝试解决一下这道easy难度的Leetcode题(个人觉得此题比上面的medium难度要难):Leetcode 110. 平衡二叉树
我觉得这个题真的是集合了模版的精髓所在,下面套三部曲模版:
-
找终止条件。 什么情况下递归应该终止?自然是子树为空的时候,空树自然是平衡二叉树了。
-
应该返回什么信息:
为什么我说这个题是集合了模版精髓?正是因为此题的返回值。要知道我们搞这么多花里胡哨的,都是为了能写出正确的递归函数,因此在解这个题的时候,我们就需要思考,我们到底希望返回什么值?
何为平衡二叉树?平衡二叉树即左右两棵子树高度差不大于1的二叉树。而对于一颗树,它是一个平衡二叉树需要满足三个条件:它的左子树是平衡二叉树,它的右子树是平衡二叉树,它的左右子树的高度差不大于1。换句话说:如果它的左子树或右子树不是平衡二叉树,或者它的左右子树高度差大于1,那么它就不是平衡二叉树。
而在我们眼里,这颗二叉树就3个节点:root、left、right。那么我们应该返回什么呢?如果返回一个当前树是否是平衡二叉树的boolean类型的值,那么我只知道left和right这两棵树是否是平衡二叉树,无法得出left和right的高度差是否不大于1,自然也就无法得出root这棵树是否是平衡二叉树了。而如果我返回的是一个平衡二叉树的高度的int类型的值,那么我就只知道两棵树的高度,但无法知道这两棵树是不是平衡二叉树,自然也就没法判断root这棵树是不是平衡二叉树了。
因此,这里我们返回的信息应该是既包含子树的深度的int类型的值,又包含子树是否是平衡二叉树的boolean类型的值。可以单独定义一个ReturnNode类,如下:
1 class ReturnNode{ 2 boolean isB; 3 int depth; 4 //构造方法 5 public ReturnNode(boolean isB, int depth){ 6 this.isB = isB; 7 this.depth = depth; 8 } 9 }
-
本级递归应该做什么。 知道了第二步的返回值后,这一步就很简单了。目前树有三个节点:root,left,right。我们首先判断left子树和right子树是否是平衡二叉树,如果不是则直接返回false。再判断两树高度差是否不大于1,如果大于1也直接返回false。否则说明以root为节点的子树是平衡二叉树,那么就返回true和它的高度。
具体的Java代码如下:
1 class Solution { 2 //这个ReturnNode是参考我描述的递归套路的第二步:思考返回值是什么 3 //一棵树是BST等价于它的左、右俩子树都是BST且俩子树高度差不超过1 4 //因此我认为返回值应该包含当前树是否是BST和当前树的高度这两个信息 5 private class ReturnNode{ 6 boolean isB; 7 int depth; 8 public ReturnNode(int depth, boolean isB){ 9 this.isB = isB; 10 this.depth = depth; 11 } 12 } 13 //主函数 14 public boolean isBalanced(TreeNode root) { 15 return isBST(root).isB; 16 } 17 //参考递归套路的第三部:描述单次执行过程是什么样的 18 //这里的单次执行过程具体如下: 19 //是否终止?->没终止的话,判断是否满足不平衡的三个条件->返回值 20 public ReturnNode isBST(TreeNode root){ 21 if(root == null){ 22 return new ReturnNode(0, true); 23 } 24 //不平衡的情况有3种:左树不平衡、右树不平衡、左树和右树差的绝对值大于1 25 ReturnNode left = isBST(root.left); 26 ReturnNode right = isBST(root.right); 27 if(left.isB == false || right.isB == false){ 28 return new ReturnNode(0, false); 29 } 30 if(Math.abs(left.depth - right.depth) > 1){ 31 return new ReturnNode(0, false); 32 } 33 //不满足上面3种情况,说明平衡了,树的深度为左右俩子树最大深度+1 34 return new ReturnNode(Math.max(left.depth, right.depth) + 1, true); 35 } 36 }
暂时就写这么多啦,作为一个高考语文及格分,大学又学了工科的人,表述能力实在差因此啰啰嗦嗦写了一大堆,希望大家能理解这个很好用的套路。一些可以用这个套路解决的题
下面我再列举几道我在刷题过程中遇到的也是用这个套路秒的题,真的太多了,大部分链表和树的递归题都能这么秒,因为树和链表天生就是适合递归的结构。
我会随时补充,正好大家可以看了上面三个题后可以拿这些题来练练手,看看自己是否能独立快速准确的写出递归解法了。
1 class Solution { 2 public boolean isSymmetric(TreeNode root) { 3 return getRes(root,root); 4 } 5 6 public boolean getRes(TreeNode p, TreeNode q){ 7 if (p == null && q == null){ 8 return true; 9 } 10 if(p == null || q == null){ 11 return false; 12 } 13 return p.val == q.val && getRes(p.left,q.right) && getRes(p.right, q.left); 14 } 15 }
和求最大深度相反
1 public int maxDepth(TreeNode root) { 2 if (root == null) { 3 return 0; 4 } 5 return 1+Math.max(maxDepth(root.left), maxDepth(root.right)); 6 }
求最小深度时将Math.max换成Math.min即可,但要注意如果根节点的左或右子树为空的话是构不成子树的。而最小深度是要求从根节点到子树的。当左或右子树为空时,不符合要求。
class Solution { public int minDepth(TreeNode root) { if(root == null){ return 0; } if (root.left == null && root.right == null) { return 1; } int m = Integer.MAX_VALUE; if(root.left != null){ m = Math.min(minDepth(root.left), m); } if(root.right != null){ m = Math.min(minDepth(root.right), m); } return m+1; } }
Leetcode 226. 翻转二叉树:这个题的备注是最骚的。Mac OS下载神器homebrew的大佬作者去面试谷歌,没做出来这道算法题,然后被谷歌面试官怼了:”我们90%的工程师使用您编写的软件(Homebrew),但是您却无法在面试时在白板上写出翻转二叉树这道题,这太糟糕了。”
1 class Solution { 2 public TreeNode invertTree(TreeNode root) { 3 if(root == null){ 4 return root; 5 } 6 TreeNode left = invertTree(root.left); 7 TreeNode right = invertTree(root.right); 8 root.left = right; 9 root.right = left; 10 return root; 11 } 12 }
1 class Solution { 2 public TreeNode mergeTrees(TreeNode root1, TreeNode root2) { 3 if(root1 == null && root2 == null){ 4 return root1; 5 } 6 if(root1 == null || root2 == null){ 7 return root1 == null ? root2: root1; 8 } 9 root1.val += root2.val; 10 root1.left = mergeTrees(root1.left, root2.left); 11 root1.right = mergeTrees(root1.right, root2.right); 12 return root1; 13 } 14 }
- 树的递归很多时候都可以套路解决,就一个模版,递归套路三部曲:
- 找终止条件:当l>r时,说明数组中已经没元素了,自然当前返回的节点为null。
- 每一级递归返回的信息是什么:返回的应该是当前已经构造好了最大二叉树的root节点。
- 一次递归做了什么:找当前范围为[l,r]的数组中的最大值作为root节点,然后将数组划分成[l,ind-1]和[ind+1,r]两段,并分别构造成root的左右两棵子最大二叉树。
1 class Solution { 2 public TreeNode constructMaximumBinaryTree(int[] nums) { 3 return getTree(nums, 0, nums.length); 4 } 5 6 public TreeNode getTree(int[] nums, int i, int j){ 7 if(i<j){ 8 Pair<Integer, Integer> pair = getNeed(nums, i, j); 9 Integer val = pair.getKey(); 10 Integer ind = pair.getValue(); 11 TreeNode root = new TreeNode(val); 12 root.left = getTree(nums, i, ind); 13 root.right = getTree(nums, ind+1, j); 14 return root; 15 } 16 return null; 17 } 18 19 public Pair<Integer, Integer> getNeed(int[] nums, int i, int j){ 20 int m = nums[i]; 21 int ind = i; 22 for (int k = i; k<j; k++){ 23 if (m<nums[k]){ 24 m = nums[k]; 25 ind = k; 26 } 27 } 28 return new Pair<>(m, ind); 29 } 30 }
递归套路解决链表问题:
- 找终止条件:当head指向链表只剩一个元素的时候,自然是不可能重复的,因此return
- 想想应该返回什么值:应该返回的自然是已经去重的链表的头节点
- 每一步要做什么:宏观上考虑,此时head.next已经指向一个去重的链表了,而根据第二步,我应该返回一个去重的链表的头节点。因此这一步应该做的是判断当前的head和head.next是否相等,如果相等则说明重了,返回head.next,否则返回head
1 class Solution { 2 public ListNode deleteDuplicates(ListNode head) { 3 if(head == null || head.next == null){ 4 return head; 5 } 6 head.next = deleteDuplicates(head.next); 7 if(head.val == head.next.val) head = head.next; 8 return head; 9 } 10 }