递归在编程世界的表现就是函数自己调用自己,只不过可能传入的参数在变。
递归的本质是栈:

微信公众号:小夕学算法

阶乘计算

  1. n! = 1 * 2 * 3 * 4 * …… * (n-1) * n

循环实现

  1. public static int factorial(int n) {
  2. int factorial = 1;
  3. for (int i = 1; i <= n; i++) {
  4. factorial *= i;
  5. }
  6. return factorial;
  7. }

递归实现

数学表达式

  1. f(1) = 1
  2. f(n) = f(n-1) * n

代码实现

  1. public static int factorial(int n) {
  2. //#1. 当 n = 1 时,递归结束
  3. if (n == 1) {
  4. return 1;
  5. }
  6. //#2. 把 factorial(n - 1) 的结果和 n 相乘,剩下的交给 factorial(n - 1) 来解决。
  7. return n * factorial(n - 1);
  8. }

汉诺塔问题

汉诺塔是一个发源于印度的益智游戏,也叫河内塔。相传它源于印度神话中的大梵天创造的三个金刚柱,一根柱子上叠着上下从小到大64个黄金圆盘。大梵天命令婆罗门将这些圆盘按从小到大的顺序移动到另一根柱子上,其中大圆盘不能放在小圆盘上面。当这64个圆盘移动完的时候,世界就将毁灭。

递归 - 图1
汉诺塔问题源于印度神话
那么好多人会问64个圆盘移动到底会花多少时间?那么古代印度距离现在已经很远,这64个圆盘还没移动完么?我们来通过计算来看看要完成这个任务到底要多少时间?
我们首先利用数学上的数列知识来看看F(n=1)=1,F(n=2)=3,F(n=3)=7,F(n=4)=15……F(n)=2F(n-1)+1;
我们使用数学归纳法可以得出通项式:F(n)=2^n-1。当n为64时F(n=64)=18446744073709551615。
我们假设移动一次圆盘为一秒,那么一年为31536000秒。那么18446744073709551615/31536000约等于584942417355天,换算成年为5845.54亿年。
目前太阳寿命约为50亿年,太阳的完整寿命大约100亿年。所以我们整个人类文明都等不到移动完整圆盘的那一
天。
两个盘子:2^2-1=3次
递归 - 图2
三个盘子:2^3-1 = 7次
递归 - 图3
递归思想:
image.png
递归 - 图5

  1. function tower(n, form, use, to) {
  2. if (n == 1) {
  3. move(form, to);
  4. return;
  5. }
  6. tower(n - 1, form, to, use);
  7. tower(1, form, use, to);
  8. tower(n - 1, use, form, to)
  9. }
  10. function move(from, to) {
  11. console.log(`${from}————>${to}`);
  12. }
  13. tower(3, "A", "B", "C");

递归如何实现

基准条件

也就是递归的结束条件,没有结束条件,就会出现死循环。

递归公式

递归公式总是描述自己和下一个递归函数直接的递进关系。

总结

image.png

斐波那契数列

0,1,1,2,3,5,8,13,21,34,55,89,144……
第三个数字开始每个数等于前面两个数之和。
基准条件:第 0,1 个元素是 0 和 1.
递归公式
递归 - 图7

  1. public class Recursive {
  2. public static int fibonacci(int n) {
  3. if (n == 0 || n == 1) {
  4. return n;
  5. }
  6. return fibonacci(n - 1) + fibonacci(n - 2);
  7. }
  8. public static void main(String[] args) {
  9. System.out.println("fibonacci(1) = " + fibonacci(1));
  10. System.out.println("fibonacci(3) = " + fibonacci(3));
  11. System.out.println("fibonacci(10) = " + fibonacci(10));
  12. }
  13. }

递归实现二分插入排序

