冒泡、选择、插入、归并排序+递归+对数器

冒泡、选择、插入、归并排序+递归+对数器

(from左神算法初级班第一节)

  1. 认识时间复杂度
  2. 冒泡、选择、插入排序
  3. 对数器
  4. 递归行为的实质、如何分析递归行为的复杂度
  5. 归并排序
  6. 小和问题和逆序对问题

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

算法时间复杂度
算法1O(N*M)
算法2O(M*logN)
算法3O(M*logM)+O(N+M)

如果N<M,则
O(MN)>O(MlogM)+O(N+M)>O(MlogN)
如果N>M,则
O(M
N)>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;
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值