算法效率的衡量方法和准则——时间复杂度、空间复杂度

一、衡量算法效率的方法(通常有两种)
1.事后统计法
衡量算法效率最简单的一个办法就是把算法变成一个程序,然后再机器上执行,然后计时,这就是事后统计法。
这样显然有一些缺点:
(1)必须到机器上去计算,而且这个计算不只是一次,我们要用多组数据对其进行重复的运算,然后得到一个统计的结果,那么你要算上机器的时间。
(2)肯定会有一些其他因素掩盖算法的本质。

2.事前分析估算法
通常比较算法好坏,都是在设计算法的时候就应该知道它的效率,这就是事前分析估算法。

说明: 要比较两个算法,实际上在设计的时候就做着工作来衡量它们的效率,因此更常用的方法是事前分析估算法。

二、怎么样估算算法的执行时间
【与算法执行时间相关的因素】
1.算法选用的策略
不同的策略选用的算法是不同的,它的执行时间也是不同的。
2.问题的规模
问题的规模不一样,算法的执行时间当然不一样。
例如:两个矩阵相乘,是10×10的,还是10000×10000的,同样一个算法,执行的时间确实不同的。
3.编写程序的语言
使用高级语言编写的程序比汇编语言编写的程序的执行时间要长。
4.编译程序产生的机器代码的质量
对于同一种高级语言,使用的编译器不同,编译出来的机器代码的质量也会不同。计算机最终执行的是机器代码,机器代码质量不同,算法变成程序后的执行时间也就不同。
5.计算机执行指令的速度
机器的速度不一样,执行的时间也是不一样的 。
例如:一个大型机和一个小型机显然是不一样的。

说明: 后面的三个因素是与计算机的硬件和软件相关的,跟我们设计算法的时候是无关的,所以在设计算法的时候就不再考虑这三个因素。所以,我们考虑算法执行的时间,取决于算法选用的策略和问题的规模,也就是说,一个算法的执行时间是问题规模的的一个函数

三、(渐进)时间复杂度
假如,随着问题规模n的增长,算法执行时间的增长率和 f ( n ) f(n) 的增长率相同,则可记作: T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) ,称 T ( n ) T(n) 为算法的(渐进)时间复杂度。
T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) 表明:算法的执行时间 T ( n ) T(n) ,它随着问题规模增大而增大的函数的趋势是和 f ( n ) f(n) 这个函数的趋势是相同的,我们称算法的时间和 f ( n ) f(n) 这个函数是成正比的,或者说,这个算法的时间是和 f ( n ) f(n) 这样一个函数的数量级的。

四、如何估算算法的时间复杂度
任何一个算法都是由控制结构和若干个原操作构成的,这里的原操作本来指的是计算机执行的基本指令,但是当我们用伪码语言或者程序语言描述算法的时候,就将固有数据类型的操作看做是原操作,即算法 = 控制结构 + 原操作(固有数据类型的操作)
时间复杂度考虑的是: 随着问题规模n的增长而增长,时间复杂度是一种趋势。一个算法中有很多的原操作,既然我们考虑的是变化的趋势,那么就不必考虑所有的原操作,而只需要在所有的原操作中选取一种所谓基本操作的原操作就行了。这个基本操作的原操作,对于研究的问题来说,它是在所有的原操作中起决定作用的,那么就以该基本操作在算法中重复执行的次数作为算法执行时间的衡量准则
说明:算法的执行时间 = ∑原操作(i)的执行次数 × 原操作(i)的执行时间,其中,原操作(i)的执行时间,对于不同的算法来说,它都是一个定值。因此,在估计算法的时候,往往都会把它忽略掉,也就是说,算法的执行时间与原操作的执行次数之和成正比。

例一、两矩阵相乘

for (i = 1; i <= n; i++)
		for (j = 1; j <= n; j++) 
		{
			c[i][j] = 0;
			for (k = 1; k <= n; k++)
				c[i][j] += a[i, k] * b[k][j];
		}

这个算法的控制结构是一个三重循环,这里的原操作有赋值、有相加、有相乘。显然,乘法操作是这个算法的基本操作,那么可以用乘法操作执行的次数作为该算法时间复杂度的衡量标准。乘法操作在这个三重循环之内,外循环n次,第二重n次,第三重n次,因此这个乘法操作执行的次数为n3,整个算法的执行时间与n3成正比,则这个算法的时间复杂度为: O O (n3)。