while的结束条件为基准条件
递归 - 图8

  1. import java.util.Arrays;
  2. public class Recursive {
  3. // 查找应该插入的索引位置
  4. public static int searchIndex(int[] array, int left, int right, int aim) {
  5. // 基准条件,和之前 while 里面的条件刚好相反
  6. if (left>=right){
  7. // 如果最终元素仍然大于目标元素,则将索引位置往左边移动一个
  8. if(array[left]>aim){
  9. return left-1;
  10. }
  11. // 否则就是当前位置
  12. return left;
  13. }
  14. int middle = (left + right) / 2;
  15. int value = array[middle];
  16. if(value<aim){
  17. // 修正 left 继续执行
  18. return searchIndex(array,middle+1,right,aim);
  19. }
  20. else{
  21. // 修正 right 继续执行
  22. return searchIndex(array,left,middle-1,aim);
  23. }
  24. /* while (left < right) {
  25. int middle = (left + right) / 2;
  26. int value = array[middle];
  27. if (value < aim) {
  28. left = middle + 1;
  29. } else {
  30. right = middle - 1;
  31. }
  32. }
  33. // #1. 如果最终元素仍然大于目标元素,则将索引位置往左边移动一个
  34. if (array[left] > aim) {
  35. return left - 1;
  36. }
  37. // 否则就是当前位置
  38. return left;*/
  39. }
  40. // 插入排序
  41. public static void insertSort(int[] array) {
  42. // 从倒数第二位开始,遍历到底0位,遍历 N-1 次
  43. for (int i = array.length - 2; i >= 0; i--) {
  44. // 存储当前抽离的元素
  45. int temp = array[i];
  46. int index = searchIndex(array, i + 1, array.length - 1, temp);
  47. // 根据插入的索引位置,进行数组的移动和插入
  48. int j = i + 1;
  49. while (j <= index) {
  50. array[j - 1] = array[j];
  51. j++;
  52. }
  53. array[j - 1] = temp;
  54. }
  55. }
  56. public static void main(String[] args) {
  57. int[] array = {9, 2, 4, 7, 5, 3};
  58. // Arrays.toString 可以方便打印数组内容
  59. System.out.println(Arrays.toString(array));
  60. insertSort(array);
  61. System.out.println(Arrays.toString(array));
  62. }
  63. }

汉罗塔算法:

image.pngimage.png
以两个环为例,我们已经将小环移动到了B柱,最大环此时可以视为不存在。那么如第二张图所示我们将B,C柱子交换位置,那么此步骤是否和移动1阶汉诺塔一样了
然后我们中间执行了将最大环从A移动到C的固定步骤
image.png
image.png
同理,在上图为2阶汉诺塔中间的步骤之一,我们已经将最大的环移动到了C柱,最大环此时可以视为不存在。那么如右图所示我们将A,B柱子交换位置,那么接下来的步骤是否和移动1阶汉诺塔一样了呢?
到这里我们总结出了如下特点:
其实2阶汉诺塔相当于执行了三大步骤:
1.在ACB的顺序下执行了一阶汉诺塔的移法
2.从A->C移动了最大盘
3.在BAC的顺序下执行了一阶汉诺塔的移法
那么推广到三阶的时候,我们将小环和中环视为一个整体,我们是否又变成了执行二阶汉诺塔方法了呢?
那么四阶前三个环视为整体,五阶前四个环视为整体……我们已经找到了解决汉诺塔方法的递归算法。下面,我们就用代码来实现它。

  1. public class Han {
  2. public void hanoi(int n, char A, char B, char C) {
  3. if (n == 1) {
  4. move(A, C);
  5. } else {
  6. hanoi(n - 1, A, C, B);//步骤1 按ACB数序执行N-1的汉诺塔移动
  7. move(A, C); //步骤2 执行最大盘子移动
  8. hanoi(n - 1, B, A, C);//步骤3 按BAC数序执行N-1的汉诺塔移动
  9. }
  10. }
  11. private void move(char A, char C) {//执行最大盘子的从A-C的移动
  12. System.out.println("move:" + A + "--->" + C);
  13. }
  14. public static void main(String[] args) {
  15. Han hanoi = new Han();
  16. System.out.println("移动步骤:");
  17. hanoi.hanoi(3, 'A', 'B', 'C');
  18. }
  19. }

关于递归的四条基本法则

1.基准情形。必须有某些基准情形,它无需递归就能解出。 2.不断推进。对于那些需要递归求解的情形,每一次递归调用都必须要使求解的状况朝接近基准情形的方向推进。 3.设计法则。假设所有的递归调用都能运行。 4.合成效益法则。在求解同一个问题的同一实例时,切勿在不同的递归调用中做重复性的工作 ——-摘自《数据结构与算法分析(机械工业出版社 Mark Allen Weiss著)》

分而治之思想——归并排序

分而治之的思想采用了递归思想,将原问题分成几个规模较小但是类似于原问题的子问题。通过递归的方式来求解这些小问题,最后将子问题的解合并来得到原问题的解。
递归 - 图13

归并排序

核心思路
将大数组分解成小数组,将每个小数组排好序,再将这些有序的小数组合并成大数组。
递归 - 图14

分——递归拆分

