冒泡、选择、插入、归并排序+递归+对数器
(from左神算法初级班第一节)
- 认识时间复杂度
- 冒泡、选择、插入排序
- 对数器
- 递归行为的实质、如何分析递归行为的复杂度
- 归并排序
- 小和问题和逆序对问题
1.认识时间复杂度
1)基本定义:评价一个算法流程的好坏,先看时间复杂度的指标,然后再分 析不同数据样本下的实际运行时间,也就是常数项时间。
2)原则: 不要低阶项,并且忽略高阶项系数
(具体来说,在常数操作数量的表达式中, 只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分 如果记为f(N),那么时间复杂度为O(f(N))。)
例如:aN2+N+1最终的时间复杂度为O(N2)
常见的时间复杂度:O(1)、O(N)、O(N^2)
3)理解时间复杂度的例子:
一个有序数组A,另一个无序数组B,请打印B中的所有不在A中的数,A数 组长度为N,B数组长度为M。
算法1:对于数组B中的每一个数,都在A中通过遍历的方式找;
算法2:对于数组B中的每一个数,都在A中通过二分的方式找;
算法3:数组B先排序,然后用类似外排的方式打印所有在A中 数;
默认log2N=logN
算法 | 时间复杂度 |
---|---|
算法1 | O(N*M) |
算法2 | O(M*logN) |
算法3 | O(M*logM)+O(N+M) |
如果N<M,则
O(MN)>O(MlogM)+O(N+M)>O(MlogN)
如果N>M,则
O(MN)>O(MlogN)>O(MlogM)+O(N+M)
2.冒泡、选择、插入排序(时间复杂度:O(N2),额外空间复杂度O(1))
1)冒泡排序算法原理如下(from百度):
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较
-(每轮从头每两个数比较,大的交换到后面,一直到比较到最后。每一轮排好一个数)
冒泡排序代码:
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int e = arr.length - 1; e > 0; e--) {//范围每次缩减1,因为每次都排好了一个数
for (int i = 0; i < e; i++) {//从头到e进行两两比较
if (arr[i] > arr[i + 1]) {
swap(arr, i, i + 1);//(前面比后面大就进行交换)
}
}
}
}
public static void swap(int[] arr, int i, int j) {//两两交换
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
2)选择排序原理:
- 在0~N-1范围内找一个最小的值,放到0位置;
- 在1~N-1范围内找一个最小的值,放到1位置;
- 一直重复到结束;
(每次过一遍范围都找到最小的,然后放到范围内的首位置)
选择排序代码:
public static void selectionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length - 1; i++) {//范围每次缩小1,从前往后缩。
int minIndex = i;//找范围内最小值,最开始默认是第一个
for (int j = i + 1; j < arr.length; j++) {
minIndex = arr[j] < arr[minIndex] ? j : minIndex;//是否比目前最小值还小,如果是,则交换,否则不交换;
}
swap(arr, i, minIndex);//将最小值放到范围的第一个数
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
3)插入排序原理:
(扑克牌插牌原理)
- 先将手里的牌排好序,然后拿新的牌跟排好序最后面的开始比较,比排好序的牌小就交换到前面去,一直到比比较的牌大,就不用动了。
- 每次排好一张牌,最后都有序。
插入排序代码:
public static void insertionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 1; i < arr.length; i++) {//有多少张新牌需要插入,每次插入一张牌
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {//从排好序的最后面开始比较,新牌小就换到比较牌的前面
swap(arr, j, j + 1);
}
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
3.对数器
使用对数器,具体步骤:
1)有一个你想要测的方法a
2)实现一个绝对正确但是复杂度不好的方法b
3) 实现一个随机样本产生器
4)实现比对的方法
5)把方法a和方法b比对很多次来验证方法a是否正确。
6)如果有一个样本使得比对出错,打印样本分析是哪个方法出 错
7)当样本数量很多时比对测试依然正确,可以确定方法a已经 正确。
好处:
- 验证方法对不对
- 可以很快找到错误case(几千几万case中)
- 判断贪心对不对
具体实现(例如测试冒泡排序方法是否正确):
想要测试冒泡排序方法a(判断该方法是否正确):
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int e = arr.length - 1; e > 0; e--) {//范围每次缩减1,因为每次都排好了一个数
for (int i = 0; i < e; i++) {//从头到e进行两两比较
if (arr[i] > arr[i + 1]) {
swap(arr, i, i + 1);//(前面比后面大就进行交换)
}
}
}
}
public static void swap(int[] arr, int i, int j) {//两两交换
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
1)产生一个长度随机的数组(可能为正,也可能为负,0)
随机样本产生器:
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}
2)绝对正确的方法
调用函数自带的排序方法(实现一个绝对正确但是复杂度不好的方法b,用于和冒泡排序测试方法比较,判断测试方法是否正确)
public static void comparator(int[] arr) {
Arrays.sort(arr);
}
3)大样本测试
public static void main(String[] args) {
int testTime = 500000;//测试次数
int maxSize = 100;
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);//产生随机数组
int[] arr2 = copyArray(arr1);
bubbleSort(arr1);//测试的方法
comparator(arr2);//绝对正确的方法
if (!isEqual(arr1, arr2)) {
succeed = false;
break;
}
}
System.out.println(succeed ? "Nice!" : "Fucking fucked!");
}
public static boolean isEqual(int[] arr1, int[] arr2) {//实现比对的方法 ,比较两个数组的每个数是否相等
if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
return false;
}
if (arr1 == null && arr2 == null) {
return true;
}
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
4.递归行为的实质、如何分析递归行为的复杂度
(老师:递归是自己调用自己,玄学hhhhh)
1)而递归的本质是将该步的所有函数参数和所有信息(包括运行到第几行)都一起压到系统栈里去。递归返回后就会恢复栈上最上面的所有状况(还原现场)。
例子:在整个数组中找到最大值
解题:分为左右两个部分,先在左边找最大值,和在右边找最大值,然后再比较最大值。
public class Test222 {
//任何递归行为都可以改为非递归行为
public static int getMax(int[] arr,int L,int R) {
if(L==R) {
return arr[L];
}
int mid = (L+R)/2;
int maxLeft = getMax(arr,L,mid);//T(N/2)
int maxright = getMax(arr,mid+1,R);//T(N/2)
return Math.max(maxLeft, maxright);// O(N)
}
public static void main(String[] args) {
int[] arr = {4,3,2,1};
System.out.println(getMax(arr,0,arr.length-1));
}
}
2)递归行为时间复杂度
master公式(适用范围:划分子过程的规模是一样的时候,才能用):
T(N) = a*T(N/b) + O(N^d)
如果 log(b,a) > d ,那么时间复杂度为O(N^log(b,a))
如果 log(b,a) = d ,那么时间复杂度为O(N^d * logN)
如果 log(b,a) < d,那么时间复杂度为O(N^d)
上面1)的例子中的时间复杂度为:
T(N) = 2*T(N/2) + O(N)
因此a=b=2,d=1,log(b,a) =1=d
所以时间复杂度为O(N * logN)
5.归并排序
时间复杂度O(N*logN),额外空间复杂度O(N)
原理:左侧排好序,右侧排好序。准备个辅助数组,左右侧谁小,谁填进辅助数组。辅助数组整体有序了,再拷贝回原数组。
T(N) = 2*T(N/2) + O(N)
public static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
mergeSort(arr, 0, arr.length - 1);
}
public static void mergeSort(int[] arr, int l, int r) {
if (l == r) {
return;
}
int mid = l + ((r - l) >> 1);
mergeSort(arr, l, mid);//左侧排好序
mergeSort(arr, mid + 1, r);//右侧排好序
merge(arr, l, mid, r);//让整体有序。进辅助数组
}
public static void merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];//生成辅助数组
int i = 0;
int p1 = l;//准备指针,指向左侧第一个数
int p2 = m + 1;//准备指针,指向右侧第一个数
while (p1 <= m && p2 <= r) {
//谁小填谁进辅助数组
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m) {
//如果p2越界
help[i++] = arr[p1++];
}
while (p2 <= r) {
//如果p1越界
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
//此时,辅助数组已经拍好序了,将辅助数组拷贝到原数组中。
arr[l + i] = help[i];
}
}
6.小和问题和逆序对问题
小和问题在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
例子: [1,3,4,2,5]
1左边比1小的数,没有;
3左边比3小的数,1;
4左边比4小的数,1、3;
2左边比2小的数,1;
5左边比5小的数,1、3、4、2;
所以小和为1+1+3+1+1+3+4+2=16
逆序对问题 在一个数组中,左边的数如果比右边的数大,则折两个数构成一个逆序对,请打印所有逆序对。
小和归并代码解(相比冒泡等排序,减少无效比较):
public static int smallSum(int[] arr) {
//用归并来实现小和。在归并的过程中计算小和。
if (arr == null || arr.length < 2) {
return 0;
}
return mergeSort(arr, 0, arr.length - 1);
}
public static int mergeSort(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
int mid = l + ((r - l) >> 1);
return mergeSort(arr, l, mid) //左侧炸出多少数
+ mergeSort(arr, mid + 1, r) //右侧炸出多少数
+ merge(arr, l, mid, r);//合并炸出多少数。
}
public static int merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = m + 1;
int res = 0;
while (p1 <= m && p2 <= r) {
//当前p1被炸出多少个数
res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
return res;
}