左程云算法课笔记(一)时间复杂度

认识时间复杂度

一、常数时间的操作

一个操作如果和样本的数量没有关系,每次都是固定使劲按内完成的操作,叫做常数操作。

时间复杂度为一个算法流程中,常数操作数量的一个指标。常用0(读作big 0)来表示。具体来说,先要对一个算法流程非常熟悉,然后去写出这个算法流程,发生了多少常数操作,进而总结出常数操作数量的表达式。

在表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果为f(N),那么时间复杂度为O(f(N))。

当两个算法所用到的时间复杂度的最高阶相同,通过常数项来比较算法优劣,这就需要通过实际的代码运行时间来检测。评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间们也就是“常数项时间”。

//查找数组中第i个元素,这就是一个常数操作
int a = arr[i];

//查找链表中的第i个节点,这就不是一个常数操作
int b = list.get(i);

二、选择排序

public static  void selectionSort(int[] arr) {
	if(arr == null || arr.length < 2){
		return;
	}
	for(int i = 0; i < arr.length-1; i++) {		//i~N-1次查找最小的数放在数组头部
		int minIndex = i;
		for(int j = i+1; j<arr.length; j++) {		//i~N-1找最小值下标
			minIndex = arr[j] < arr[minIndex] ? j : minIndex;
		}
		swap(arr, i, minIndex);
	}
}

选择排序分为N轮,每一轮找到数组中最小的数放在数组首部,时间复杂度为O(N^2)
在选择排序的过程中,开辟的变量空间只有i,j,minIndex三个,而minIndex在每一轮排序完成后都会释放空间,因此用到的辅助空间为常数,空间复杂度为O(1)

三、冒泡排序

public static void bubbleSort(int[] arr) {
	if(arr == null || arr.length < 2) {
		returns;
	}
	for(int i = arr.length-1; i > 0; i--) {
		for(int j = 0; j < i; j++) {
			if(arr[j]>arr[j+1]) {
				swap(arr, j, j+1);
			}
		}
	}
}

在冒泡排序的N轮中,每一轮都是前一个数与后一个数比较大小,大的放在后边,直到本轮所有数排列完毕,这样就可以确定最后一个数是最大的,如此重复N轮,时间复杂度为O(N)
在冒泡排序的过程中,开辟的辅助空间只有i,j,因此空间复杂度为O(1)
一个简单的冒泡排序到底坑死了多少人

四、异或

异或运算相信大家都知道是相同为0、不同为1,我们也可以将其理解为无进位相加,下面介绍异或的几个性质:

  1. 0 ^ N = N
  2. N ^ N = 0
  3. a ^ b = b ^ a
  4. ( a ^ b ) ^ c = a ^ ( b ^ c )
  5. 同样一组数异或成为一个数的结果与它们异或的顺序无关
//交换arr的i和j位置上的数
public static void swap(int[] arr, int i, int j) {
	arr[i] = arr[i] ^ arr[j];
	arr[j] = arr[i] ^ arr[j];
	arr[i] = arr[i] ^ arr[j];
}

对上面的代码进行解释:

假设定义两个变量A的值为甲,B的值为乙

  1. 第一行代码执行完后A = 甲 ^ 乙,B = 乙
  2. 第二行代码执行完后A = 甲 ^ 乙,B = 甲 ^ 乙 ^ 乙 = 甲(乙 ^ 乙 = 0,甲 ^ 0 = 甲)
  3. 第三行代码执行完后A = 甲 ^ 乙 ^ 甲 = 乙(甲 ^ 甲 = 0,乙 ^ 0 = 乙),B = 甲

至此两个变量的值交换。此方法有一个前提:必须保证两个数在内存中占用的是不同的空间

假设在数组中运用此方法对两个数进行交换,一定要保证这两个数的值不同

我们再来看一道题:

假设有一个整形数组 int arr[],要求在时间复杂度为O(n),空间复杂度为O(1)的情况下完成:
1) 数组中有一个数出现了奇数次,其他数都出现了偶数次,求这个数

首先准备一个变量eor,将这个变量与数组中所有数进行异或操作,最后的结果就是这个出现奇数次的数。因为从之前异或运算的性质来看,运算的顺序对于结果没有影响,因此如果将所有数排序进行运算,那么出现偶数次的数运算之后的结果为0,出现奇数次异或运算后的结果还将是其本身。

public static void printOddTimesNum1(int[] arr) {
	int eor = 0;
	for (int cur : arr) {
		eor ^= cur;
	}
	System.out.println(eor);
}

2)数组中有两个数出现了奇数次,其他数都出现了偶数次,求这两个数

public static void printOddTimesNum2(int[] arr) {
	int eor = 0;
	for(int i = 0; i < arr.length; i++) {
		eor ^= arr[i]; 
	}
	//eor = a ^ b
	//eor != 0
	//eor必然有一个位置上是1
	int rightOne = eor & (~eor + 1);	//提取出最右的1
	int onlyOne = 0;
	for(int cur : arr) {
		if((cur & rightOne) == 0) {
		onlyOne ^= cur; 
		}
	}
	System.out.println(onlyOne + "" + (eor ^ onlyOne));
}

