1.什么是递归

任何调用自身的函数称为递归。用递归方法求解问题,要点在于递归函数调用自身去解决一个规模比原始问题小一些的问题。这个过程称为递归步骤。递归步骤会导致更多的递归调用。因此,保证递归过程能够终止是很重要的。每次函数都会用比原问题规模更小的问题来调用自身。问题随着规模不断变小必须能最终收敛到基本情形。

2.为什么要用递归

递归是从数学领域借鉴过来的一种有用的技术。递归代码通常比迭代代码更加简洁易懂。一般来说,在编译或解释时,循环会转化为递归函数。当任务能够被相似的子任务定义时,采用递归处理十分有效。例如,排序、搜索和遍历等问题往往有更简洁的递归解决方案。

3.递归和内存(可视化)

每次递归调用都在内存中生成一个新的函数副本(实际上仅仅是一些相关的变量)。一旦函数结束(即返回某些数据),该返回函数的副本就从内存中删除。

4.递归与迭代

在讨论递归的时候,会有一个基本的问题是递归和迭代哪种方法更好?这个问题的答案取决于我们想做什么。递归方法通过类似镜像的方式来解决问题。当问题没有明显的答案时,递归方法通过简化问题来解决它。但是,每次递归调用都会增加开销(栈需要空间开销)。

递归:

  • 当达到基本情形时,递归终止。

  • 每次递归调都需要额外的空间用于栈帧(内存)开销。

  • 如果出现无穷递归,程序可能会耗尽内存,并出现栈溢出。

  • 某些问题采用递归方法更容易解决。

迭代:

  • 当循环条件为假时,迭代终止。

  • 每次迭代不需要任何额外的空间开销。

  • 由于没有额外的空间开销,所以若出现死循环,则程序会一直循环执行。

  • 采用迭代求解问题可能没有递归解决方案那样显而易见

5.递归说明

  • 递归算法有两类情形:递归情形和基本情形。

  • 每个递归函数必须终止于基本情形。

  • 通常,迭代解决方案比递归解决方案更加有效(因为后者有函数调用的开销)。

  • 一个递归算法可以通过使用栈代替递归函数的方式来实现,但通常是得不偿失的。这意味着任何能用递归求解的问题也能用迭代来求解。

  • 对于某些问题,没有明显的迭代求解算法。

  • 有些问题非常适合用递归来求解,而有些则不适合。

6.递归算法的经典用例

6.1斐波那契数列

斐波那契数列(Fibonacci sequence),又称黄金分割数列、因数学家列昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……在数学上,斐波纳契数列以如下被以递推的方法定义:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用,为此,美国数学会从1963年起出版了以《斐波纳契数列季刊》为名的一份数学杂志,用于专门刊载这方面的研究成果。

