数据结构与算法学习--复杂度分析

568 阅读8分钟

数据结构和算法本身解决的是“快”和“省”的问题,即如何让代码运行得更快,如何让代码更省存储空间。所以,执行效率是算法一个非常重要的考量指标 。衡量代码的执行效率,又不需要具体的测试数据来测试,就可以粗略的估算算法的执行效率的方法,即时间复杂度分析和空间复杂度分析。

大O复杂度表示法
let sum = 0
for(let i=0;i++;i< n){
	sum += i
}

我们假设每行代码执行的时间都一样,记为T,以此来分析下代运行的时间。第一行代码只需要运行一次,即时间T,第二行第三行属于循环,而循环了n次,相当于代码执行了两个n次,也就是说这块代码执行总时间为(2n+1)T。

let sum = 0
for(let i=0;i++;i< n){
	for(let j=0;j++;j<n){
		sum = sum +  i * j;
	}
}

同样假设每行代码执行的时间都一样,记为T,以此来分析下代运行的时间。第一行代码只执行一次,运行时间为T,第二行代码会循环n次,运行时间为n*T,第三第四行会运行 n^2次,所有的时间加起来就是(2n^2 + 2 + 1)T。

总结一下:所有代码的执行时间T(n)与每行代码执行次数n成正比。

可以总结出一个公式:

T(n)=O(f(n)) T(n) = O(f(n))

其中T(n)表示代码执行的时间;n表示数据规模;f(n)表示每行代码执行的次数总和;而O表示T(n)与f(n)成正比。

所以第一个例子用上述公式可以写成 T(n) = O(2n+1) ,第二个例子可以写成 T(n) = O(2n^2 + 2 + 1) 。这就是大O的时间复杂度表示法。 大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度。

当 n 很大时,公式中的低阶、常量、系数三部分并不左右增长趋势,所以都可以忽略。我们只需要记录一个最大量级就可以了,如果用大 O 表示法表示刚讲的那两段代码的时间复杂度,就可以记为:T(n) = O(n); T(n) = O(n2)。

时间复杂度分析

分析一段代码的时间复杂度,可以有以下三个方法:

  1. 只关注循环执行次数最多的一段代码
  2. 加法法则:总复杂度等于量级最大的那段代码的复杂度
  3. 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

常见的时间复杂度量级:

  • O(2n) 和 O(n!) ,这是两个非多项式量级 , 当数据规模 n 越来越大时,非多项式量级算法的执行时间会急剧增加,求解问题的执行时间会无限增长。所以,非多项式时间复杂度的算法其实是非常低效的算法。

下面是一些常见的多项式时间复杂度

  • 常量阶O(1), O(1) 只是常量级时间复杂度的一种表示方法,并不是指只执行了一行代码。 只要代码的执行时间不随 n 的增大而增长,这样代码的时间复杂度我们都记作 O(1)。或者说,一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)。

  • 对数阶O(logn)和线性对数阶O(nlogn)

 i=1;
 while (i <= n)  {
   i = i * 2;
 }

分析时间复杂度可以根据代码循环次数做多的那行代码来分析,也就是第三行。 只要能计算出这行代码被执行了多少次,就能知道整段代码的时间复杂度。

这里 i 的取值就是一个等比数列 ,即2^0、2^1、2^2......通项公式即为n = 2^x,x就是我们的执行次数,求x可以两边同时取对数,可以得到 x=log2n 。所以,这段代码的时间复杂度就是 O(log2n)。

  • O(m+n)和O(m*n)
for(let i=0;i++;i< m){
	sum += i
}
for(let i=0;i++;i< n){
	sum += i
}

从代码中可以看出,m 和 n 是表示两个数据规模。我们无法事先评估 m 和 n 谁的量级大,所以我们在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。所以,上面代码的时间复杂度就是 O(m+n)。

空间复杂度分析

时间复杂度的全称是渐进时间复杂度**,**表示算法的执行时间与数据规模之间的增长关系。类比一下,空间复杂度全称就是渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系。

let a = []
for(let i=0;i++;i< n){
	a[i] = i * i;
}

像上面这段代码,第一行申请了一个空间存储变量a,跟数据规模没什么关系,而后续循环也没有占用更多空间,所以这段代码空间复杂度就是O(1)。

我们常见的空间复杂度就是 O(1)、O(n)、O(n2 ),像 O(logn)、O(nlogn) 这样的对数阶复杂度平时都用不到。而且,空间复杂度分析比时间复杂度分析要简单很多。

最好、最坏情况时间复杂度
function find(arr,x){
	let n = arr.length
	let index = -1
	for(let i=0;i++;i<n){
		if(arr[i] == x){
			index = i
			break
		}
	}
	return index
}

上面是一个查找数组元素位置的函数,根据时间复杂度分析法则:只关注循环执行次数最多的一段代码,这里循环最多的循环了n次,然而时间复杂度并不是O(n),因为要查找的变量x可能出现在数组的任意位置。如果数组中第一个元素正好是要查找的变量 x,那就不需要继续遍历剩下的 n-1 个数据了,那时间复杂度就是 O(1)。但如果数组中不存在变量 x,那我们就需要把整个数组都遍历一遍,时间复杂度就成了 O(n)。所以,不同的情况下,这段代码的时间复杂度是不一样的。 需要引入三个概念:最好情况时间复杂度、最坏情况时间复杂度和平均情况时间复杂度

  • 最好情况时间复杂度就是,在最理想的情况下,执行这段代码的时间复杂度。就像我们刚刚讲到的,在最理想的情况下,要查找的变量 x 正好是数组的第一个元素,这个时候对应的时间复杂度就是最好情况时间复杂度。

  • 最坏情况时间复杂度就是,在最糟糕的情况下,执行这段代码的时间复杂度。就像刚举的那个例子,如果数组中没有要查找的变量 x,我们需要把整个数组都遍历一遍才行,所以这种最糟糕情况下对应的时间复杂度就是最坏情况时间复杂度。

  • 平均时间复杂度 ,即平均情况时间复杂度。 要查找的变量 x,要么在数组里,要么就不在数组里 。我们假设在数组中与不在数组中的概率都为 1/2。另外,要查找的数据出现在 0~n-1 这 n 个位置的概率也是一样的,为 1/n。所以,根据概率乘法法则,要查找的数据出现在 0~n-1 中任意位置的概率就是 1/(2n) ,进行下加权计算,可以得到加权平均值为 (3n+1)/4 , 用大 O 表示法来表示,去掉系数和常量,这段代码的加权平均时间复杂度是 O(n)。

均摊时间复杂度

均摊时间复杂度就是一种特殊的平均时间复杂度。

对一个数据结构进行一组连续操作中,大部分情况下时间复杂度都很低,只有个别情况下时间复杂度比较高,而且这些操作之间存在前后连贯的时序关系,这个时候,我们就可以将这一组操作放在一块儿分析,看是否能将较高时间复杂度那次操作的耗时,平摊到其他那些时间复杂度比较低的操作上。而且,在能够应用均摊时间复杂度分析的场合,一般均摊时间复杂度就等于最好情况时间复杂度。