2分法拆分原数组,直到每个数组只有一个元素。
核心逻辑:

  1. 如何递归进行数组拆分
  2. 如何用原始数组创建两个子数组
  1. // 拷贝原数组的部分内容,从 left 到 right
  2. public static int[] subArray(int[] source, int left, int right) {
  3. // 创建一个新数组
  4. int[] result = new int[right - left];
  5. // 依次赋值进去
  6. for (int i = left; i < right; i++) {
  7. result[i - left] = source[i];
  8. }
  9. return result;
  10. }
  11. // 归并排序,返回排好序的数组
  12. public static int[] mergeSort(int[] array) {
  13. // 为了方便查看结果,我们将每个数组进行打印
  14. System.out.println(Arrays.toString(array));
  15. if (array.length == 1) {
  16. return array;
  17. }
  18. int middle = array.length / 2;
  19. // #1. 处理 0 到 middle 左侧数组部分
  20. int[] left = mergeSort(subArray(array, 0, middle));
  21. // #2. 处理 middle 到 array.length 右侧数组部分
  22. int[] right = mergeSort(subArray(array, middle, array.length));
  23. // TODO处理合并问题
  24. return array;
  25. }

递归 - 图15

治——合并排序

递归 - 图16

以最后两个数组 2,4,9 和 3 ,5,7 举例。
第一步:
递归 - 图17
第二步:
递归 - 图18

  1. import java.util.Arrays;
  2. public class Sort {
  3. // 归并排序
  4. public static int[] mergeSort(int[] array) {
  5. // 为了方便查看结果,我们将每个数组进行打印
  6. if (array.length == 1) {
  7. return array;
  8. }
  9. int middle = array.length / 2;
  10. // 处理 0 到 middle 左侧数组部分
  11. int[] left = mergeSort(subArray(array, 0, middle));
  12. // 处理 middle 到 array.length 右侧数组部分
  13. int[] right = mergeSort(subArray(array, middle, array.length));
  14. // TODO处理合并问题
  15. int l = 0;
  16. int r = 0;
  17. int index = 0;
  18. // 依次比较左右两个数组
  19. while (l < left.length && r < right.length) {
  20. array[index] = Math.min(left[l], right[r]);
  21. index++;
  22. if (left[l] < right[r]) {
  23. l++;
  24. } else {
  25. r++;
  26. }
  27. }
  28. // 右侧数组已经遍历完成,左侧有剩余
  29. if (l < left.length) {
  30. for(int i = l; i < left.length; i++){
  31. array[index] = left[i];
  32. index++;
  33. }
  34. }
  35. // 左侧数组已经遍历完成,右侧有剩余
  36. if(r < right.length){
  37. for(int i = r; i < right.length; i++){
  38. array[index] = right[i];
  39. index++;
  40. }
  41. }
  42. return array;
  43. }
  44. // 拷贝原数组的部分内容,从 left 到 right
  45. public static int[] subArray(int[] source, int left, int right) {
  46. // 创建一个新数组
  47. int[] result = new int[right - left];
  48. // 依次赋值进去
  49. for (int i = left; i < right; i++) {
  50. result[i - left] = source[i];
  51. }
  52. return result;
  53. }
  54. public static void main(String[] args) {
  55. int[] array = {9, 2, 4, 7, 5, 3};
  56. // Arrays.toString 可以方便打印数组内容
  57. System.out.println("raw: " + Arrays.toString(array));
  58. int[] result = mergeSort(array);
  59. System.out.println("result: " + Arrays.toString(result));
  60. }
  61. }

分而治之思想——快速排序

快速排序是现在面试官最喜欢面试的排序算法,是现在编程语言自带函数中,使用最多的算法。

实现原理

分区
image.png
递归 - 图20
image.png
第一步:
递归 - 图22
第二步:
递归 - 图23
第二次分区
通过第一次分区,我们将数组分为左侧和右侧,按照递归的思想,我们继续将左右两侧数组按照选轴分区重复排序。
**递归 - 图24

时间复杂度

每次分区需要全数组比较一次所以为O(N),而又要进行O(log(N))次分区,所以最终结果为O(Nlog(N))
最坏的情况:数组本身是有序的。
递归 - 图25
每次分区,实际上没有分区,因为轴的值永远是最大值,这个时候时间复杂度为O(N^2)

