递归

本文主要内容有:

1、递归的样子
2、递归简介
3、递归特点
4、递归分析方法
5、递归程序模板
6、递归程序调试
7、总结
8、例子解答

递归的样子

递归,顾名思义,分为两个部分一是递,二是归。
去的过程叫“递”,回来的过程叫“归”,递的开始是问题的开始,递的终止就是归的开始,归的开始是递的终止,归的终止是问题的开始。好吧,已经晕了,但这就是真实的它。

递归代码举例
这段代码没有什么特别的含义,只是为了说了这段代码的执行过程。
递归代码一般分为三个部分:

1、终止条件,因为没有终止条件,代码将会进入死循环。
2、递的阶段,递归函数参数重新计算,参数规模要越来越小。从上往下执行,直到终止条件。
3、归的阶段,在调用本身函数后执行的代码。从终止条件往上执行。

public void f(int number){
   // 1、终止条件
  if(number <=1){
     return ;
  }
  // 递,重新计算参数
  f(number-3);
  //归1
  f(number-2);
  // 归2
  f(number-1);
   //归3
}

看下这段代码的执行过程假如求f(6)
递:从上往下


递.png

归:从下往上


归.png

递归是可以树结构一一对应的,我们可以用树码转换来进行问题分析

简介

递归是程序结构中最复杂(如上图例子)的一种形式之一,递归程序的执行过程分为两步分,先递、后归。研究表明普通人一般最多能处理7个信息,递归的复杂度已经远远超出了人类处理信的能力了,所以递归理解起来非常费劲, 也是很难掌握的一个编程技能。递归除了本身的复杂性外, 还有另外一个复杂性,如何将待处理的问题,抽象处递推公式。

虽然我们不能再脑海中,演算递归的执行过程,但是它都是做重复相同的工作。这块恰恰是计算机擅长处理的事情。我们只需要告诉计算从哪儿开始,在哪儿结束,不用关心它详细的执行的过程。对于复杂问题的抽象,这个复杂性只能由人类来完成了。人类可以可以通过一些技巧,或者工具,找出问题的规律性。从而总结出 模型或者公式。有了计算模型和公式,我们写递归程序就相对容易些。计算机怎么完成这个工作的话,就让它处理吧。从心里上面不要惧怕递归,敢于正视他,我们的重点应该放在问题的抽象上面,而不是复杂程序过程里面。

递归特点

1、可以从上往下把一个大问题拆分为小问题,问题是可以递的。

2、拆分问题和汇总的过程是重复的。

3、存在最小问题的解决办法(终止条件)。

4、问题解决是从下往上一步步解决的。

递归分析方法

1、依据问题描述抽象出递推公式
例如斐波拉契数列:1、1、2、3、5、8、13、21、34、……
公式为 f(n) = f(n-1)+f(n-2) 并且 f(1)=1,f(2)=2

2、递推公式翻译成代码

int fib(int i) {
 // 1、终止条件
 if (i == 1 || i == 2) {
   return 1;
 }
 // 2、递   减小问题规模 ,调用函数的参数值变小
 int result1= fib(i - 1);
 int result2= fib(i-2);
  // 3、归
 return result1 +result2 ;
 
 // 简写 return fib(i - 1)+fib(i-2)
}

3、使用树码分析执行流程

分析我们的问题是在递的部分解决,还是需要在归的部分解决。比如穷举可能性,这种问题一般是在递的部分解决。比如要求一个最终值,那么是在归的部分解决。对照 "递归的样子" 中递归固定的代码模板,进行代码填充。

递归函数本身调用的次数对应的是树有几个分支,参见 "递归的样子" 递与归的部分。比如fib 调用了本身两次,树有两个分支。如下图:

f(n).png

递归程序模板

递归程序一般有三种用途1、求问题的最终解。2、记录解决的详细过程。3、记录每一个子问题的结果。这三种情况都有固定的代码格式和相似的函数参数返回值设计。

1、求问题的最终解

1.1斐波拉契数列

递推公式如下:
f(n) = f(n-1)+f(n-2) ,f(1)=1,f(2)=2

代码

 static int fib(int i) {
  // 1、终止条件
    if (i == 1 || i == 2) {
      return 1;
    }
    // 2、递   减小问题规模 ,调用函数的参数值变小
    int result1= fib(i - 1);
    int result2= fib(i-2);
     // 3、归
    return result1 +result2 ;
    
    // 简写 return fib(i - 1)+fib(i-2)
  }

1.2 求全排列总数

f(n)=f(n-1)*n ,1!=1

 static int permutate(int number) {
    // 1、终止条件
    if (number == 1) {
      return 1;
    }
    // 递,减小问题规模 ,调用函数的参数值变小
    int result=permutate(number - 1); 
   
    //  归,将所有值汇总
    return result * number;
  }

1.3 归并排序

