算法是解决特定问题求解步骤的描述。
从1加到100的结果
# include <stdio.h>
int main(){
int i, sum = 0, n = 100; // 执行1次
for(i = 1; i <= n; i++){ // 执行n + 1次
sum = sum + i; // 执行n次
}
printf("%d", sum); // 执行1次
return 0;
}
高斯求和
# include <stdio.h>
int main(){
int sum = 0, n = 100; // 执行1次
sum = (1 + n) * n / 2; // 执行1次
printf("%d\n", sum); // 执行1次
return 0;
}
概念
算法的定义
解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。
算法的特性
- 输入输出
- 具有零个或多个输入
- 至少有一个或多个输出
- 有穷性
- 执行有限步骤后会自动结束
- 每个步骤在可接受的时间内完成
- 确定性
- 每一步骤具有确定的含义
- 无二义性
- 可行性
- 每一步都必须是可行的,能在有限步内完成
算法的设计要求
- 正确性
- 算法程序没有语法错误
- 算法程序对于合法的输入数据能够产生满足要求的输出结果
- 算法程序对于非法的输入数据能够得出满足规格说明的结果
- 算法程序对于精心选择的,甚至刁难的测试数据都有满足要求的输出结果
- 可读性
- 健壮性
- 时间效率高,存储量低
算法的度量方法
事后统计法
通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时间进行比较,从而确定算法效率的高低。
缺点:
- 必须依据算法事先编制好程序,需要花费大量时间和精力,若编制出发现是个很糟糕的算法,竹篮打水一场空
- 时间的比较以来计算机硬件和软件等环境因素,有时会掩盖算法本身的优劣
- 算法的测试数据设计困难,程序运行时间往往与数据的规模有很大关系,效率高的算法在小的测试数据面前得不到体现
事前分析估算法
在计算机程序编制前,依据统计方法对算法进行估算。
一个用高级程序语言编写的程序在计算机上运行时所消耗的时间取决于:
- 算法采用的策略和方法 —> 算法好坏的根本
- 编译产生的代码质量 —> 由软件支持
- 问题的输入规模
- 机器执行指令的速度 —> 硬件性能
一个程序的运行时间,依赖于算法的好坏和问题的输入规模(输入量)。
两种求和算法
# include <stdio.h>
int main(){
int i, sum = 0, n = 100; // 执行1次
for(i = 1; i <= n; i++){ // 执行n + 1次
sum = sum + i; // 执行n次
}
printf("%d", sum); // 执行1次
return 0;
}
2n+3次
# include <stdio.h>
int main(){
int sum = 0, n = 100; // 执行1次
sum = (1 + n) * n / 2; // 执行1次
printf("%d\n", sum); // 执行1次
return 0;
}
3次
延展
# include <stdio.h>
int main() {
int i, j, x = 0, sum = 0, n = 100; // 执行一次
for(i = 1; i <= n; i++){
for (j = 1; j <= n; j++){
x++; // 执行n×n次
sum = sum + x;
}
}
printf("%d", sum); // 执行一次
return 0;
}
测定运行时间最可靠的方法就是计算对运行时间有消耗的的基本操作的执行次数。
-
不计循环索引的递增、循环终止条件、变量声明、打印结果等操作
-
分析程序的运行时间时,最重要的是把程序看成是独立于程序设计语言的算法或一系列步骤
-
分析一个算法的运行时间,重要的是把基本操作的数量和输入规模关联起来,即基本操作的数量必须表示成输入规模的函数
函数的渐近增长
给定两个函数f(n)和g(n),若存在一个整数N,使得对于所有n>N,f(n)总是比g(n)大,那么我们说f(n)的增长渐近快于g(n)
- 可以忽略加减常数
- 与最高次项相乘的常数并不重要
判断一个算法的效率时,函数中的常数和其他次要项常常可忽略,而更应该关注最高阶项的阶数。
某个算法,随着n的增大,它会越来越优于另一算法或越来越差于另一算法。
算法的时间复杂度
进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。
- 算法的时间复杂度,也就是算法的时间度量,记作T(n)=O(f(n)) —> 大O记法
- 它随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称:时间复杂度
- f(n)是问题规模n的某个函数
增长最慢的算法为最优算法。
推导大O阶方法
- 用常数1取代运行时间中的所有加法常数
- 在修改后的运行次数中函数中,只保留最高阶项
- 如果最高阶项存在且系数不是1,则去除与这个项相乘的系数
常数阶 O(1)
顺序结构的时间复杂度。
执行时间恒定的算法:O(1)的时间复杂度 —> 常数阶
不论常数为多少,都记作O(1)!
对于分支结构而言,无论真假,执行的次数都是恒定的,不会随着n的变大而发生变化,单纯的分支结构 -> 复杂度也为O(1)
线性阶 O(n)
线性阶的循环结构。
要确定某个算法的阶数,常需要确定某个特定语句或某个语句集的运行次数。
分析算法的复杂度,关键就是要分析循环结构的运行情况!
int i;
for(i = 0; i < n; i++){
// 时间复杂度为O(1)的程序步骤序列
}
对数阶 O(logn)
int count = 1;
while (count < n){
count = count * 2;
// 时间复杂度为O(1)的程序步骤序列
}
每次count乘2后,离n就近一分, 2 x = n , x = log 2 n 2^x=n,x=\log_{2}n 2x=n,x=log2n
平方阶 O( n 2 n^2 n2)
int i,j;
for(i = 0; i < n; i++){
for(j = 0; j < n; j++){
// 时间复杂度为O(1)的程序步骤序列
}
}
循环的时间复杂度等于循环体的复杂度乘以该循环的次数。
理解大O阶推导不算难,难的是对数列的一些相关运算,更多的是考察数学知识和能力!特别是数列方面的知识和解题能力!
常见时间复杂度表
最坏与平均情况
最坏情况是一种保证,通常我们提到的运行时间都是最坏情况的运行时间。
平均运行时间是所有情况中最有意义的,因为它是期望的运行时间。
算法空间复杂度
计算公式:S(n)=O(f(n))
n -> 问题的规模,f(n)为语句关于n所占存储空间的函数
一般情况,一个程序在机器上执行时,除了需要存储程序本身的指令、常数、变量和输入数据外,还需要存储对数据操作的存储单元。
通常使用“时间复杂度”指运行时间的需求;使用“空间复杂度”指空间需求。
不用限定词地使用“复杂度”时,通常指:时间复杂度