代码实现

  1. import java.util.Arrays;
  2. public class QuickSort {
  3. // 快速排序
  4. public static void quickSort(int[] array) {
  5. // 调用快速排序的核心,传入left,right
  6. quickSortCore(array, 0, array.length - 1);
  7. }
  8. // 快速排序的核心,同样也是递归函数
  9. public static void quickSortCore(int[] array, int left, int right) {
  10. // 递归基准条件,left >= right 即表示数组只有1个或者0个元素。
  11. if (left >= right) {
  12. return;
  13. }
  14. // 根据轴分区
  15. int pivotIndex = partition(array, left, right);
  16. // 递归调用左侧和右侧数组分区
  17. quickSortCore(array, left, pivotIndex - 1);
  18. quickSortCore(array, pivotIndex + 1, right);
  19. }
  20. // 对数组进行分区,并返回当前轴所在的位置
  21. public static int partition(int[] array, int left, int right) {
  22. int pivot = array[right];
  23. int leftIndex = left;
  24. int rightIndex = right - 1;
  25. while (true) {
  26. // 左指针移动
  27. while (array[leftIndex] <= pivot && leftIndex < right) {
  28. leftIndex++;
  29. }
  30. // 右指针移动
  31. while (array[rightIndex] >= pivot && rightIndex > 0) {
  32. rightIndex--;
  33. }
  34. if (leftIndex >= rightIndex) {
  35. break;
  36. } else {
  37. swap(array, leftIndex, rightIndex);
  38. }
  39. }
  40. swap(array, leftIndex, right);
  41. return leftIndex;
  42. }
  43. public static void swap(int[] array, int index1, int index2) {
  44. int temp = array[index1];
  45. array[index1] = array[index2];
  46. array[index2] = temp;
  47. }
  48. public static void main(String[] args) {
  49. int[] array = {9, 2, 4, 7, 5, 3};
  50. // Arrays.toString 可以方便打印数组内容
  51. System.out.println("raw: " + Arrays.toString(array));
  52. quickSort(array);
  53. System.out.println("result: " + Arrays.toString(array));
  54. }
  55. }

快速排序应用——快速选择

image.png
也就是第四大的元素。
递归 - 图27
image.png

代码实现

  1. import java.util.Arrays;
  2. public class QuickSort {
  3. // 快速选择,返回选中的元素
  4. public static int quickFind(int[] array, int aim) {
  5. // 调用快速选择的核心,传入left,right
  6. return quickFindCore(array, aim, 0, array.length - 1);
  7. }
  8. // 快速选择的核心,同样也是递归函数
  9. public static int quickFindCore(int[] array, int aim, int left, int right) {
  10. // 递归基准条件,left >= right 即表示数组只有1个或者0个元素,返回当前的元素
  11. if (left >= right) {
  12. return array[left];
  13. }
  14. // 根据轴分区
  15. int pivotIndex = partition(array, left, right);
  16. // 根据 aim 确定继续递归的方向
  17. if (pivotIndex > aim) {
  18. return quickFindCore(array, aim, left, pivotIndex - 1);
  19. } else if (pivotIndex < aim) {
  20. return quickFindCore(array, aim, pivotIndex + 1, right);
  21. } else {
  22. return array[pivotIndex];
  23. }
  24. }
  25. // 对数组进行分区,并返回当前轴所在的位置
  26. public static int partition(int[] array, int left, int right) {
  27. int pivot = array[right];
  28. int leftIndex = left;
  29. int rightIndex = right - 1;
  30. while (true) {
  31. // 左指针移动
  32. while (array[leftIndex] < pivot && leftIndex < right) {
  33. leftIndex++;
  34. }
  35. // 右指针移动
  36. while (array[rightIndex] > pivot && rightIndex > 0) {
  37. rightIndex--;
  38. }
  39. if (leftIndex >= rightIndex) {
  40. break;
  41. } else {
  42. swap(array, leftIndex, rightIndex);
  43. }
  44. }
  45. swap(array, leftIndex, right);
  46. return leftIndex;
  47. }
  48. public static void swap(int[] array, int index1, int index2) {
  49. int temp = array[index1];
  50. array[index1] = array[index2];
  51. array[index2] = temp;
  52. }
  53. public static void main(String[] args) {
  54. int[] array = {72, 77, 48, 17, 71, 2, 25, 97, 82, 5, 2, 18, 15, 57, 7, 48, 93, 47, 38, 74, 18, 93, 98, 41, 54, 4, 47, 4, 63, 76};
  55. System.out.println("raw: " + Arrays.toString(array));
  56. // 目标是倒数第 6 个元素
  57. int result = quickFind(array, array.length - 6);
  58. System.out.println("result: " + result);
  59. System.out.println("after: " + Arrays.toString(array));
  60. }
  61. }