一 递归,回溯
1.递归定义
- 程序调用自身的编程技巧称为递归.
- 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
2.递归的使用条件
- 子问题须与原始问题为同样的事,且更为简单;
- 递归要有边界条件、递归前进段和递归返回段。
- 当边界条件不满足时,递归前进;
- 当边界条件满足时,递归返回。
3.经典案例
- 斐波那契数列
从第三个数开始 就等于前面两个数相加;
例如:1 ,1 ,2, 3, 5, 8, 13
可以得出逻辑公式:
f(n) = f(n-1)+f(n-2);
终止条件:
f(1) = 1 ;f(2) = 1; - 电商分销系统
例如:B是A的下线,C是B的下线,那么在分钱返利的时候A可以分B,C的钱,如果要计算出A的钱就要分别找B,C的最后上级,然后才能分级计算
4.递归优化
- 使用非递归。所有的递归代码理论上是一定可以转换成非递归的。
- 加入缓存:把我们中间的运算结果保存起来,这样就可以把递归降至为o(n)
- 尾递归:什么是尾递归?尾递归就是调用函数一定出现在末尾,没有任何其他的操作了。因为我们编译器在编译代码时,如果发现函数末尾已经没有操作了,这时候就不会创建新的栈,而且覆盖到前面去。倒着算,不需要在回溯了,因为我们每次会把中间结果带下去。
5.代码及分析
- 斐波那契数列
- 递归代码
public class MyFibonacci {
public static long fab(int n) {
if (n <= 2) {
return 1;
}else {
return fab(n - 1) + fab(n - 2);
}
}
public static void main(String[] args) {
// 1 1 2 3 5 8 13 21
long l = System.currentTimeMillis();
int n = 50;
long fab = fab(n);
System.out.println("第" + n + "个数为:" + fab);
System.out.println("用时:" + (System.currentTimeMillis() - l) + "毫秒");
/**
* 第50个数为:12586269025
* 用时:28497毫秒
*/
}
}
- 时间复杂度和空间复杂度分析
- 简单递归解决斐波那契问题的时间复杂度和空间复杂度非常高,每新增一个点,就会多出两个分支进行重复计算,可以看到,当计算第50个数的时候已经用时超过28s,如果计算更大的数时间将更长,如果程序中使用此代码将会导致程序阻塞
- 优化:尾递归
那么如何去优化这个代码呢
从上面的图中可以看到,高时间复杂度和空间复杂度是因为重复计算导致,如果我们把计算结果记录下来,并且递归的时候进行传递呢,可以试验一下
/**
*
* @param n 第几个数
* @param pre 上一个计算的结果
* @param res 当前节点的计算结果
* @return
*/
public static long tailFab(int n ,long pre,long res) {
if (n <= 2) {
return res;
}else {
// 这个相当于倒着计算,知道第一个第二个点的数据,依次往后累加
// 下一个点的res就相当于当前点pre+res,下一个点的pre就相当于当前点的res
return tailFab(n - 1, res,pre + res);
}
}
public static void main(String[] args) {
// 1 1 2 3 5 8 13 21
long l = System.currentTimeMillis();
int n = 50;
// 倒着计算,从2开始,所以pre和res都是1
long fab = tailFab(n,1,1);
System.out.println("第" + n + "个数为:" + fab);
System.out.println("用时:" + (System.currentTimeMillis() - l) + "毫秒");
/**
* 第50个数为:12586269025
* 用时:1毫秒
*/
}
可以看到,计算50个数,时间缩短到1毫秒.尾递归的巧妙之处在于从尾开始,倒着计算,传递已经计算好的值,减少了回溯过程,时间复杂度和空间复杂度降低到o(n)
二.排序算法
1.概述
- 常见的排序算法及特点
- 快速排序
- 时间复杂度: O (nlogn)
- 是否稳定:不稳定
- 插入排序
- 时间复杂度: O(n^2)
- 是否稳定:稳定
- 冒泡排序
- 时间复杂度: O(n²)
- 是否稳定:稳定
- 希尔排序
- 时间复杂度: O(n^2)
- 是否稳定:不稳定
- 二分排序
- 时间复杂度: O(logn)
- 是否稳定:稳定
- 归并排序
- 时间复杂度: O(nlogn)
- 是否稳定:稳定
- 选择排序
- 时间复杂度: O(n^2)
- 是否稳定:不稳定
- 快速排序
2.插入排序
- 实现步骤
1.将数组分成已排序段和未排序段。初始化时已排序端只有一个元素
2.到未排序段取元素插入到已排序段,并保证插入后仍然有序
3.重复执行上述操作,直到未排序段元素全部加完。 - 代码
package algorithm.sort;
/**
* @author darlight
* @date 2021/8/24 10:40
* @blame ue189 Team
*/
public class InsertSort {
public static void sort(int[] value) {
for (int i = 1; i < value.length; i++) {
int data = value[i];
for (int j = i - 1; j >= 0; j--) {
// 如果这个数比当前数大,当前数就前移
if (value[j] > data) {
value[j + 1] = value[j];
value[j] = data;
} else {
// 直到找到比当前数小的数,就不需要移动
break;
}
}
}
}
public static void main(String[] args) {
int[] a = {9, 5, 6, 3, 1, 2, 8, 7};
sort(a);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
}
}
代码中可以看到,插入排序的时间复杂度是比较高的,嵌套循环,时间复杂度为O(n^2),并且是稳定的,相同大小的元素,不会改变元素原有的排列顺序
3.希尔排序
-
实现步骤
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量 =1( < …<d2<d1),即所有记录放在同一组中进行直接插入排序为止。
其实就是分成很多小组使序列尽可能的变成有序段,因为我们通过对插入排序分析可知,插入排序对已经排好序的序列速度是很快的。
来看一个具体的过程:7 8 9 0 4 3 1 2 5 10
我们取的这个增量分别就是5 2 1 -
图解
-
代码
package algorithm.sort;
import java.util.Arrays;
/**
* @author darlight
* @date 2021/8/24 13:39
* @blame ue189 Team
*/
public class HillSort {
public static void main(String[] args) {
int[] arr = {1, 4, 2, 7, 9, 8, 3, 6};
sort(arr);
System.out.println(Arrays.toString(arr));
int[] arr1 = {1, 4, 2, 7, 9, 8, 3, 6};
sort1(arr1);
System.out.println(Arrays.toString(arr1));
}
/**
* 希尔排序 针对有序序列在插入时采用交换法
*
* @param arr
*/
public static void sort(int[] arr) {
//增量gap,并逐步缩小增量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
//从第gap个元素,逐个对其所在组进行直接插入排序操作
for (int i = gap; i < arr.length; i++) {
int j = i;
while (j - gap >= 0 && arr[j] < arr[j - gap]) {
//插入排序采用交换法
swap(arr, j, j - gap);
j -= gap;
}
}
}
}
/**
* 希尔排序 针对有序序列在插入时采用移动法。
*
* @param arr
*/
public static void sort1(int[] arr) {
//增量gap,并逐步缩小增量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
//从第gap个元素,逐个对其所在组进行直接插入排序操作
for (int i = gap; i < arr.length; i++) {
int j = i;
int temp = arr[j];
if (arr[j] < arr[j - gap]) {
while (j - gap >= 0 && temp < arr[j - gap]) {
//移动法
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
}
}
}
/**
* 交换数组元素
*
* @param arr
* @param a
* @param b
*/
public static void swap(int[] arr, int a, int b) {
arr[a] = arr[a] + arr[b];
arr[b] = arr[a] - arr[b];
arr[a] = arr[a] - arr[b];
}
}
4.归并排序
-
实现步骤
顾名思义,采用递归和分治的思想进行排序,将要排序的数组分到最小单元后再对比排序,依次合并,如下图所示 -
图解
-
代码示例
package algorithm.sort; import java.util.Arrays; /** * @author darlight * @date 2021/8/24 15:28 * @blame ue189 Team */ public class RecursionSort { public static void mergeSort(int data[],int left ,int right) { // 如果左边和右边相等,说明已经分到只有一个数了,不需要再拆分了 if (left < right) { int mid = (left + right) / 2; // 拆分左边 System.out.println("拆分左边:left:"+left+";right:"+mid+";"); mergeSort(data, left, mid); // 拆分右边 System.out.println("拆分右边:left:"+(mid+1)+";right:"+right+";"); mergeSort(data, mid + 1, right); // 合并 System.out.println("合并:left:"+left+";mid:"+mid+";right:"+right+";"); merge(data, left, mid, right); } } public static void merge(int[] data, int left, int mid, int right) { int[] temp = new int[data.length]; // 左边第一个数的位置 int point1 = left; // 右边第一个数的位置 int point2 = mid + 1; // 表示当前已经到了哪个位置 int loc = left; // 左边到头并且右边也到头了就停止合并 while (point1 <= mid && point2 <= right) { // 哪个小就把哪个放到前面 if (data[point1] < data[point2]) { temp[loc] = data[point1]; point1++; loc++; }else { temp[loc] = data[point2]; point2++; loc++; } } // 如果左边没有走完但是右边走完了,那么左边没走完的放入新的数组 while (point1 <= mid) { temp[loc++] = data[point1++]; } // 如果右边没有走完但是左边走完了,右边没有走完的放入新的数组 while (point2 <= right) { temp[loc++] = data[point2++]; } // 排序过的数组替换原数组继续下一轮排序 for (int i = left; i <=right ; i++) { data[i] = temp[i]; } } public static void main(String[] args) { int[] data = {9, 5, 6, 8, 0, 3, 7, 1,2,4,23,51,98,14}; mergeSort(data, 0, data.length - 1); System.out.println(Arrays.toString(data)); } }
5.选择排序
-
实现步骤
选择排序的思路和插入排序非常相似,也分已排序和未排序区间。但选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
例如:对 4 5 6 3 2 1 进行排序
第一次排序: 1 5 6 3 2 4
第二次排序: 1 2 6 3 5 4 -
图解
-
代码示例
package algorithm.sort;
import java.util.Arrays;
/**
* 选择排序
* @author darlight
* @date 2021/8/25 14:31
* @blame ue189 Team
*/
public class SelectSort {
public static void selectSort(int[] data) {
// 未排序区的指针
for (int i = 0; i < data.length ; i++) {
int min = data[i];
int loc = i;
for (int j = i; j < data.length; j++) {
// 如果最小的数大于当前数,那么当前数就是最小的数
if (min > data[j]) {
min = data[j];
loc = j;
}
}
int datum = data[i];
data[i] = data[loc];
data[loc] = datum;
}
}
public static void main(String[] args) {
int[] data = {4, 5, 6, 3, 2, 1};
selectSort(data);
System.out.println(Arrays.toString(data));
}
}
6.冒泡排序
-
实现方式
冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序工作。 -
图解
-
代码示例
package algorithm.sort; import java.util.Arrays; /** * @author darlight * @date 2021/8/25 15:05 * @blame ue189 Team */ public class BubbleSort { public static void bubbleSort(int[] data) { // 这是排序的次数,后面已经拍好的就不需要再进行比较了 for (int i = 0; i < data.length ; i++) { boolean flag = false; for (int j = 0; j < data.length - i - 1; j++) { if (data[j] > data[j + 1]) { int a = data[j]; data[j] = data[j + 1]; data[j + 1] = a; flag = true; } } // 如果没有进行交换了,说明已经排完序了 if (!flag) { break; } } } public static void main(String[] args) { int[] data = {4, 5, 6, 3, 2, 1}; bubbleSort(data); System.out.println(Arrays.toString(data)); } }
7.快速排序
-
实现步骤
取一个基准数(通常是排序的第一个数),从后面往前找到比基准数小的数进行对换,从前面往后面找比基准数大的进行对换,直到不需要交换,以基准数分为3部分,左边的比之小,右边比之大. -
图解
-
代码示例
package algorithm.sort;
import java.util.Arrays;
/**
* @author darlight
* @date 2021/8/26 11:00
* @blame ue189 Team
*/
public class QuicklySort {
public static void quicklySort(int[] data, int left, int right) {
int base = data[left];
// 从左边开始找的位置
int ll = left;
// 从右边开始找的位置
int rr = right;
// 如果右边比基准数大就一直找
while (ll < rr ) {
while (data[rr] >= base) {
rr--;
}
// 如果右边有比基准数小的就和基准数就行交换
if (ll < rr) {
data[ll] = data[rr];
data[rr] = base;
// 已经找过的数下次就不需要找了
ll++;
}
// 如果左边一直小于基准数就一直找
while (ll < rr && data[ll] <= base) {
ll++;
}
// 如果在左边找到了比基准数大的数,就和基准数进行交换
if (ll < rr) {
data[rr] = data[ll];
data[ll] = base;
rr--;
}
}
// 左边起始点比移动到的左边下标小,说明左边需要继续排序
if (left < ll) {
quicklySort(data, left, ll - 1);
}
// 分出来的的部分也需要继续排序
if (ll < right) {
quicklySort(data, ll + 1, right);
}
}
public static void main(String[] args) {
int[] data = {45, 62, 72, 36, 12, 81, 90, 11, 15, 13};
quicklySort(data, 0, data.length - 1);
System.out.println(Arrays.toString(data));
// [11, 12, 13, 15, 36, 45, 62, 72, 81, 90]
}
}