例二、选择排序

void select_sort(int a[], int n)
	{
		//将a中整数序列重新排列成自小到大有序的整数序列
		for (i = 0; i < n - 1; i++)
		{
			j = i;
			for (k = i + 1; k < n; k++)
				if (a[k] < a[j]) j = k;
			if(j!=i) a[j]←→a[j]
		}
	}//选择排序

这个算法的控制结构是两重循环,原操作有赋值,有比较,有交换。显然,基本操作取比较操作。比较操作在两重循环以内,外层循环n次(实际上是n-1次,但是为了方便,可记作n次),内层循环的次数随着i的变化而变化,依次为n-1,n-2,…,1,总次数为等差数列之和,等于 n ( n 1 ) 2 \frac{n(n-1)}{2} ,即 1 2 n 2 1 2 n \frac{1}{2}{n^2}-\frac{1}{2}{n} 。所以,算法的执行时间与 1 2 n 2 1 2 n \frac{1}{2}{n^2}-\frac{1}{2}{n} 成正比,当然与n2成正比,时间复杂度为 O O (n2)。

上面两个例子有两个共同的特点:
1、一般情况下,算法的基本操作都是在最深层的循环语句中。假如称语句的执行次数为语句的频度,那么在估算算法的时候也可以计算最深层循环中语句的频度,以这个语句频度的函数作为该算法的时间复杂度。
2、算法的效率与输入数据无关。它只是问题规模的函数,无论输入的数据是什么样的,按问题规模的大小,每一步操作都需要进行。但有的算法不是这样的。

例三、冒泡排序

void bubble_sort(int a[], n)
{
	//将a中整数序列重新排列成从小到大的有序的整数序列
	for (i = n - 1, change = true; i > = 1 && change; i--)
	{
		change = false;
		for (j = 0; j < i; j++)
		{
			if (a[j] > a[j + 1])
			{
				a[j]←→ a[j + 1];
				change = ture;
			}
		}
	}
}//bubble_sort

外循环次数取决于 i i 和change,如果一开始的整数序列为[8,7,6,5,4,3,2,1],显然,这个外循环需要进行n-1次;而一开始的整数序列为[1,2,3,4,5,6,7,8,],那么这个外循环只进行1次。分别对应的是最坏的情况和最好的情况,而基本操作比较操作,分别为 1 2 n 2 1 2 n \frac{1}{2}{n^2}-\frac{1}{2}{n} 次和n-1次。这时,算法的效率不仅与问题规模n有关,还与输入数据的取值有关。而时间复杂度通常取最坏的情况 O O (n2)。

说明:在有的时候,也可以考虑平均的时间复杂度,所谓平均的时间复杂度是在统计概率的情况下。比如,初始的序列是随机的,也就是出现任何情况的概率都是相同的,这时可以取最好情况下和最坏情况下的平均作为它的平均时间复杂度。对于冒泡排序来说,它的平均时间复杂度仍为 O O (n2)。

五、算法的存储空间需求
与算法的时间复杂度类似,对应讨论的是算法的(渐进)空间复杂度。
算法的空间复杂度: S ( n ) = O ( g ( n ) ) S(n)=O(g(n)) 表示随着问题规模n的增大,算法运行所需存储量的增长率与g(n)的增长率相同。

算法的存储量包括:
1、输入数据所占的空间
2、程序本身所占的空间
3、辅助变量所占的空间

说明:任何算法写成程序后,本身都会占有一定的空间,不同的算法占有的空间可能会不同,但是这个差距是细微的,一般情况下,可以不考虑。若输入数据所占空间只取决于问题本身,则只需要分析除输入和程序之外的辅助变量所占的额外空间
若所需的额外空间相对于输入数据量来说是一个常数,则称此算法为原地工作
若所需的额外空间依赖于特定的输入,则通常按最坏的情况考虑。

发布了42 篇原创文章 · 获赞 30 · 访问量 7186

猜你喜欢

转载自blog.csdn.net/Mr____Cheng/article/details/103941985