一、什么是算法?
算法是解决特定问题求解步骤的描述,在计算机中则表现为一个或多个指令的组合规则;常用的经典算法有:循环累加求和/高斯求和、二叉树查找、冒泡排序、散列算法查找(哈希表)等。
二、算法与数据结构的关系
我们之前章节大概阐述了数据结构与算法之间的关系。这里为啥也要重点说明一下呢?事实上,数据结构和算法的关系非常紧密,只谈数据结构,不谈算法,当然也是可以的。但是你可能听完后没什么感觉,也不知道数据结构有什么用。如果我们结合算法来讲,你可以很清晰的了解计算机前辈们为啥要这样设计数据结构,这样设计的好处是什么。
先来看点实际的吧,我想大家或多或少在大学里学过C、会写点程序;比如这里有个程序要求,1+2+3+....+100求和,你应该怎么写呢?
循环求和法方式(学过C语言的,我想大多数人都会这样写):
int i,sum = 0, n = 100; //执行1次
for (i = 1;i <= n; i++) { //执行了n+1次
sum = sum + i; //执行n 次
}
printf(" %d",sum); //执行1次
看到以上代码,我们觉得是简单的写法,但是还有没有更高效的写法吗?
此时,我们不得不提一个伟大的数学家高斯,据说18世纪生于德国小村庄的高斯,上小学的一天,课堂很乱,老师很生气,老师就要求每个学生都计算1+2+...+100的结果,谁先算出来谁先回家。天才当然不会被这样的问题难倒,高斯很快就得出答案,是5050。老师非常惊讶,因为他自己想必也是通过1+2=3,3+3=6,6+4=10,...,4950+100=5050 这样算出来的,也算了很久很久。说不定为了怕错,还算了N遍。可眼前这个少年,为何可以这么快地得出结果?
现在来谈高效的第二种算法(高斯求和算法或者说是求等差数列算法)
int i,sum = 0, n = 100; //执行1次
sum = (1 + n ) *n / 2; //执行1次
printf("%d", sum); //执行1次
看现在代码是不是比之前高效啊,不仅可以很快算出100,甚至1千,1万,1亿也可以瞬间算出;如果用之前循环算法,循环1亿次显然不可取;
通过上述概念的解释及问题实践,我想大家对算法有一个清晰的认识;当然任何算法都没有完美可以解决任何问题,必须根据不同的问题选择合适的算法求解,这是我们需要掌握的技能。
三、算法的特性
算法具有五个基本特性:输入、输出、有穷性、确定性和可行性
输入输出
输入和输出特性比较容易理解,输出可以理解为打印,如之上代码中的printf。那么输入则是传入的数据,比如1到100的数据;
有穷性
指算法的执行是有限的。执行完结果后自动终止,不会造成死循环。实践中经常会写出死循环代码,这就是不满足有穷性。当然这里有穷的概念并不是纯数学意义的,而是在实际应用当中合理的、可以接受的边界。比如说你写个计算结果的算法,可能要等待20年后才有。那这个算法的意义何在是一个道理。
确定性
算法的每个步骤都是具有确定的含义,不会出现歧义。算法在一定条件夏,只有一条执行路径,相同的输入只能有唯一的输出结果。
可行性
算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成。并得到正确结果。
四、算法设计准则
正确性
算法的正确性是算法至少应该具有输入、输出和无歧义。能够得到问题的正确答案。
可读性
算法设计的目的是为了便于阅读、理解和交流。
健壮性
当输入数据不合法是,算法也能做出相应的容错处理。而不是产生异常或莫名其妙的结果
高时效性和存储量低
好的算法还应该具备执行效率高和存储量低的特点。遵循低成本高效率原则。
综上,好的算法,应该具备正确性、可读性、健壮性、高效率和低存储量的特点。
五、算法效率的度量方法
刚才我们提到设计算法要提高效率。这里效率大多数指执行算法需要的时间。那么我们如何度量一个算法的执行时间呢?比较容易想到的方法就是,我们通过对算法的数据测试,利用计算机的计时功能,来计算不同算法的效率高低。
事后统计方法
这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定算法效率高低。
缺点:
1、必须依据算法事先编制好程序,这通常需要花费大量时间和精力。
2、时间的比较依赖计算机硬件和软件等外界因素,有时会掩盖算法本身的优势。要知道,现在的一台4核处理器的计算机,跟当年286、386、486等老款机器相比,在处理算法的运行速度上,是完全不相同的;而所用的操作系统、编译器、运行框架等软件的不同,也有很大影响。就算是同一台机器,当前时间段cpu使用率和内存占用情况不一样,也会造成细微差异。
3、算法的测试数据设计困难,并且程序的运行时间往往还与测试数据的规模有很大关系,效率高的算法在小的测试数据面前往往得不到体现。比如10个数字的排序,不管用什么算法,差异几乎为零。而如果有一百万个随机数字排序,那不同算法的差异就非常大。那么我们为了比较算法,到底准备多少数据测试,这是很难判断的问题。
所以我们不考虑这种事后统计方法作为度量方法。
事前分析估算方法
在计算机程序编写钱,依据统计方法对算法进行估算。
程序在计算机上运行时所消耗的时间取决于下列因素:
1、算法采用的策略、方法。(这是评估算法好坏的根本)
2、编译产生的代码质量。(通过软件来支持)
3、问题的输入规模。(指输入的数据量)
4、机器执行指令的速度。(硬件性能决定)
我们看看上面的代码的执行次数。不难发现第1种算法执行了1+(n+1)+n+1=2n+3次;而第二种算法,是1+1+1=3次。事实上两个算法的第一条和最后一条语句是一样的,所以我们关注的代码其实中间的那部分,我们把循环看作一个整体,忽略头尾循环判断的开销,那么这两个算法其实就是n次与1次的差距。
由此可以看出测定运行时间最可靠的方法就是计算对运行时间有消耗的基本操作的执行次数。运行时间与执行次数成正比。
六、函数的渐近增长
现在判断一下,两个算法A和B哪个更好。假设两个算法的输入规模都是n,算法A要做2n+3次操作,你可以理解为先有一个n次的循环,执行完后,再有一个n次循环,最后有三次赋值或运算,共2n+3次操作。算法B要做3n+1次操作。你觉得它们谁更快?
准确说,答案不一定。看下面表格:
次数 | 算法A(2n+3) | 算法B(3n+1) |
n=1 | 5 | 4 |
n=2 | 7 | 7 |
n=3 | 9 | 10 |
n=10 | 23 | 31 |
n=100 | 203 | 301 |
当n=1时,算法A效率不如算法B,而当n=2时,两者效率相同;当n>2时,算法A就开始优于算法B了,随着n的增加,算法A比算法B越来越好了。于是我们可以得出结论,算法A总体上要好过算法B。由此定义,输入规模n在没有限制的情况下,只要超过一个数值N,这个函数就总是大于另一个函数,我们称函数是渐近增长。
判断一个算法好不好,我们只通过少量数据是不能做出准备判断的。根据刚才的例子,我们发现,如果我们可以对比这个几个算法的关键执行次数函数的渐近增长性,基本就可以分析出:某个算法,随着n的增大,它会越来越优于另一算法,或者越来越差于另一算法。这其实就是事前估算方法的理论依据,通过算法时间复杂度来估算算法时间效率。
七、算法时间复杂度
进行算法分析是,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)=O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称做算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。这样用大写O()来体现算法时间复杂度的记法,我们称之为大O记法。一般情况下,随着n的增大,T(n)增长最慢的算法为最优算法。
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项相乘的常数。
通过以上方式得到的结果就是大O阶。
常见的时间复杂度如下表:
执行次数函数 | 阶 | 非正式术语 |
12 | O(1) | 常数阶 |
2n+3 | O(n) | 线性阶 |
3n²+2n+1 | O(n²) | 平方阶 |
5log2n+20 | O(logn) | 对数阶 |
2n+3nlog2n+19 | O(nlogn) | nlogn阶 |
6n³+2n²+3n+4 | O(n³) | 立方阶 |
2" | O(2") | 指数阶 |
常用时间复杂度所耗费的时间从小到大依次是:
0(1) < O(logn) < O(n) < O(n l ogn) < 0(n 2 ) < 0(n 3 ) < 0(2 ") < O(n!) < O(n")
总结:有很多程序员做了很长时间开发工作,却不清楚时间复杂度估算,所以就从来不研究自己的代码是否效率低下,是不是可以通过优化让计算机更加快速高效。他们的借口是,现在CPU越来越快,根本不用考虑算法的优劣,实现功能即可,用户感觉不到算法好坏造成的快慢。可事实却不是这样。所以希望大家在今后工作学习中,好好利用算法分析工具,改进自己的代码,让计算机轻松点,这样你就更加胜人一筹。