归并排序.png
 public static int[] merge_sort(int[] to_sort) {
    // 1、终止条件
    if (to_sort == null) {
      return new int[0];
    }

    // 如果分解到只剩一个数,返回该数
    if (to_sort.length == 1) {
      return to_sort;
    }

    // 递,简化规模
    // 将数组分解成左右两半
    int mid = to_sort.length / 2;
    int[] left = Arrays.copyOfRange(to_sort, 0, mid);
    int[] right = Arrays.copyOfRange(to_sort, mid, to_sort.length);

    // 嵌套调用,对两半分别进行排序
    left = merge_sort(left);
    right = merge_sort(right);

    // 归,合并排序后的两半
    int[] merged = merge(left, right);
    return merged;
  }

小结:
递归程序是有固定的代码格式的,如我们第一个递归一样 见“递归的样子” 从第一个到后面几个他们的格式都是一样的,说明递归代码格式是有章可循的。求问题的最终解,这样的解决方法重点在归部分的代码。

练习例子

1、假如这里有 n 个台阶,每次你可以跨 1 个台阶或者 2 个台阶,请问走这 n 个台阶有多少种走法?

2、有1,2,5,10 的钱币,能够组成10元有多少中方式。

2、记录解决的详细过程

1、求全排列所有可能情况

static void permutateDetail(ArrayList<Integer> numbers,ArrayList<Integer> result) {
    // 终止条件
    if(numbers.size()==0){
      System.out.println(result);
    }
     // 路径详情在递的阶段设置
    for (int i = 0; i < numbers.size(); i++) {
      Integer integer = numbers.get(i);
      ArrayList<Integer> tempResult= new ArrayList<>();
      tempResult.add(integer);
      tempResult.addAll(result);
      ArrayList numberBack = (ArrayList<Integer>)numbers.clone();
      numberBack.remove(i);
      permutateDetail(numberBack,tempResult);
    }

这种解题思路是求递归树上面,从根节点到叶子节点所有的路径。这种情况我们可以在递的阶段来处理,递阶段主要工作就是参数计算,我们每次调用递归函数的时候,用一个新的参数,新的参数添加上一次调用的节点值。在递结束阶段进行搜集,也就是终止条件处,这样就能够求出所有路径。可以画树图来进行分析。


全排列.png

练习

1、假如这里有 n 个台阶,每次你可以跨 1 个台阶或者 2 个台阶,请问走这 n 个台阶的所有走法?

2、有1,2,5,10 的钱币,能够组成10元的组合明细。

3、记录每一个子问题的结果

1、斐波拉契数列所有节点

static int fibDetail(int i, Map<Integer, Integer> history) {
 if (history.containsKey(i)) {
   return history.get(i);
 }
 if (i == 1 || i == 2) {
   // 第一步返回值->归
   // 最后一步递
   history.put(i, 1);
   return 1;
 }
 // 递拆分过程,改变参数值
 int fi = fibDetail(i - 1, history) + fibDetail(i - 2, history);
 // 返回值->归
 history.put(i, fi);
 return history.get(i);
}

2、二叉树的先中后序遍历
测试数据

public static TreeNode createTree() {
    TreeNode node0 = new TreeNode(0);
    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);
    TreeNode node7 = new TreeNode(7);

    node0.left = node1;
    node0.right = node2;

    node1.left= node3;
    node1.right=node4;

    node2.left=node5;
    node2.right=node6;

    node4.left= node7;

    return node0;
  }


  static class TreeNode {

    int val;
    TreeNode left;
    TreeNode right;

    TreeNode(int x) {
      val = x;
    }
  }

前中后序遍历名称以访问根节点的顺序来命名。

前序:根结点 ---> 左子树 ---> 右子树
中序:左子树 --->根结点---> 右子树
后续:左子树 --->右子树---> 根结点

前序:

  /**
   * 前序遍历,根结点 第一个 前序遍历:根结点 ---> 左子树 ---> 右子树
   * 在递的阶段处理
   *
   */
  static void preOrderTraversal(TreeNode root, ArrayList<Integer> nodeCollect) {
    // 终止条件 ,左右子节点都为空。
    if (root.left == null && root.right == null) {
        nodeCollect.add(root.val);
        return;
    }

    // 分的时候添加每个节点的值
    nodeCollect.add(root.val);

    if(root.left!=null){
      preOrderTraversal(root.left, nodeCollect);

    }

    if(root.right!=null){
      preOrderTraversal(root.right, nodeCollect);
    }

  }

中序:

/**
   * 中序遍历,根结点 在中间 中序遍历:左子树---> 根结点 ---> 右子树
   * 在第一部分归中处理。
   */
  static void inOrderTraversal(TreeNode root, ArrayList<Integer> nodeCollect) {
    // 终止条件一样 ,左右子节点都为空。
    if (root.left == null && root.right == null) {
      nodeCollect.add(root.val);
      return;
    }
    if(root.left!=null){
      inOrderTraversal(root.left, nodeCollect);
    }
    nodeCollect.add(root.val);
    if(root.right!=null){
      inOrderTraversal(root.right, nodeCollect);
    }


  }

