《大话数据结构》读书笔记(二)

第二章 算法
2.1两种算法的比较
计算 1+2+3+4+...+99+100的和
1.大多数人会想到
int sum =0;int n=100
for(int i = 0;i<=n;i++){
sum = sum+i;
}
System.out.println(sum);
2.高斯的算法
int sum= 0; n=100;
sum = (1+n) * n/2;
System.out.println(sum);
2.2算法定义
算法:是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。
算法定义中,提到了指令,指令能被人或机器等计算装置执行。他可以是计算机指令,也可以是我们平时的语言文字。
为了解决某个或某类问题,需要把指令表示成一定的操作序列,操作序列包括一组操作,每一个操作都完成特定的功能,这就是算法了。
2.3算法的特性
算法具有五个基本特性:输入、输出、有穷性、确定性和可行性。
2.3.1 输入输出
输入:算法具有零个或多个输入。
输出:算法至少有一个或多个输出。
2.3.2 有穷性
有穷性:指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。
2.3.3 确定性
确定性:算法的每一步骤都具有确定的含义,不会出现二义性。算法在一定条件下,只有一条执行路径,相同的输入只能有唯一的输出结果。算法的每个步骤被精确定义而无歧义。
2.3.4 可行性
可行性:算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成。可行性意味着算法可以转化为程序上机执行,并得到正确的结果。
2.4算法设计的要求
2.4.1正确性
正确性:算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性、能正确反映问题的需求、能够得到问题的正确答案。
算法的“正确”通常在用法上有很大的差别,大体分为以下四个层次:
1.算法程序没有语法错误。
2.算法程序对于合法的输入数据能够产生满足要求的输出结果。
3.算法程序对于非法的输入数据能够得到满足规格说明的结果。
4.算法程序对于精心选择的,甚至***难的测试数据都有满足要求的输出结果。
2.4.2 可读性
可读性:算法设计的另一目的是为了便于阅读、理解和交流。
可读性高有助于人们理解算法,晦涩难懂的算法往往隐含错误,不易被发现,并且难于调试和修改。
我们写代码的目的,一方面是为了让计算机执行,但还有一个重要的目的是为了便于他人阅读,让人理解和交流,自己将来也可能阅读,如果可读性不好,时间长了自己都不知道写了什么。可读性是算法(也包括实现它的代码)好坏很重要的标志。
2.4.3 健壮性
一个好的算法还应该能对输入数据不合法的情况做合适的处理。比如输入的时间或者距离不应该是负数等。
健壮性:当输入数据不合法时,算法也能做出相关处理,而不是产生异常或者莫名其妙的结果。
2.4.4 时间效率高和存储量低
时间效率指的是算法的执行时间,对于同一个问题,如果有多个算法能够结局,执行时间短的算法效率高,执行时间长的效率低。
存储量需求指的是算法在执行过程中需要的最大存储空间,主要指算法程序运行时所占用的内存或外部硬盘存储空间。
设计算法应该尽量满足时间效率高和存储量低的需求。
综上所诉,一个好的算法,应该具有正确性、可读性。健壮性、高效率和低存储量的特征。
2.5 算法效率的度量方法
2.5.1 事后统计方法
事后统计方法:这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序运行时间进行比较,从而确定算法效率的高低。
但这种方法显然有很大的缺陷的:
1.必须依据算法实现编制好程序,这通常需要花费大量的时间和经理。
2.时间的比较依赖计算机硬件和软件等环境因素,有时会掩盖算法本身的优劣。
3.算法的测试书卷设计困难,并且程序的运行时间往往还与测试数据的规模有很大关系,效率高的算法在小的测试数据棉签往往得不到体现。
基于事后统计方法缺陷种种,考虑不予采纳。
2.5.2事前分析估算方法
事前分析估算方法:在计算机程序编制前,依据统计方法对算法进行估算。
一个高级程序语言编写的程序在计算机运行时所消耗的时间取决于下列因素:
1.算法采用的策略、方法。
2.编译产生的代码质量。
3.问题的输入规模。
4.机器执行指令的速度。
第1条当然是算法好坏的根本,第2条由软件支持,第4条要看硬件的性能。抛开这些因素,一个程序的运行时间,依赖于算法的好坏和问题的输入规模。所谓问题输入规模是指输入量的多少。
比如,还是 1+2+3+....+99+100的例子
第一种算法执行了 1+(n+1)+n+1 = 2n+3次 第二种算法执行了 1+1+1 =3次
将上述案例扩展
这个例子中,i 从1到100每次都要让 j 循环100 次,而当中x++和sum = sum + x 也就需要 n * n 次。两种算法随着 n 的增加, 循环计算的开销远大于 高斯的计算方式。
在分析程序的运行时间时,最重要的是把程序看成是独立于程序设计语言的算法或一系列步骤。
2.6 函数的渐近增长
案例判断一
假设两个算法的输入规模都是 n ,算法A 需要2n+3 次,算法B需要 3n+1次,谁的效率更快?
当 n = 1 时,算法B要优于算法A ,当n=2时,算法A 优于算法B,当n 不断增加,算法A要越来越优于算法B。
函数的渐近增长:给定两个函数 f(n) 和 g(n) ,如果存在一个整数N,使得对于所有的 n > N
,f(n)总是比 g(n)大,那么,我们就说 f(n) 的增长渐近快于 g(n)。
案例判断二
算法C 是 4n+8,算法D是 2n^2+ 1 判断优劣快慢。
当n ≤ 3 的时候,算法C 要茶于算法D ,但当n > 3后,算法C的优势就越来越优于算法D了,到后来更是远胜。当后面的常数去掉之后,我们发现结果并没有改变。也就是说,与最高次项相乘的常数并不重要。
案例判断三
算法E 2n^2+3n+1 ,算法F是 2n^3+3n+1
当n=1时,算法E和算法F的结果相同,但当n >1 后,算法E的优势就要开始优于算法F了,随着 n 的增大,差异非常明显。通过观察发现,最高次项的指数大的,函数随着 n 的增长,结果也会变得增长特别快。
案例判断四
算法G: 2n^2 ,算法H是 3n+1,算法I 是 2n^2+3n+1
当n值越来越大时,发现 3n+1已经没办法和2n^2的结果相比较。也就是说,随着n值变得非常大以后。算法G其实已经很趋近与算法 I 。于是我们可以得到结论:判断一个算法的效率时,函数中的常数和其他次要项通常可以忽略,而更应该关注主项(最高阶项)的阶数。
某个算法,随着n 的增大,他会越来越优于另一算法,或者越来越差于另一算法。
2.7 算法时间复杂度
2.7.1 算法时间复杂度定义
在进行算法分析时,语句总的执行次数 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(n) ,O(1),O(n^2),我们分别给他们取了非官方的名称 O(n) 叫线性阶,O(1)叫常数阶,O(n^2)叫平方阶。
2.7.2 推到大O阶方法
推到大O阶:
1.用常数1取代运行时间中的所有加法常数。
2.在修改后的运行次数函数中,只保留最高阶项。
3.如果最高阶项存在且不是1,则去除与这个项相乘的常数。
得到的记过就是大O阶。
2.7.3 常数阶
首先顺序结构的时间复杂度。
int sum =0,n=100; /* 执行一次 */
sum = (1+n) * n/2; /* 执行一次 */
Sysout.out.println(sum); /* 执行一次 */
这个算法的运行次数函数是 f(n) =3 根据大O阶推导方法,把常数项改为 1 。在保留最高阶项时发现,它根本没有最高阶项,所以这个算法的大O阶时间复杂度为O(1)
执行时间恒定的算法,我们称之为具有O(1)的时间复杂度,又叫常数阶。
注意:不管这个常数是多少,我们都记为 O(1) 不能写为O(3),O(12)等其他任意数字,这是初学者常常犯的错误。
对于分支结构而言,无论是真还是假,执行的次数都是恒定的,不会随着n的变大而发生变化,所以单纯的分支结构(不包含循环结构中),其时间复杂度也是O(1)。
2.7.4线性阶
线性阶的循环结构会复杂很多。要确定某个算法的阶次,我们常常需要确定某个特定语句或某个语句集运行的次数。因此,我们要分析算法的复杂度,关键就是要分析循环结构的运行情况。
循环的时间复杂度为O(n)的案例
int i ;
for(i = 0; i < n; i++){
/*时间复杂度为O(1)的程序步骤序列*/
}
2.7.5 对数阶
int count = 1;
while( count < n ){
count = count * 2;
/*时间复杂度为O(1)的程序步骤序列*/
}
由于每次count 乘以2之后,就距离n更近了一分。也就是说,有多少个2相乘后大于 n ,则会退出循环。由于 2^x = n 得到 x = log2(n) 所以这个循环的时间复杂度为O(logn)。
2.7.6 平方阶
案例1:
int i , j ;
for(i = 0; i < n ; i ++){
for( j = 0 ; j < n; j ++){
/* 时间复杂度为O(1)的程序步骤序列 */
}
}
对于外层循环,不过是内部这个时间复杂度为O(n) 的语句,在循环 n 次。所以这段代码的时间复杂度为 O(n^2)。
如果外侧循环次数改为了 m ,时间复杂度就变为 O(m*n)。
int i , j ;
for(i = 0; i < m ; i ++){
for( j = 0 ; j < n; j ++){
/* 时间复杂度为O(1)的程序步骤序列 */
}
}
所以我们可以总结出结论,循环的时间复杂度等于循环体的复杂度乘以该循环的运行次数
案例2:
int i , j ;
for(i = 0; i < n ; i ++){
for( j = i ; j < n ; j ++){ /* 注意 j = i 而不 j = 0*/
/* 时间复杂度为O(1)的程序步骤序列 */
}
}
由于当i = 0 时,内循环执行了n次,当i = 1 时。执行了 n-1 次,....当i = n-1 时,执行了 1次。所以总次数为 n +(n-1)+(n-2)+....+1 = n(n+1)/2 = n^2/2+n/2
用我们推导大O阶的方法,第一,没有加法常数,不予考虑,第二只保留最高阶项,因此保留 n^2/2;第三去除这个项相乘的常数,也就是去除 1/2 ,最终这段代码的时间复杂度为O(n^2)。
由此案例,我们可以理解 大O推导不算难,难的是对数列的一些相关运算,这更多的是考察你的数学知识和能力。
2.8常见的时间复杂度
2.9最坏情况与平均情况
最坏情况运行时间是一种保证,那就是运行时间将不会再坏了。在应用中,这是一种最重要的需求,通常,除非特别指定,我们提到的运行时间都是最坏情况的运行时间。
而平均时间也就是从概率的角度看,这个数字在每一个位置的可能性是相同的,所以平均的查找时间为n/2次后发现这个目标元素。
平均运行时间是所有情况中最有意义的,因为他是期望的运行时间。也就是说,我们运行一段程序代码时,是希望看到平均运行时间的。可现实中,平均运行时间很难通过分析得到,一般都是通过运行预定数量的实验数据后估算出来的。
对于算法的分析,一种方法是计算所有情况的平均值,这种时间复杂度的计算方法称为平均时间复杂度。另一种方法是计算最坏情况下的时间复杂度,这种方法称为最坏时间复杂度。一般在没有特殊说明的情况下,都是指最坏时间复杂度。
2.10算法空间复杂度
算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n) = O(f(n)),其中 n 为问题的规模,f(n) 为语句关于n 所占存储空间的函数。
一般情况下,一个程序在机器上执行时,除了需要存储程序本身的指令、常数、变量和输入数据外,还需要存储对数操作的存储单元。若输入数据所占空间只取决于问题本身,和算法无关,这样只需要分析该算法在实现时所需的辅助单元即可。若算法执行时所需的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为O(1)。
2.11总结回顾
1.算法的定义:算法是解决特定问题求解步骤的描述,在计算机中为指令的有限序列,并且每条指令表示一个或多个操作。
2.算法的特性:有穷性、确定性、可行性、输入、输出。
3.算法的设计要求:正确性、可读性、健壮性、高效率和低存储量
4.算法特性与算法设计容易混,需要对比记忆。
5.算法的度量方法:事后统计方法(不科学、不准确)、事前分析估算方法。
6.函数的渐近增长:给定两个函数 f(n) 和 g(n),如果存在一个整数N,使得对于所有的n > N ,f(n)总是比g(n) 大,那么,我们就说f(n) 增长渐近快于 g(n)。于是我们可以得出一个结论,判断一个算法好坏,我们只通过少量数据是不能做出准确判断的,如果我们可以对比算法的关键执行次数函数的渐近增长性,基本就可以分析出:某个算法,随着n的变大,他会越来越优于另一算法,或者越来越差于另一算法。
7.推导大O阶:
a.用常数1取代运行时间中的所有加法常数
b.在修改后的运行次数函数中,只保留最高阶项
c.如果最高阶项存在且不是1,则去除这个项相乘的常数。
得到的结果就是大O阶。
8.常见的时间复杂度所耗时间大小比较
9.最坏情况和平均情况
10.空间复杂度
posted @ 2018-07-20 14:51  僵尸吃过跳跳糖  阅读(138)  评论(0编辑  收藏  举报