递归在编程世界的表现就是函数自己调用自己,只不过可能传入的参数在变。
递归的本质是栈:
阶乘计算
n! = 1 * 2 * 3 * 4 * …… * (n-1) * n
循环实现
public static int factorial(int n) {int factorial = 1;for (int i = 1; i <= n; i++) {factorial *= i;}return factorial;}
递归实现
数学表达式
f(1) = 1f(n) = f(n-1) * n
代码实现
public static int factorial(int n) {//#1. 当 n = 1 时,递归结束if (n == 1) {return 1;}//#2. 把 factorial(n - 1) 的结果和 n 相乘,剩下的交给 factorial(n - 1) 来解决。return n * factorial(n - 1);}
汉诺塔问题
汉诺塔是一个发源于印度的益智游戏,也叫河内塔。相传它源于印度神话中的大梵天创造的三个金刚柱,一根柱子上叠着上下从小到大64个黄金圆盘。大梵天命令婆罗门将这些圆盘按从小到大的顺序移动到另一根柱子上,其中大圆盘不能放在小圆盘上面。当这64个圆盘移动完的时候,世界就将毁灭。

汉诺塔问题源于印度神话
那么好多人会问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^3-1 = 7次
递归思想:
function tower(n, form, use, to) {if (n == 1) {move(form, to);return;}tower(n - 1, form, to, use);tower(1, form, use, to);tower(n - 1, use, form, to)}function move(from, to) {console.log(`${from}————>${to}`);}tower(3, "A", "B", "C");
递归如何实现
基准条件
递归公式
总结
斐波那契数列
0,1,1,2,3,5,8,13,21,34,55,89,144……
第三个数字开始每个数等于前面两个数之和。
基准条件:第 0,1 个元素是 0 和 1.
递归公式:
public class Recursive {public static int fibonacci(int n) {if (n == 0 || n == 1) {return n;}return fibonacci(n - 1) + fibonacci(n - 2);}public static void main(String[] args) {System.out.println("fibonacci(1) = " + fibonacci(1));System.out.println("fibonacci(3) = " + fibonacci(3));System.out.println("fibonacci(10) = " + fibonacci(10));}}
递归实现二分插入排序
while的结束条件为基准条件
import java.util.Arrays;public class Recursive {// 查找应该插入的索引位置public static int searchIndex(int[] array, int left, int right, int aim) {// 基准条件,和之前 while 里面的条件刚好相反if (left>=right){// 如果最终元素仍然大于目标元素,则将索引位置往左边移动一个if(array[left]>aim){return left-1;}// 否则就是当前位置return left;}int middle = (left + right) / 2;int value = array[middle];if(value<aim){// 修正 left 继续执行return searchIndex(array,middle+1,right,aim);}else{// 修正 right 继续执行return searchIndex(array,left,middle-1,aim);}/* while (left < right) {int middle = (left + right) / 2;int value = array[middle];if (value < aim) {left = middle + 1;} else {right = middle - 1;}}// #1. 如果最终元素仍然大于目标元素,则将索引位置往左边移动一个if (array[left] > aim) {return left - 1;}// 否则就是当前位置return left;*/}// 插入排序public static void insertSort(int[] array) {// 从倒数第二位开始,遍历到底0位,遍历 N-1 次for (int i = array.length - 2; i >= 0; i--) {// 存储当前抽离的元素int temp = array[i];int index = searchIndex(array, i + 1, array.length - 1, temp);// 根据插入的索引位置,进行数组的移动和插入int j = i + 1;while (j <= index) {array[j - 1] = array[j];j++;}array[j - 1] = temp;}}public static void main(String[] args) {int[] array = {9, 2, 4, 7, 5, 3};// Arrays.toString 可以方便打印数组内容System.out.println(Arrays.toString(array));insertSort(array);System.out.println(Arrays.toString(array));}}
汉罗塔算法:


以两个环为例,我们已经将小环移动到了B柱,最大环此时可以视为不存在。那么如第二张图所示我们将B,C柱子交换位置,那么此步骤是否和移动1阶汉诺塔一样了
然后我们中间执行了将最大环从A移动到C的固定步骤

同理,在上图为2阶汉诺塔中间的步骤之一,我们已经将最大的环移动到了C柱,最大环此时可以视为不存在。那么如右图所示我们将A,B柱子交换位置,那么接下来的步骤是否和移动1阶汉诺塔一样了呢?
到这里我们总结出了如下特点:
其实2阶汉诺塔相当于执行了三大步骤:
1.在ACB的顺序下执行了一阶汉诺塔的移法
2.从A->C移动了最大盘
3.在BAC的顺序下执行了一阶汉诺塔的移法
那么推广到三阶的时候,我们将小环和中环视为一个整体,我们是否又变成了执行二阶汉诺塔方法了呢?
那么四阶前三个环视为整体,五阶前四个环视为整体……我们已经找到了解决汉诺塔方法的递归算法。下面,我们就用代码来实现它。
public class Han {public void hanoi(int n, char A, char B, char C) {if (n == 1) {move(A, C);} else {hanoi(n - 1, A, C, B);//步骤1 按ACB数序执行N-1的汉诺塔移动move(A, C); //步骤2 执行最大盘子移动hanoi(n - 1, B, A, C);//步骤3 按BAC数序执行N-1的汉诺塔移动}}private void move(char A, char C) {//执行最大盘子的从A-C的移动System.out.println("move:" + A + "--->" + C);}public static void main(String[] args) {Han hanoi = new Han();System.out.println("移动步骤:");hanoi.hanoi(3, 'A', 'B', 'C');}}
关于递归的四条基本法则
1.基准情形。必须有某些基准情形,它无需递归就能解出。 2.不断推进。对于那些需要递归求解的情形,每一次递归调用都必须要使求解的状况朝接近基准情形的方向推进。 3.设计法则。假设所有的递归调用都能运行。 4.合成效益法则。在求解同一个问题的同一实例时,切勿在不同的递归调用中做重复性的工作 ——-摘自《数据结构与算法分析(机械工业出版社 Mark Allen Weiss著)》
分而治之思想——归并排序
分而治之的思想采用了递归思想,将原问题分成几个规模较小但是类似于原问题的子问题。通过递归的方式来求解这些小问题,最后将子问题的解合并来得到原问题的解。
归并排序
核心思路
将大数组分解成小数组,将每个小数组排好序,再将这些有序的小数组合并成大数组。
分——递归拆分
2分法拆分原数组,直到每个数组只有一个元素。
核心逻辑:
- 如何递归进行数组拆分
- 如何用原始数组创建两个子数组
// 拷贝原数组的部分内容,从 left 到 rightpublic static int[] subArray(int[] source, int left, int right) {// 创建一个新数组int[] result = new int[right - left];// 依次赋值进去for (int i = left; i < right; i++) {result[i - left] = source[i];}return result;}// 归并排序,返回排好序的数组public static int[] mergeSort(int[] array) {// 为了方便查看结果,我们将每个数组进行打印System.out.println(Arrays.toString(array));if (array.length == 1) {return array;}int middle = array.length / 2;// #1. 处理 0 到 middle 左侧数组部分int[] left = mergeSort(subArray(array, 0, middle));// #2. 处理 middle 到 array.length 右侧数组部分int[] right = mergeSort(subArray(array, middle, array.length));// TODO处理合并问题return array;}
治——合并排序
以最后两个数组 2,4,9 和 3 ,5,7 举例。
第一步:
第二步:
import java.util.Arrays;public class Sort {// 归并排序public static int[] mergeSort(int[] array) {// 为了方便查看结果,我们将每个数组进行打印if (array.length == 1) {return array;}int middle = array.length / 2;// 处理 0 到 middle 左侧数组部分int[] left = mergeSort(subArray(array, 0, middle));// 处理 middle 到 array.length 右侧数组部分int[] right = mergeSort(subArray(array, middle, array.length));// TODO处理合并问题int l = 0;int r = 0;int index = 0;// 依次比较左右两个数组while (l < left.length && r < right.length) {array[index] = Math.min(left[l], right[r]);index++;if (left[l] < right[r]) {l++;} else {r++;}}// 右侧数组已经遍历完成,左侧有剩余if (l < left.length) {for(int i = l; i < left.length; i++){array[index] = left[i];index++;}}// 左侧数组已经遍历完成,右侧有剩余if(r < right.length){for(int i = r; i < right.length; i++){array[index] = right[i];index++;}}return array;}// 拷贝原数组的部分内容,从 left 到 rightpublic static int[] subArray(int[] source, int left, int right) {// 创建一个新数组int[] result = new int[right - left];// 依次赋值进去for (int i = left; i < right; i++) {result[i - left] = source[i];}return result;}public static void main(String[] args) {int[] array = {9, 2, 4, 7, 5, 3};// Arrays.toString 可以方便打印数组内容System.out.println("raw: " + Arrays.toString(array));int[] result = mergeSort(array);System.out.println("result: " + Arrays.toString(result));}}
分而治之思想——快速排序
快速排序是现在面试官最喜欢面试的排序算法,是现在编程语言自带函数中,使用最多的算法。
实现原理
分区

第一步:
第二步:
、
第二次分区
通过第一次分区,我们将数组分为左侧和右侧,按照递归的思想,我们继续将左右两侧数组按照选轴分区重复排序。
**
时间复杂度
每次分区需要全数组比较一次所以为O(N),而又要进行O(log(N))次分区,所以最终结果为O(Nlog(N))
最坏的情况:数组本身是有序的。
每次分区,实际上没有分区,因为轴的值永远是最大值,这个时候时间复杂度为O(N^2)
代码实现
import java.util.Arrays;public class QuickSort {// 快速排序public static void quickSort(int[] array) {// 调用快速排序的核心,传入left,rightquickSortCore(array, 0, array.length - 1);}// 快速排序的核心,同样也是递归函数public static void quickSortCore(int[] array, int left, int right) {// 递归基准条件,left >= right 即表示数组只有1个或者0个元素。if (left >= right) {return;}// 根据轴分区int pivotIndex = partition(array, left, right);// 递归调用左侧和右侧数组分区quickSortCore(array, left, pivotIndex - 1);quickSortCore(array, pivotIndex + 1, right);}// 对数组进行分区,并返回当前轴所在的位置public static int partition(int[] array, int left, int right) {int pivot = array[right];int leftIndex = left;int rightIndex = right - 1;while (true) {// 左指针移动while (array[leftIndex] <= pivot && leftIndex < right) {leftIndex++;}// 右指针移动while (array[rightIndex] >= pivot && rightIndex > 0) {rightIndex--;}if (leftIndex >= rightIndex) {break;} else {swap(array, leftIndex, rightIndex);}}swap(array, leftIndex, right);return leftIndex;}public static void swap(int[] array, int index1, int index2) {int temp = array[index1];array[index1] = array[index2];array[index2] = temp;}public static void main(String[] args) {int[] array = {9, 2, 4, 7, 5, 3};// Arrays.toString 可以方便打印数组内容System.out.println("raw: " + Arrays.toString(array));quickSort(array);System.out.println("result: " + Arrays.toString(array));}}
快速排序应用——快速选择
代码实现
import java.util.Arrays;public class QuickSort {// 快速选择,返回选中的元素public static int quickFind(int[] array, int aim) {// 调用快速选择的核心,传入left,rightreturn quickFindCore(array, aim, 0, array.length - 1);}// 快速选择的核心,同样也是递归函数public static int quickFindCore(int[] array, int aim, int left, int right) {// 递归基准条件,left >= right 即表示数组只有1个或者0个元素,返回当前的元素if (left >= right) {return array[left];}// 根据轴分区int pivotIndex = partition(array, left, right);// 根据 aim 确定继续递归的方向if (pivotIndex > aim) {return quickFindCore(array, aim, left, pivotIndex - 1);} else if (pivotIndex < aim) {return quickFindCore(array, aim, pivotIndex + 1, right);} else {return array[pivotIndex];}}// 对数组进行分区,并返回当前轴所在的位置public static int partition(int[] array, int left, int right) {int pivot = array[right];int leftIndex = left;int rightIndex = right - 1;while (true) {// 左指针移动while (array[leftIndex] < pivot && leftIndex < right) {leftIndex++;}// 右指针移动while (array[rightIndex] > pivot && rightIndex > 0) {rightIndex--;}if (leftIndex >= rightIndex) {break;} else {swap(array, leftIndex, rightIndex);}}swap(array, leftIndex, right);return leftIndex;}public static void swap(int[] array, int index1, int index2) {int temp = array[index1];array[index1] = array[index2];array[index2] = temp;}public static void main(String[] args) {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};System.out.println("raw: " + Arrays.toString(array));// 目标是倒数第 6 个元素int result = quickFind(array, array.length - 6);System.out.println("result: " + result);System.out.println("after: " + Arrays.toString(array));}}

