结论:假设一个问题可以分解为若干个子问题A、B、C、D...,且A问题需要在B、C、D...已经解决的基础上求解。
1)我们可以假设B问题已经解决,考虑A如何在B问题已经解决的前提下,如何处理(即寻找递推关系)
2)有了递推关系,我们就可以由A==>B==>C...==>最后一个最基本的子问题,我们处理最基本的子问题(即寻找终止条件)
基本上,所有的递归问题都可以用递推公式来表示。我们不需要按照程序进行的步骤去,一步一步嵌套调用,这样细节问题只会越想越复杂。
例1: (比较明显的能递推公式的问题)
问题:斐波那契数列的第n项
递推公式:
f(n)=f(n-1)+f(n-2) 其中,f(0)=0,f(1)=1
终止条件:
if (n <= 2) return 1;
递归代码:
int f(int n) {
if (n <= 2) return 1;
return f(n-1) + f(n-2);
}
例2:(不那么明显的有递推公式的问题)
问题:逆序打印一个数组
递推公式:
假设令F(n)=逆序遍历长度为n的数组
那么F(n)= 打印数组中下标为n的元素 + F(n-1)
终止条件:
if (n <0) return ;
递归代码:
public void Print(int[] nums,int n){
if(n<0) return;
System.out.println(nums[n]);
Print(nums,n-1);
}
到这里,不知道大家对写递归有没有一些理解了。其实写递归不能总想着去把递归平铺展开,这样脑子里就会循环,一层一层往下调,然后再一层一层返回,试图想搞清楚计算机每一步都是怎么执行的,这样就很容易被绕进去。对于递归代码,这种试图想清楚整个递和归过程的做法,实际上是进入了一个思维误区。只要找到**递推公式**,我们就能很轻松地写出递归代码。
到这里,我想进一步跟大家说明我这个思路是比较能够容易出代码的,那么就树的遍历问题来和大家讲。递归总是和树分不开,其中,最典型的便是树的遍历问题。刚开始学的时候,不知道大家是怎么理解先/中/后序遍历的递归写法的,这里我提供我的思路供参考,以前序遍历为例:
问题:二叉树的先序遍历
递推公式:
令F(Root)为问题:遍历以Root为根节点的二叉树,
令F(Root.left)为问题:遍历以F(Root.left)为根节点的二叉树
令F(Root.right)为问题:遍历以F(Root.right)为根节点的二叉树
那么其递推公式为:
F(Root)=遍历Root节点+F(Root.left)+F(Root.right)
递归代码:
public void preOrder(TreeNode node){
if(node==null) return;
System.out.println(node.val);
preOrder(node.left);
preOrder(node.righr);
}
递归是一种关于某个重复动作(完成重复性的功能)的形式化描述**。具体点讲,如果一个问题 A 可以分解为若干子问题 B、C、D,**你可以假设子问题 B、C、D 已经解决,在此基础上思考如何解决问题 A**。而且,你只需要思考问题 A 与子问题 B、C、D 两层之间的关系即可,不需要一层一层往下思考子问题与子子问题,子子问题与子子子问题之间的关系(也就是说,递归只能考虑当前层和下一层的关系,不能继续往下深入)。我们需要屏蔽掉递归细节,理解为完成了某种功能的形式化描述即可。
好了,那我们来分析这个题。
问题:单向链表的反转
递推公式:
令F(node)为问题:反转以node为头节点的单向链表;
一般,我们需要考虑F(n)和F(n-1)的关系,那么这里,如果n代表以node为头节点的单向链表,那么n-1就代表以node.next为头节点的单向链表.
所以,我们令F(node.next)为问题:反转以node.next为头节点的单向链表;
那么,F(node)和F(node.next)之间的关系是?这里我们来简单画个图,假设我们反转3个节点的链表:
1 -> 2 -> 3
那么,F(node=1)=F(node=2)+?
这里假设子问题F(node=2)已经解决,那么我们如何解决F(node=1):
很明显,我们需要反转node=2和node=1, 即 node.next.next=node; 同时 node.next=null;
所以,这个问题就可以是:F(node=1)=F(node=2)+ 反转node=2和node=1
递归代码:
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode node = reverseList(head.next);//下一个节点已经反转结束
head.next.next = head;//将未反转的节点,放置已经反转的最后
head.next = null;//将最后一个反转的指向null;
return node;
}
后续添加递归的例子:
剑指 Offer 25. 合并两个排序的链表
输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
示例1:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
//递归
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null ){//考虑l1为空的特殊情况
return l2;
}
if(l2 == null){//考虑l2为空的特殊情况
return l1;
}
if(l1.val < l2.val){
l1.next = mergeTwoLists(l1.next,l2);//l1后接已经已经合并好的链表
return l1;
}else{
l2.next = mergeTwoLists(l1,l2.next);//l2后接已经合并号的链表
return l2;
}
}
}
//时间复杂度:O(m+n)
//空间复杂度:O(1)
判断二叉树 A 中是否包含子树 B
题目描述:输入两棵二叉树 A,B,判断 B 是不是 A 的子结构。(ps:我们约定空树不是任意一个树的子结构)
思路:若根节点相等,利用递归比较他们的子树是否相等,若根节点不相等,则 利用递归分别在左右子树中查找。
package Q18;
//判断二叉树 A 中是否包含子树 B
//输入两棵二叉树 A,B,判断 B 是不是 A 的子结构。(ps:我们约定空 树不是任意一个树的子结构)
//思路:若根节点相等,利用递归比较他们的子树是否相等,若根节点不相等,则 利用递归分别在左右子树中查找。
public class Q18Demo {
public static void main(String[] args) {
TreeNode node1 = new TreeNode(1);
TreeNode node2 = new TreeNode(2);
TreeNode node3 = new TreeNode(3);
TreeNode node4 = new TreeNode(4);
TreeNode node5 = new TreeNode(5);
TreeNode node6 = new TreeNode(6);
node1.left = node2;
node1.right = node3;
node2.left = node4;
node3.left = node6;
node3.right = node5;
TreeNode node11 = new TreeNode(3);
TreeNode node12 = new TreeNode(5);
TreeNode node13 = new TreeNode(6);
node11.left = node12;
node11.right = node13;
boolean result;
Solution s = new Solution();
result = s.hasSubTree(node1, node11);
System.out.println(result);
}
}
class Solution {
public boolean hasSubTree(TreeNode source, TreeNode target) {
if(target == null){
return true;
}
if(source == null){
return false;
}
if(doesTree1HaveTree2(source, target)){//如果根节点与target相等,且其左右子树相等。
return true;
}
//否则 遍历其 左右子节点
return hasSubTree(source.left, target) || hasSubTree(source.right, target);
}
public static boolean doesTree1HaveTree2(TreeNode source,TreeNode target){
if(source == null && target == null){//终止条件:两者都为空
return true;
}
if(source == null || target == null){//两者有一个为空,则不含有
return false;
}
if(source.val != target.val){//值不相等,则返回false
return false;
}
return doesTree1HaveTree2(source.left, target.left) && doesTree1HaveTree2(source.right,
target.right);//左子树 和 右子数必须相等
}
}
剑指 Offer 27. 二叉树的镜像
请完成一个函数,输入一个二叉树,该函数输出它的镜像。
例如输入:
4
/ \
2 7
/ \ / \
1 3 6 9
镜像输出:
4
/ \
7 2
/ \ / \
9 6 3 1
示例 1:
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode mirrorTree(TreeNode root) {
if(root == null) return null;
TreeNode tmp = root.left;
root.left = mirrorTree(root.right);
root.right = mirrorTree(tmp);
return root;
}
}
剑指 Offer 33. 二叉搜索树的后序遍历序列
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。
参考以下这颗二叉搜索树:
5
/ \
2 6
/ \
1 3
示例 1:
输入: [1,6,3,2,5]
输出: false
示例 2:
输入: [1,3,2,6,5]
输出: true
class Solution {
//二叉搜索树满足左子节点值小于父节点值,右子节点大于父节点值
//思路通过数组值,来确定哪一颗二叉搜索树
public boolean verifyPostorder(int[] postorder) {
return recur(postorder,0,postorder.length - 1);
}
public boolean recur(int[] postorder,int i,int j){
if(i >= j) return true;
int p = i;//遍历postorder,找到右子树的第一个节点
while(postorder[p] < postorder[j]) p++;
int m = p;//索引m为所要找的右子树第一个节点
while(postorder[p] > postorder[j]) p++;//看其是否满足右子树都大于根节点
return p==j && recur(postorder,i,m-1) && recur(postorder,m,j-1);
}
}