奶奶嘀,不解释了,想知道咋弄的吗?不告诉你,虽然我也是抄的。

如果数组中有两种出现了奇数次的数,剩下的都是偶数次,同样是设置一个变量将所有数异或,假设这两个数是ab,异或的结果就是a^b。因为是两种数,所以这个结果不可能是0,再假设这个结果的第7位是1,就可以将数组中所有数分为两类:一类是第7位为1的数,另一类是第7位为0的数。ab两个数一定在不同的分类,这时设置另外一个变量与第7位是1的数进行异或,得到的结果就是ab其中一个数,最后用这个结果与eor异或就可以得到另外一个数。

五、插入排序

插入排序的基本思想是:将整个数组a分为有序和无序的两个部分。左边是有序的,右边是无序的。开始有序的部分只有a[0] , 其余都属于无序的部分。每次取出无序部分的第一个(最左边)元素,把它加入有序部分。假设插入合适的位置p,则原p位置及其后面的有序部分元素都向右移动一个位置,有序的部分即增加了一个元素。一直做下去,直到无序的部分没有元素。

public static void insertionSort(int[] arr) {
	if (arr == null || arr.length < 2)
		returns;
	for( int i = 1; i < arr.length; i++) {	//想要的是0~i位置上的有序
		for(int j = i - 1; j >= 0 && arr[j] > arr[j+1]; j--) {	//从后往前看,如果无序就交换,直到越界或有序
			swap(arr, j, j + 1);
		}
	}
}

按照最差情况来估计时间复杂度,时间复杂度位O(n^2),空间复杂度位O(1)

对数器

对数器的概念和使用

  1. 有一个你想要测的方法A
  2. 实现复杂度不好但是容易实现的方法B
  3. 实现一个随机样本发生器
  4. 把方法A和方法B跑相同的随机样本,看看得到的结果是否一样
  5. 如果有一个随机样本使得对比结果不一致,打印样本进行人工干预,改方法A或者方法B
  6. 当样本数量很多时对比测试依然正确,可以确定方法A已经正确

我们将之前提到的插入排序当作方法A,系统提供的排序方法作方法B,让它们两个方法作对数器。

//方法B
public static void comparator(int[] arr) {
	Arrays.sort(arr);
}

测试过程

public static void main(String[] args) {
	int testTime = 500000;	//测试次数
	int maxSize = 100;			//数组长度(随机)
	int maxValue = 100;		//数组元素值(随机)
	boolean succeed = true;
	for(int i = 0; i < testTimes; i++) {
		int[] arr1 = generateRandomArray(maxSize,maxValue);
		int[] arr2 = copyArray(arr1);
		insertionSort(arr2);
		comparator(arr2);
		if(!isEqual(arr1, arrr2)) {
			succeed = false;
			berak;
		}
	}
}

这里介绍一下generateRandomArray()函数,作用是生成一个所有值都随机的数组

public static int[] generateRandomArray(int maxSize,int maxValue) {
	//Math.random()       [0,1)所有的小数,等概率返回一个
	//Math.random() * N   [0,N)所有小数,等概率返回一个
	//(int)(Math.random() * N)    [0,N-1]所有的整数,等概率返回一个
	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;
}

六、递归

剖析递归行为和递归行为和递归行为时间复杂度的估算

用递归方法找一个数组中的最大值,系统上是如何实现的?

public class GetMax {
	public static int getMax(int[] arr) {
		return process(arr, 0, arr.length - 1);
	}
	//arr[L,R]范围上求最大值
	public static int process(int[] arr,int L, int R) {
		if(L == R)
			return arr[L];
		//这里中点为什么不是(R + L)/ 2呢?
		//如果数组比较大的时候R + L可能会溢出,因此就采用这种方法,>>表示右移一位,相当于除以2	
		int mid = L + ((R - L) >> 1);     //中点
		int leftMax = process(arr, L, mid);
		int rightMax = process(arr, mid + 1, R);
		return Math.max(leftMax, rightMax); 
	}
}

master公式

T( N ) = a × T( N / b ) + O( N ^ d )

  1. log(b,a) > d
    复杂度为 O( N ^ log( b , a ) )
  2. log(b,a) = d
    复杂度为 O( N ^ d × log N)
  3. log(b,a) < d
    复杂度为 O( N ^ d )

对上述公式进行解释:在递归的过程中,T(N)是母问题的数据规模,将母问题数据规模等分解为b份(如果不是等分master公式失效),T(N/b)是子问题的数据规模。a是子问题一共递归调用的次数,最后的O(N^d)是递归之外的操作的时间复杂度。

针对上面的寻找最大值的代码进行分析,从数组中间等分为两部分,因此b=2,对左右两部分分别递归,因此a=2。除此之外的操作就是对比左右两部分的最大值比较大小然后返回,这个过程的时间复杂度为O(1)。最终的master公式为T(N) = 2×T(N/2) + O(1)

补充阅读:https://blog.gocalf.com/algorithm-complexity-and-master-theorem

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值