public class FibonacciDemo {
​// 全局变量sum1记录第一个方法调用的次数,sum2记录第二个方法调用的次数static int sum1;static int sum2;
​public static void main(String[] args) {// n代表数列的长度 、 a代表数列的第一项 、 b代表数列的第二项int n = 10;int a = 1;int b = 1;long algResult = fibonacci(n);// 使用未优化的递归算法得到值System.out.println("使用未优化的递归算法得到值:" + algResult + "方法调用次数为:" + sum1);// 使用优化的递归算法,减少冗余的方法调用得到的值long algEvoResult = fibonacciEvolution(a, b, n);System.out.println("使用已经优化的递归算法得到值:" + algEvoResult + "方法调用次数为:" + sum2);
​}
​public static long fibonacci(int n) {// 记录调用方法次数sum1++;if (n < 2) {return n;}return fibonacci(n - 1) + fibonacci(n - 2);}
​public static long fibonacciEvolution(int a, int b, int n) {// 记录调用方法次数sum2++;if (n > 2) {return fibonacciEvolution(a + b, a, n - 1);}return a;}
}

6.2阶层

阶乘是基斯顿·卡曼(Christian Kramp,1760~1826)于 1808 年发明的运算符号,是数学术语。一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。自然数n的阶乘写作n!。1808年,基斯顿·卡曼引进这个表示法。亦即n!=1×2×3×...×n。阶乘亦可以递归方式定义:0!=1,n!=(n-1)!×n。

public class FactorialDemo {// 全局变量sum,记录方法调用的次数static int sum;
​public static void main(String[] args) {int n = 10;int result = factorial(n);System.out.println("n的阶乘为:" + result + ";调用方法次数为:" + sum);}
​public static int factorial(int n) {sum++;// 0 和 1 的阶乘都是1if (n == 1 || n == 0) {return 1;}int result = n * factorial(n - 1);return result;}
}

6.3归并排序

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 用途: (速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列,应用见2011年普及复赛第3题“瑞士轮”的标程)

public class MergingSortDemo {
// private static long sum = 0;
​/***  * <pre>*  * 二路归并*  * 原理:将两个有序表合并和一个有序表*  * </pre>*  **  * @param a*  * @param s*  * 第一个有序表的起始下标*  * @param m*  * 第二个有序表的起始下标*  * @param t*  * 第二个有序表的结束下标*  **/private static void merge(int[] a, int s, int m, int t) {int[] tmp = new int[t - s + 1];int i = s, j = m, k = 0;while (i < m && j <= t) {if (a[i] <= a[j]) {tmp[k] = a[i];k++;i++;} else {tmp[k] = a[j];j++;k++;}}while (i < m) {tmp[k] = a[i];i++;k++;}while (j <= t) {tmp[k] = a[j];j++;k++;}System.arraycopy(tmp, 0, a, s, tmp.length);}
​/***  **  * @param a*  * @param s*  * @param len*  * 每次归并的有序集合的长度*/public static void mergeSort(int[] a, int s, int len) {int size = a.length;int mid = size / (len << 1);int c = size & ((len << 1) - 1);// -------归并到只剩一个有序集合的时候结束算法-------//if (mid == 0)return;// ------进行一趟归并排序-------//for (int i = 0; i < mid; ++i) {s = i * 2 * len;merge(a, s, s + len, (len << 1) + s - 1);}// -------将剩下的数和倒数一个有序集合归并-------//if (c != 0)merge(a, size - c - 2 * len, size - c, size - 1);// -------递归执行下一趟归并排序------//mergeSort(a, 0, 2 * len);}
​public static void main(String[] args) {int[] a = new int[]{4, 3, 6, 1, 2, 5};mergeSort(a, 0, 1);for (int i = 0; i < a.length; ++i) {System.out.print(a[i] + " ");}}
}
​

6.4快速排序

快速排序(Quicksort)是对冒泡排序的一种改进。快速排序由C. A. R. Hoare在1962年提出。它的基本思想是: 通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小, 然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

public class QuickSortDemo {
​// 分区private static int partition(int[] arr, int start, int end) {// 得到数组中第一个元素的值int key = arr[start];// 判断数组长度while (start < end) {while (arr[end] >= key && end > start)end--;arr[start] = arr[end];while (arr[start] <= key && end > start)start++;arr[end] = arr[start];
​}arr[start] = key;return start;}
​// 快排public static void quickSort(int arr[], int start, int end) {if (start < end) {int index = partition(arr, start, end);// 递归调用quickSort(arr, start, index - 1);quickSort(arr, index + 1, end);}}
​// 测试public static void main(String[] args) {int arr[] = {12, 32, 41, 90, 8, 68, 44, 31, 33, 9};// start:arr数组第一个元素的下标// end  :arr数组最后一个元素的下标int start = 0;int end = arr.length - 1;quickSort(arr, start, end);System.err.println("排序后为:" + Arrays.toString(arr));
​}
}

6.5二分查找

二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。

public class BinarySearchDemo {
​public static void main(String[] args) {int[] arr = {1, 3, 5, 7, 9, 11};int key = 11;int index = binarySearch(arr, key);System.out.println("二分查找元素的下标为:" + index);int index1 = recursionBinarySearch(arr, key - 2, 0, arr.length - 1);System.out.println("递归二分查找元素的下标为:" + index1);}
​// 二分查找 key为目标元素 返回查找元素的下标public static int binarySearch(int arr[], int key) {int low = 0;int high = arr.length - 1;int middle;
​while (low <= high) {middle = (low + high) / 2;if (arr[middle] > key) {// 比关键字大则关键字在左区域high = middle - 1;} else if (arr[middle] < key) {// 比关键字小则关键字在右区域low = middle + 1;} else {return middle;}}System.err.println("不存在此元素");return -1;}
​// 递归实现二分查找public static int recursionBinarySearch(int[] arr, int key, int low, int high) {
​if (key < arr[low] || key > arr[high] || low > high) {return -1;}
​int middle = (low & high) + ((low ^ high) >> 1);  //初始中间位置if (arr[middle] > key) {//比关键字大则关键字在左区域return recursionBinarySearch(arr, key, low, middle - 1);} else if (arr[middle] < key) {//比关键字小则关键字在右区域return recursionBinarySearch(arr, key, middle + 1, high);} else {return middle;}
​}
}

6.6树的遍历和许多树的问题:中序遍历、前序遍历、后序遍历

package com.wxt.recursive;
​
import com.wxt.pojo.Node;
​
/*** @description: 使用递归方法演示前序、中序、后续遍历* @author: Mr.Wang* @create: 2019-01-04 22:58**/
public class BinaryTree {public Node init() {//注意必须逆序建立,先建立子节点,再逆序往上建立,因为非叶子结点会使用到下面的节点,而初始化是按顺序初始化的,不逆序建立会报错Node J = new Node(8, null, null);Node H = new Node(4, null, null);Node G = new Node(2, null, null);Node F = new Node(7, null, J);Node E = new Node(5, H, null);Node D = new Node(1, null, G);Node C = new Node(9, F, null);Node B = new Node(3, D, E);Node A = new Node(6, B, C);//返回根节点return A;}
​// 得到当前节点public void printNode(Node node) {int data = node.getData();String result = String.valueOf(data);System.out.print(result);}
​// The former sequence traversal - 前序遍历public void firstTraversal(Node root) {printNode(root);// 使用递归算法遍历左节点if (root.getLeftNode() != null) {firstTraversal(root.getLeftNode());}// 使用递归算法遍历右节点if (root.getRightNode() != null) {firstTraversal(root.getRightNode());}}
​// In the sequence traversal - 中序遍历public void inOtherTraversal(Node root) {if (root.getRightNode() != null) {inOtherTraversal(root.getRightNode());}
​printNode(root);
​if (root.getLeftNode() != null) {inOtherTraversal(root.getLeftNode());}
​}
​// The former sequence traversal - 后续遍历public void postOtherTraversal(Node root) {if (root.getRightNode() != null) {postOtherTraversal(root.getRightNode());}if (root.getLeftNode() != null) {postOtherTraversal(root.getLeftNode());}printNode(root);}
​public static void main(String[] args) {BinaryTree tree = new BinaryTree();// 初始化二叉树Node root = tree.init();System.out.println("先序遍历:");tree.firstTraversal(root);System.out.println("");System.out.println("中序遍历:");tree.inOtherTraversal(root);System.out.println("");System.out.println("后序遍历:");tree.postOtherTraversal(root);System.out.println("");}
​
}

你真的理解什么是“递归”吗相关推荐