后续:

 /**
   * 后续遍历,根结点最后 后序遍历:左子树 ---> 右子树 ---> 根结点
   * 在所有归完成后处理。
   */
  static void postOrderTraversal(TreeNode root, ArrayList<Integer> nodeCollect) {
    // 终止条件一样 ,左右子节点都为空。
    if (root.left == null && root.right == null) {
      nodeCollect.add(root.val);
      return;
    }
    if(root.left!=null){
      postOrderTraversal(root.left, nodeCollect);
    }

    if(root.right!=null){
      postOrderTraversal(root.right, nodeCollect);
    }
     nodeCollect.add(root.val);
  }

小结:
这类问题,我们的函数参数被设计为一个节点收集器,每一次处理一个节点就添加到收集器中。程序执行完成后,收集器中就是所有节点的信息。斐波拉契数列所有节点 中由于有重复的节点,所以需要去重。

递归程序调试

递归程序的调试出来打断点,还应应该记录日志。因为断点是一个局部信息,日志可以记录程序执行详细的过程,从全局的观点看,更容易发现问题。将代码转换成树图来进行分析,看下逻辑上面能不能走的通。用一个很小的规模自己在树图上面来推演。验证递归公式的正确性。记得,几次递归调用就会对应几个树分支。

总结

递归的难点不在程序执行,而在怎么把问题抽象出正确的递推公式。需要一定的归纳总结能力,这没有什么技巧,只有多练习。递归的优点是,利用计算机的计算能力,能够解决很多复杂的问题,如果有递推公式,可以按照固定的格式直接将递推公式翻译成代码。递归的缺点,有重复计算的可能性。调用的方法栈会太长,可能函数栈溢出。循环的性能要高于递归,减少了栈调用。尾递归,一般可以转换成循环。能用循环解决的问题,优先选择循环。

递归有比较通用的代码格式和固定的编程技巧,比如怎么设计函数的参数,返回值,都有一定方法可寻,见前面三种情况。

例子解答

1、假如这里有 n 个台阶,每次你可以跨 1 个台阶或者 2 个台阶,请问走这 n 个台阶有多少种走法?

 static int step(int n) {
    if (n == 1) {
      return 1;
    }
    if (n == 2) {
      return 2;
    }
    int k1 = step(n - 1);
    int k2 = step(n - 2);
    return k1 + k2;
  }

2、有1,2,5,10 的钱币,能够组成10元有多少中方式?

 static int reward(int total){
    if(total==0){
      return 1;
    }else if (total<0){
      return 0;
    }
    // f(n)=f(n-10)+f(n-5)+f(n-2)+f(n-1)
    // f(0)=1
    return reward(total-1)+reward(total-2)+reward(total-5)+reward(total-10);
  }

3、假如这里有 n 个台阶,每次你可以跨 1 个台阶或者 2 个台阶,请问走这 n 个台阶的所有走法?

 static void stepDetail(int n,ArrayList<Integer> perDetail) {
    if (n == 1) {
      perDetail.add(1);
      System.out.println(perDetail);
      return ;
    }
    if (n == 2) {
      ArrayList list1 = (ArrayList<Integer>)perDetail.clone();
      list1.add(1);
      list1.add(1);
      System.out.println(list1);

      ArrayList list2 = (ArrayList<Integer>)perDetail.clone();
      list2.add(2);
      System.out.println(list2);
      return ;
    }

    ArrayList list1 = new ArrayList();
    list1.add(1);
    list1.addAll(perDetail);

    ArrayList list2 = new ArrayList();
    list2.add(2);
    list2.addAll(perDetail);

    stepDetail(n-1,list1);
    stepDetail(n-2,list2);
  }

4、有1,2,5,10 的钱币,能够组成10元的所有组合。

 static int rewardDetail(int total, List<Integer> perDetail){
    if(total==0){
      System.out.println(perDetail);
      return 1;
    }else if (total<0){
      return 0;
    }
    int totalCount=0;
    // f(n)=f(n-10)+f(n-5)+f(n-2)+f(n-1)
    // f(0)=1
    // 递阶段拆分参数,记录拆分值。
    List<Integer> y1List= new ArrayList<>();
    y1List.add(1);
    y1List.addAll(perDetail);
    List<Integer> y2List= new ArrayList<>();
    y2List.add(2);
    y2List.addAll(perDetail);
    List<Integer> y5List= new ArrayList<>();
    y5List.add(5);
    y5List.addAll(perDetail);
    List<Integer> y10List= new ArrayList<>();
    y10List.add(10);
    y10List.addAll(perDetail);

    int y1= rewardDetail(total-1,y1List);
    int y2= rewardDetail(total-2,y2List);
    int y5= rewardDetail(total-5,y5List);
    int y10= rewardDetail(total-10,y10List);
    return y1+y2+y5+y10;
  }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270