个人主页:聆风吟
系列专栏:图解数据结构、算法模板
少年有梦不应止于心动,更要付诸行动。
算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。
算法具有五个基本特性:输入、输出、有穷性、确定性和可行性。
算法具有零个或多个输入,尽管对于大多数算法来说,输入参数都是有必要的,但对于个别情况,如打印"hello world!"
这样的代码,不需要任何输入参数,因此算法的输入可以是零个。
算法至少有一个或多个输出,算法是一定需要输出的,不需要输出,你用这个算法干嘛呢?输出的形式可以打印输出,也可以是返回一个或多个值等。
有穷性:是指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤都在可接受的时间内完成。
确定性:算法的每一步骤都具有确定的含义,不会出现二义性。算法在一定条件下,只有一条执行的路径,相同的输入只能有唯一的输出结果。算法的每一步骤都被精确定义而无歧义。
可行性:算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成。
算法不是唯一的。也就是说,解决同一个问题,可以有多种解决问题的算法。通常为了设计一个 “好” 的算法应考虑达到一下目标:
正确性:算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性,能够得到问题的正确答案。但是算法的 “ 正确 ” 一词在用法上通常有很大差别,大体分为一下四个层次:
对于这四层含义,层次 1
的要求最低,但仅仅没有语法错误实在谈不上是好算法。这就是如同仅仅解决温饱,不算是生活幸福一样。而层次 4
是最难实现的,我们几乎不可能逐一验证所有的输入都得到正确的结果。所以一般情况下,我们通常把层次 3
作为衡量一个算法算法是否合格的标准。
可读性:算法的另一个目的是为了便于阅读,来理解和交流。可读性高有助于人们理解算法,晦涩难懂的算法往往隐含错误,不易被发现,并且难以调试和修改。
健壮性:当输入数据不合法时,算法也能做出相关处理而不是产生异常或莫名其妙的结果。
时间效率指的是算法的执行时间。对于同一个问题,如果有多个算法能够解决,执行时间短的算法效率高,执行时间长的效率低。存储量需求指的是算法在执行过程中需要的最大空间,主要指算法程序运行时所占用的内存或外部硬盘存储空间。因此,设计算法时应尽量满足时间效率高和存储量低的需求。
刚才我们提到了设计算法要提高效率。这里的效率大都指算法的执行时间。算法的执行时间需要依据该算法编制的程序在计算机上运行时所消耗的时间来度量的。而度量一个程序的执行时间通常有有两种方法 —— 事后统计方法和事前分析估算方法。
事后统计方法:这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同的算法编制的程序的运行时间进行比较,从而确定算法效率的高低。但是这种方法明显是有很大的缺陷:
基于事后统计方法有这样那样的缺陷,我们一般不予以采纳,而是采用另一种事前分析估算方法。
事前分析估算方法:在计算机程序编制前,依据统计方法对算法进行估算。经过分析我们可以发现,一个用高级语言程序语言编写的程序在计算机上运行时所消耗的时间取决于一下因素:
解析:
第(1)条
是一个好算法的根本,第(2)条
要有软件来支持,第(4)条
要看硬件性能。因此,抛开这些与计算机硬件、软件有关的因素,一个程序的运行时间,依赖于算法的好坏和问题的输入规模。所谓问题输入规模是指输入量的多少。
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
由此可以看出算法复杂度的重要性,所以说同学们下面的内容一定要好好学哦。言归正传接下来让我们开始具体讲解时间复杂度和空间复杂度。
在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
++count;//执行 N*N 次
}
}
for (int k = 0; k < 2 * N; ++k)
{
++count;//执行 2*N 次
}
int M = 10;
while (M--)
{
++count;//执行 10次
}
printf("%d\n", count);
}
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么我们就可以使用大O的渐进表示法。看到这估计有同学该问了大O的渐进表示法是什么,它又是如何推导的呢?且听我慢慢道来,让我们继续接着向下面学习。
大O符号(big O notation):是用于于描述函数渐进行为的数学符号。
推导大O阶方法:
(1)用常数1取代运行时间中的所有加法常数。
(2) 在修改后的运行次数函数中,只保留最高阶项。
(3)如果最高阶项存在且其系数不是1,则去除与这个项相乘的系数。得到的结果就是大O阶。
结合上面示例: 使用大O的渐进表示法以后,Func1的时间复杂度为:
通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
常见的时间复杂度如下表所示:
注:对数在文本中不好表示,以 2 为底的对数通常简写为(logn) 。
常用的时间复杂度所耗费的时间从小到大依次是:
O(1) < O(logn) < O(n) < O(nlogn) < O(2n) < O(n3) < O(2n) < O(n!)
另外有些算法的时间复杂度存在最好、平均和最坏情况:
- 最坏情况:任意输入规模的最大运行次数(上界)
- 平均情况:任意输入规模的期望运行次数
- 最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
在实际中 一般在没有特殊说明的情况下,关注的都是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)。
算法空间复杂度的定义:
注意: 函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
本文主要讲解:
今天的干货分享到这里就结束啦!如果觉得文章还可以的话,希望能给个三连支持一下,聆风吟的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是作者前进的最大动力!