  1. 你真的理解【函数式编程】吗?

    本文来自作者 李龙生 在 GitChat 上分享「你真的理解函数式编程吗?」,「阅读原文」查看交流实录 「文末高能」 编辑 | 克拉克 前言 现在机器学习.人工智能的发展趋势如火如萘,很多培训班也在引 ...

  2. 你真的理解“吃亏是福”么?

    你真的理解"吃亏是福"么?且看 一个10几年的运维老鸟老男孩的随笔! 供朋友参考! 一定不要计较一时的得失! 有一次老男孩老师看电视,一位老大爷(当时70岁)感慨的说," ...

  3. TCP 三次握手原理,你真的理解吗

    转载自  TCP 三次握手原理,你真的理解吗 最近,阿里中间件小哥哥蛰剑碰到一个问题--client端连接服务器总是抛异常.在反复定位分析.并查阅各种资料文章搞懂后,他发现没有文章把这两个队列以及怎么 ...

  4. 简单人物画像_你真的理解用户画像吗?| 船说

    " 「设计师沙龙」是ARK下半年开始逐渐形成的传统,由ARKers自发组织,分为视觉和交互两类,每月各举办一次.大家围绕一个话题展开,聊聊行业最新案例和工作上的心得,帮助大家共同进步. AR ...

