2 算法
2.1 什么是算法?
算法(Algorithm)是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。
使计算机能够理解,并且是能够解决问题的方法,称为算法。算数方法。
数据结构与算法密不可分。
2.2算法的特性
算法具有五个基本特性:输入、输出、有穷性、确定性和可行性。
2.2.1输入输出
算法具有**零个或多个输入。至少有一个或多个输出。**不输出要算法干什么
2.2.2有穷性
指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。
不能写个死循环,没有意义。
同样也涉及到一个边界问题。
2.2.3确定性
算法的每一步骤都具有确定的含义,不会出现二义性。
你要清楚你的程序每一步是干什么的,会不会起冲突。相同的输入只能有唯一输出的结果。
2.2.4可行性
算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成。可行性意味着算法可以转换为程序上机运行,并得到正确的结果。
不能是整出来个死循环或者内存溢出等等。一定要保证正确。
2.3算法设计的要求
**算法是不唯一的。**同一个问题,可以有很多解决的方案。
2.3.1正确性
算法的正确性是指算法至少应该具有输入、输出和加工处理无歧义性、能正确反映问题的需求、能够得到问题的正确答案。
- 算法程序没有语法错误。
- 算法程序对于合法的输入数据能够产生满足要求的输出结果。
- 算法程序对于非法的输入数据能够得出满足规格说明的结果。
- 算法程序对于精心选择的,甚至刁难的测试数据都有满足要求的输出结果。
好算法还有什么特征呢
能够让人容易理解,看得懂!!!一定要写注释!!!写注释!!!写注释!!!
2.3.2可读性
算法设计的另一目的是为了便于阅读、理解和交流。
晦涩难懂的代码难于调试与修改,有时候甚至自己都看不懂,可能当下能看懂,但是过几天就根本看不懂。
*****p
2.3.3健壮性
(鲁棒性)
当输入数据不合法时,算法也能做出相关处理,而不是产生异常或莫名其妙的结果。
2.3.4时间效率高和存储量低
对于同一个问题,解决所花费的时间越短,消耗的内存空间越小越好。效率要高
那么如何比较算法的时间与空间呢?
2.4两种算法的比较
求1+2+3+4+…+100
法一:
int n = 100;
int sum = 0;
for(int i = 1;i<=n;i++)
{
sum += i;
}
printf("sum = %d",sum);
法二:
int n = 100;
int sum = 0;
sum = n*(n+1)/2;
printf("sum = %d",sum);
语句结构
2.5算法效率的度量方法
2.5.1事后统计方法
让计算机运行完毕程序,统计时间和空间的消耗多少。
显然有很大缺陷:
- 如果算法很糟糕,得从头再来。
- 依赖于硬件和环境的要求。
- 算法测试数据设计困难。并且时间大小往往与测试数据量的多少有很大关系。
2.5.2事前分析估算方法
在计算机程序编制前,依据统计方法对算法进行估算。
一个用高级程序语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:
- 算法采用的策略、方法。(根本)
- 编译产生的代码质量。
- 问题的输入规模。
- 机器执行指令的速度。
而对于程序本身,我们一般看语句的执行次数,来反映时间的多少。
int n = 100; //执行一次
int sum = 0; //执行一次
for(int i = 1;i<=n;i++) //执行n+1次
{
sum += i; //执行n次
}
printf("sum = %d",sum);//执行一次
int n = 100; //执行一次
int sum = 0; //执行一次
sum = n*(n+1)/2; //执行一次
printf("sum = %d",sum);//执行一次
相比而言,法一,2n+4次,法二,4次,所以,我们可以直观的得出,方法二的执行算法所消耗的时间是比方法一少的,效率比方法一高。
对于for嵌套
for(int i = 0;i<n;i++)
{
for(int j = 0;j<m;j++)
{
printf("hello");
}
}
//每执行一次外层循环,内部循环都要执行完毕,所以是一个相乘的关系
//n*m次
2.6函数的渐进增长
给定两个函数f (n)和g (n),如果存在一个整数N,使得对于所有的n > N,f (n)总是比g(n)大,那么,我们说f (n)的增长渐近快于g (n)。
- 对于以下算法,给定不同的测试样例:
次数 | 算法A(2n+3) | 算法A1(2n) | 算法B(3n+1) | 算法B1(3n) |
---|---|---|---|---|
n = 1 | 5 | 2 | 4 | 3 |
n = 2 | 7 | 4 | 7 | 6 |
n = 3 | 9 | 6 | 10 | 9 |
n = 4 | 23 | 20 | 31 | 30 |
n = 5 | 203 | 200 | 301 | 300 |
在n≤3时,四种算法各个值都没有明显的大于关系。
在n≥4时,总出现了B>B1>A>A1,我们就可以认为,B的增长渐快于其余算法,也就可以认为B的消耗时间最多。这也是和常理相符合的。
可能前期或者局部情况下要小于另一个函数,但是整体而言是远远大于该函数的。
根据以上我们发现:后面的+3还是+1其实是不影响最终的算法变化。因此,我们可以忽略这些加法常数。
- 例二
次数 | 算法C(4n+8) | 算法C1(n) | 算法D(2n^2+1) | 算法D1(n^2) |
---|---|---|---|---|
n = 1 | 12 | 1 | 3 | 1 |
n = 2 | 16 | 2 | 9 | 4 |
n = 3 | 20 | 3 | 19 | 9 |
n = 10 | 48 | 10 | 201 | 100 |
n = 100 | 408 | 100 | 20 001 | 10 000 |
n = 1000 | 4 008 | 1 000 | 2 000 001 | 1 000 000 |
当n>3时,算法C就明显优于算法D了,由例一可知,去掉常数对于最后的判断结果也没有影响,进一步证明了结论。
而我们再发现,去掉与n相乘的常数,对于最后的结果也没有影响(算法C1与算法D1的比较)。因此,可以认为,与最高次项相乘的常数并不重要。
- 例三
次数 | 算法E(2n^2+3n+1) | 算法E1(n^2) | 算法F(2n^3+3n+1) | 算法F1(n^3) |
---|---|---|---|---|
n = 1 | 6 | 1 | 6 | 1 |
n = 2 | 15 | 4 | 23 | 8 |
n = 3 | 28 | 9 | 64 | 27 |
n = 10 | 231 | 100 | 2 031 | 1 000 |
n = 100 | 20 301 | 10 000 | 2 000 301 | 1 000 000 |
通过例一例二的分析方法,对于例三算法分析,我们可以得出:
最高次项的指数越大,增长越快。
- 例四
次数 | 算法G(2n^2) | 算法H(3n+1) | 算法I(2n^2+3n+1) |
---|---|---|---|
n = 1 | 2 | 4 | 6 |
n = 2 | 8 | 7 | 15 |
n = 5 | 50 | 16 | 66 |
n = 10 | 200 | 31 | 231 |
n = 100 | 20 000 | 301 | 20 301 |
n = 1000 | 2 000 000 | 3 001 | 2 003 001 |
n = 10000 | 200 000 000 | 30 001 | 200 030 001 |
n = 100000 | 20 000 000 000 | 300 001 | 20 000 300 001 |
n = 1000000 | 2 000 000 000 000 | 3 000 001 | 200 000 30000 001 |
当n越来越大时,相比于其他算法而言,算法H的值可以忽略不计。
而对于算法G和算法I,n变得非常大之后,算法G越来越趋向于算法I,因此,判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数。
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法
2.7.2推导大O阶方法(重点)
大O法:
**1.用常数1取代运行时间中的所有加法常数。**O(3) = O(1)
**2.在修改后的运行次数函数中,只保留最高阶项。**O(4n+8) = O(4n+1) = O(4n) = O(n)
3.如果最高阶项存在且不是1,则去除与这个项相乘的常数。得到的结果就是大О阶。 O(3n^2+2) = O(3n^2+1) = O(3n^2) = O(n^2)
下面进行详细说明
2.7.3常数阶
int n = 100; //执行一次
int sum = 0; //执行一次
sum = n*(n+1)/2; //执行一次
printf("sum = %d",sum);//执行一次
根据大O法,把O(4)改为O(1)。怎么理解呢?
时间复杂度并不是反映的算法的一个真实执行过程,而是一个函数。
这种与问题大小无关,执行时间恒定的算法,不会随着n的变化而去变化,我们用常数去表示,而我们常常使用1去表示常数。所以是O(1)
2.7.4线性阶
需要确定某个语句执行的次数。常常为for或while循环。
与n有关。
for(int i = 0;i<n;i++)
{
printf("hello world");
}
O(n)
2.7.5对数阶
int count = 1;
while(count < n)
{
count *= 2;
}
设运行了 x 次,那么最终跳出 w h i l e 循环, c o u n t 的变化为: 2 x = n 。所以, x = log 2 n 设运行了x次,那么最终跳出while循环,count的变化为:2^x = n。所以,x =\log_{2}{n} 设运行了x次,那么最终跳出while循环,count的变化为:2x=n。所以,x=log2n
O(logn)
搞清楚什么时候是x,什么时候是n。
2.7.6平方阶
对于一般的for嵌套我们之前也说过。
for(int i = 0;i<n;i++)
{
for(int j = 0;j<m;j++)
{
printf("hello");
}
}
O(nxm)
那么对于以下代码
for(int i = 0;i<n;i++)
{
for(int j = i;j<n;j++)//注意,是int j = i;
{
printf("hello");
}
}
i = 0时,内循环执行n次。
i = 1时,内循环执行n-1次。
i = 2时,内循环执行n-2次。
…
所以,n+n-1+n-2+…+1 = n(n+1)/2 = n^2/2+n/2
O(n^2)
那么对于一大段程序呢
int n = 10;//执行一次
for(int i = 0;i<n;i++)//执行n*n次
{
for(int j = 0;j<n;j++)
{
printf("hello");
}
}
for(int i = 0;i<n;i++)//执行n^2/2+n/2次
{
for(int j = i;j<n;j++)
{
printf(" world");
}
}
最后f(n) = 3/2*n^2+2/3*n+1
而O(n) = n^2
所以,对于大段程序来说,只要找到次幂最大的那段程序即可。找for嵌套或者是while类循环
顺序执行的代码只会影响常数项,可以忽略。
只需挑循环中的一个基本操作分析它的执行次数与n的关系即可
如果有多层嵌套循环,只需关注最深层循环循环了几次
2.8常见的时间复杂度
2.9最坏情况和平均情况
我们查找一个有n个随机数字数组中的某个数字,最好的情况是第一个数字就是,那么算法的时间复杂度为0(1),但也有可能这个数字就在最后一个位置上待着,那么算法的时间复杂度就是o(n),这是最坏的一种情况了。
最坏情况运行时间是一种保证,那就是运行时间将不会再坏了。在应用中,这是一种最重要的需求,通常,除非特别指定,我们提到的运行时间都是最坏情况的运行时间。
而平均运行时间也就是从概率的角度看,这个数字在每一个位置的可能性是相同的,所以平均的查找时间为n/2次后发现这个目标元素。
2.10算法空间复杂度
算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作: S(n)=O(f(n)),其中,n为问题的规模,f([n)为语句关于n所占存储空间的函数。
程序所消耗的内存空间的多少。
在实际过程中,完全可以用空间来换取时间。
判断输入的年份是否为闰年
if(year%4==0)
{
printf("是闰年");
}
else
{
printf("不是");
}
int arr[2050];
arr[2000] = 1;
arr[2001] = 0;
arr[2002] = 1;
arr[2003] = 0;
arr[2004] = 1;
if(arr[year])
{
printf("是闰年");
}
else
{
printf("不是");
}
2.11总结与结语
- **算法的定义:**算法是解决特定问题求解步骤的描述,在计算机中为指令的有限序列,并且每条指令表示一个或多个操作。
- **算法的特性:**有穷性、确定性、可行性、输入、输出。
- **算法的设计的要求:**正确性、可读性、健壮性、高效率和低存储量需求。算法特性与算法设计容易混,需要对比记忆。
- **算法的度量方法:**事后统计方法(不科学、不准确)、事前分析估算方法。
大O法:
**1.用常数1取代运行时间中的所有加法常数。**O(3) = O(1)
**2.在修改后的运行次数函数中,只保留最高阶项。**O(4n+8) = O(n)
3.如果最高阶项存在且不是1,则去除与这个项相乘的常数。得到的结果就是大О阶。 O(3n^2+2) = O(n^2)
现在由于硬件的高速发展,CPU、GPU的高速发展(cache),可能越来越不重视算法的优劣,不注重算法的效率,这是很不对的。算法的提高无论是对于实际的企业(无人驾驶)或者是应用等等都有大帮助,一个O(n)就可以实现的算法你非要写一个O(n^2)出来,速度会非常的慢。