前言
在参加面试的时候,多多少少都会问到一些关于算法的知识。
这其实是有原因的:在多个人专业知识相同的情况下,公司为什么选择放弃他人而选择你,其中的一个因素就是看你的算法基础。
本文将详细介绍算法的基础概念,如果对算法不太理解的同学可以借鉴参考。
算法基础
一、概念
根据《算法导论第三版》中的描述:算法就是任何问题的解决过程,它接收一些值或集合,对这些值或集合进行加工,最后产生一些值或集合作为输出,算法指的就是将输入转换为输出这个过程中的一系列计算流程。
简而言之,我们可以说算法就是解决一个特定任务的一系列步骤。
二、特征
一个算法应具有以下五个重要特征:
- 有穷性:算法必须在执行有限个步骤后终止,如果你设计的算法永无休止地尝试解决问题,那么它是无用的;
- 确切性:算法的每一步指令都必须具有确切的意义,并且在任何场景下,每一步指令都应该没有歧义;
- 有效性(可行性):一个算法设计出来是用以解决某个问题的,如果你设计的算法不能解决问题,那么它也是无用的;
- 输入项:一个算法有0个或者多个输入,用来初始化算法,这主要看你设计的算法需不需要输入参数;
- 输出项:一个算法有1个或者多个输出。没有输出项的算法是毫无意义的,输出项反映了数据加工后的结果。
三、评定
针对同一个问题,可以用多种算法来解决,不过具体用哪个就由每个算法的效率来决定了。一个算法的质量优劣将会影响程序的执行效率。
那么如何分析一个算法的优劣呢?主要是从时间复杂度和空间复杂度来考虑:
1. 时间复杂度
指执行算法所需要的计算工作量,简单点说就是指执行算法所需要消耗的时间。
一个算法执行所耗费的时间,从理论上来讲是不能算出来的,必须通过计算机运行测试才能知道。
不过一个算法具体耗费多少时间我们并不关心,我们只需要知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。
一个算法花费的时间与算法中语句的执行次数成正比例。哪个算法中语句执行次数多,它花费的时间就多。
此时我们引入时间频度的概念,记为T(n)。
n指的是问题的规模,当n不断变化时,时间频度T(n)也会不断变化。
我们想要呈现出它的变化规律,为此,我们引入了时间复杂度的概念。
一般来说,假设这个算法是问题规模为n的函数f(n),那么时间复杂度就记做:
T(n) = O(f(n))
O在表达式中代表着最坏的情况,这里最坏的情况就是n = 无穷大。
简单来说,上述公式表达的含义就是在n趋于无穷大的时候,T(n) = f(n)。
f(n)指的是具体的函数,虽然对f(n)没有规定,但是我们一般尽可能地去简化f(n)。
例如:O(n2+n+1)=O(3n2-n+5)=O(9n2+5)=O(n2)
注意大O中是携带一个常数C的,所以f(n)一般不加系数。
为什么可以这样换算,为什么最后都可以换算为O(n2)?
这主要是因为当n趋于无限大时, n2 是远远大于 Cn的,也就是高次方项远远大于低次方项,导致低次方项完全可以忽略不计。
这就像是什么,我们把n2+3n+1想象为一棵树的话,n2就是主干部分,3n+1就是一些细枝末节,是完全可以忽略的。
下面我们就来举几个示例,计算它们的时间复杂度:
第一题:
int x = 0; //执行1次
int y = 5; //执行1次
x = 10; //执行1次
int sum = x+y; //执行1次
以上四条语句的频度均为1,该程序的执行时间是一个与n完全无关的常数。算法的时间复杂度就是常数阶,记做:T(n) = O(1)
假设这个算法有上千条语句,因为它的执行时间不会随着n的增长而变化,其执行时间也不过是一个较大的常数。
此类与n毫无关联的算法的时间复杂度均可以记为:T(n) = O(1)。
第二题:
int number = 0; //执行一次
for(int i =0;i<n;i++){ //执行n+1次,注意为什么是n+1,因为当条件不满足时,也执行了一次判断
number++; //执行n次
}
语句一的频度为1。
语句二的频度为n+1。
语句三的频度为n。
所以上述代码的时间复杂度为:T(n) =1 + n+1 + n = 2n+2。
接着当n趋于无限大时,我们只需保留最高的次方项,即:T(n) = O(2n),接着我们将系数省略到O中,即T(n)=O(n)。
第三题:
int number = 0; //执行1次
for(int i =0;i<n;i++){ //执行n+1次
for(int j =0;j<n;j++){ //执行(n+1)*n次
number++; // 执行n*n次
}
}
第三题可能有些难度,是一个嵌套的for循环,我们来一句一句分析。
语句一是一个频度为1的语句,和n无关,记为1。
语句二是一个外层for循环,执行次数为n+1,1代表的是当条件不满足时,结束循环的那次操作。
语句三是嵌套的for循环,自身的执行次数也是n+1,但是配合上外层的for循环,将会执行n次嵌套for循环,所以语句三的频度为:(n+1)*n。
语句四是嵌套for循环中的语句,一次嵌套for循环中的语句需要执行n次,而嵌套for循环也需要执行n次,所以语句四的频度为:n*n。
那么整个程序的时间复杂度即为:T(n) = 1+ n+1 + (n+1) * n + n * n = 2n2 +2n + 2。
当n趋于无限大时,只保留最高次方项,并且省略系数,即:T(n) = O(n2);
通过上面三题的练习,各位看官对时间复杂度的计算过程应该已经了解了。
其实时间复杂度的真正计算,还是比较简单的,难在时间复杂度概念理解。
接下来我们来看一下常见的时间复杂度函数有哪些:
从图中可以看出,随着n的变化,时间复杂度各个函数的变化规律。
这些常见的时间复杂度大小排序如下:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)<…<Ο(2n)<Ο(n!)
我们应该尽可能使用时间复杂度小的算法,从而提升程序的运行效率。
2. 空间复杂度
算法的空间复杂度指的是一个算法所消耗的存储空间。
一个算法在计算机所占用的空间,由三方面决定:
- 存储算法本身所占用的空间;
- 算法输入输出的数据所占用的空间;
- 算法在运行过程中临时占用的空间。
存储算法本身所占用的空间与算法书写的长短成正比,要压缩这方面的存储空间,就必须编写出较短的算法。
算法输入输出数据所占用的空间是由这个算法要解决的问题决定的,针对同一问题,它占用的空间不会随着算法的变化而改变,但它确实占用着空间。
算法在运行过程中临时占用的空间会根据算法的不同而产生差异,也是我们最需要着重关心的一点。
有些算法只需要占用很少的临时工作单元,并且不会随问题规模的变大而变大,我们称这种算法是“就地”进行的,是节省存储的算法。
有些算法占用的临时工作单元数与解决的问题规模n有关,占用空间随着n的增大而增大,当n较大时,将占用较多的存储单元,一些常见的排序算法就属于这种情况,比如快速排序、归并排序,属于占用较多存储空间的算法。
当一个算法的空间复杂度不会随着问题规模的变化而变化时,可表示为O(1)。
当一个算法的空间复杂度与问题规模n呈线性比例关系时,可表示为O(n)。
同理,其他变化关系也可以推导出来。同时间复杂度相比,空间复杂度的分析还是比较简单的。
3. 其他
除了上述两条评定规则外,评价一个算法的质量还有其他几个因素:
- 正确性:算法的正确性是评价一个算法优劣最重要的标准!这个算法的结果是错误的,那么这个算法毫无意义。
- 可读性:可读性指的是人们理解这个算法的难易程度,当时间复杂度与空间复杂度相同的情况下,当然要选择易于人们理解的算法。
- 健壮性:健壮性是指一个算法对不合理数据输入的反应能力和处理能力,也称为容错性。
总结
本文是算法最基本的一些知识,看完本文,需要你完全理解下图的含义:
后续还会针对算法,做一些更深入的分析,感谢各位看官的浏览。
鞠躬。