  5. [转载] Java内存管理-你真的理解Java中的数据类型吗(十)

    参考链接: Java中的字符串类String 1 做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 推荐阅读 第一季 0.Java的线程安全.单例模式.JVM内存结构等知识 ...

  6. 程序员你真的理解final关键字吗?

    文章目录 1.修饰类 2.修饰方法 3.修饰变量 4.final变量修饰变量(成员变量.局部变量) 4.1 final修饰成员变量: 4.2 final修饰局部变量: 5.final变量和普通变量的区 ...

  7. 史上最容易理解的暴力递归和动态规划~~

    史上最容易理解的暴力递归和动态规划~~ 介绍递归和动态规划 暴力递归: 1, 把问题转化为规模缩小了的同类问题的子问题 2, 有明确的不需要继续进行递归的条件(base case) 3, 有当得到了子 ...

  8. linux 运行长后内存就满了,关于Linux 内存的使用,你确定真的理解吗?

    原标题:关于Linux 内存的使用,你确定真的理解吗? 作者:coloriy

  9. 您真的理解了SQLSERVER的日志链了吗?

    您真的理解了SQLSERVER的日志链了吗? 先感谢宋沄剑给本人指点迷津,还有郭忠辉童鞋今天在QQ群里抛出的问题 这个问题跟宋沄剑讨论了三天,再次感谢宋沄剑 一直以来,SQLSERVER提供了一个非常 ...

最新文章

  1. Verilog初级教程(3)Verilog 数据类型
  2. 大物实验总结模板_高考化学实验题答题模板归类总结!
  3. 微信小程序服务(功能)直达是什么?有什么作用?
  4. software engineering interview domain
  5. mybatis关系映射(1对1,1对多,多对多)
  6. OpenCV添加(混合)两个图像
  7. C#反射设置属性值和获取属性值
  8. 写一本Linux内核方面的书籍
  9. Word技巧:快速将不同字数的姓名对齐
  10. (休息几天)读曼昆之微观经济学——税收归宿
  11. Pandas一键爬取解析代理IP与代理IP池的维护
  12. 浅谈“从神经网络——迁移学习引发的一系列的思考”
  13. 华侨大学计算机等级,全国计算机等级考试报名系统-华侨大学.doc
  14. 想转行学IT!0基础应该要学习哪个技术
  15. 单片机:红外遥控实验(内含红外遥控介绍+硬件原理+软件编程+配置环境)
  16. H3CNE、H3CSE考试总结
  17. 开发板BMP图片显示(6818开发板)
  18. vue3使用useMouseInElement实现图片局部放大预览效果
  19. 用IP地址签发SSL证书
  20. Fenix:Mozilla推出的新型移动浏览器

热门文章

  1. matplotlib一维散点分布图的绘制
  2. 【华为机试真题 Python】能量消耗
  3. erput实现学生成绩管理系统
  4. c语言学生选课系统word,Linux环境下C语言《学生选课管理系统》全源代码,含流程图...
  5. 2019年7月中国奶业经济月报
  6. Vue前端表格导出Excel文件
  7. 犀牛书阅读笔记(第二章)
  8. typeinfo、typeid、 typeof 介绍
  9. easyswoole 数据库相关操作集合大复盘
  10. riscv linux 参考启动流程