(1)、算法采用的策略、方案;
(2)、编译产生的代码质量(编译器的优劣、编译器采用的解决方案的优劣);
(3)、问题的输入规模 n;
(4)、机器执行指令的速度
故 抛开计算机硬件、软件有关的因素,一个程序运行时间依赖于 算法的好坏 和 问题的输入规模
例如:等差求和
算法(1):
int sum = 0, n = 1000000000; // 1
for (int i = 1; i <= n; i++) // 判断了 n+1 次
{
sum += i; // 执行了 n 次
}
算法(2):
int sum = 0, n = 1000000000; // 1次
sum = (1 + n) * n / 2; // 1次,而不是O(n^2),时间复杂度看的是 执行次数
分析:算法(1)执行了 1+(n+1)+ n = 2n + 2 次,而算法(2)只执行了 2 次,忽略头尾判断,那么这两个算法就是 n 和 1 的差距
一方面,如果较真研究 精确执行的次数 是非常累的;另一方面,研究算法的复杂度,侧重研究算法随着输入规模的增长量的一个抽象,而不是精确定位次数。
因此只需要看抽象出来的次数,从而得出时间复杂度
分析一个算法的运行时间时,重要的是把基本操作的 数量 和 输入模式 关联起来
(1)、函数渐进增长:给定两个函数 f(n)、g(n),如果存在一个正数 N ,使得所有 n>N 使得 f(n) > g(n),那么f(n) 增长渐进快于 g(n)
(2)、例1:算法A要做 2n+3 次(2个不嵌套循环,之后有3次运算),算法B要做 3n+1 次(同上),哪个更快呢?
规模 | 算法A1(2n+3) | 算法A2(2n) | 算法B1(3n+1) | 算法B2(3n) |
---|---|---|---|---|
n=1 | 5 | 2 | 4 | 3 |
n=2 | 7 | 4 | 7 | 6 |
n=3 | 9 | 6 | 10 | 9 |
n=10 | 23 | 20 | 31 | 30 |
n=100 | 203 | 200 | 301 | 300 |
当 n=1 时,A1 不如 B1;
当 n=2 时,二者效率(耗费时间)相等;
当 n>2 时,A1 优于 B1,算法A 逐渐拉大与 B 的差距,总体上 A 优于 B
随着规模增大,算法A1、A2 和 B1、B2基本相互覆盖,可见加一个系数对时间影响不大
(3)、例2:算法C :4n+8,算法D:2n2+1 哪个更快呢?
规模 | 算法C1(4n+8) | 算法C2(n) | 算法D1(2n2+1) | 算法D2(n2) |
---|---|---|---|---|
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 | 20001 | 10000 |
n=1000 | 4008 | 1000 | 2000001 | 1000000 |
观察发现,哪怕去掉与 n 相乘的系数,二者最终结果没有改变:算法C2 次数随着n的增长,还是远小于D2,也就是说, 与最高次项相乘的系数并不重要,可以忽略
(4)、结论:判断算法的效率,应主要考虑主项(最高次项)的阶数,而最高项的系数也不用关注
(1)语句总的执行次数 T(n) 是关于问题规模 n 的函数,进而分析 T(n) 随 n 变化情况并确定 T(n) 的数量级;
算法时间复杂度就是算法的时间量度,记作 T(n) = O( f(n) ) ;
它表示随问题规模 n 的增大,算法执行时间的增长率和 f(n) 的增长率相同,称作算法的渐进时间复杂度,简称为时间复杂度。其中 f(n)是时间规模 n 的某个函数
(2)、用大写的 O() 来体现算法时间复杂度的记法,称为 大O记法
(3)、一般情况下,随着输入规模 n 的增大, T(n) 增长最 慢 的算法为最优算法
(1)、用 常数1 取代所有 相加的常数
(2)、取代后的运行次数,只保留最高阶项
(3)、如果最高阶存在,且不为1,则去除与这个项相乘的系数
(4)、分类:
a、常数阶:
int sum, n; // O(1)
printf("Hello World\n"); // O(1)
printf("Hello World\n");
printf("Hello World\n");
printf("Hello World\n");
printf("Hello World\n");
printf("Hello World\n");
sum += (1 + n) * n / 2; // O(1)
O(8)吗 ?分析一下概念“T(n)是关于问题规模 n 的函数”,再根据推导方法(1),所有 常数和的规模是O(1)
b、线性阶: 随问题规模n的增加,计算次数线性增长
int sum, n;
for (int i = 0; i < n;i++)
{
sum += i;
}
时间复杂度:O(n)
c、m方阶: m 层嵌套
int sum, n;
for (int i = 0; i < n;i++)
{
for (int j = 0; j < n;j++)
{
sum += i;
}
}
时间复杂度:O(nm)
特殊情况:
int sum, n;
for (int i = 0; i < n;i++)
{
for (int j = i; j < n;j++) // j = i,与 i有关,通过执行次数判断
{
sum += i;
}
}
执行次数:n + (n-1) + (n-2) + … + 1 = (n+1)*n/2
复杂度:O(n2)
d、对数阶:
int i=1, x;
while (i < x)
{
i *= 2;
}
循环次数:每次 I * 2后,离 x 更进一步,假设有 n 个2相乘后大于 x,结束循环,即 2^n = x,次数 n = log x
复杂度:O(logn)
1 < log < n < n*log < n2 < n3 < 2n < n! < nn
1、例子:
void function(int n) // O(n^2)
{
int j = 0;
for (j = 0; j < n;j++)
{
printf("%d\n", j);
}
}
int main()
{
int n = 10, i = 0, j = 0;
n++; // O(1)
function(n); //O(n^2)
for (i = 0; i < n;i++) // O(n^2)
{
function(i);
}
for (i = 0; i < n;i++) // O(n^2)
{
for (j = 0; j < n;j++)
{
printf("%d\n", j);
}
}
return 0;
}
注:对于for和function的结合,尽管function本身是O(n2 ) 可能会认为应该是O(n3),但将function带入后,与下一个for嵌套等效
(1)、平均运行时间是期望得到的运行时间
(2)、最坏运行时间是一种保证,在应用中,这是一种最重要的需求,除非特别指定,否则提到的时间都是最坏情况的时间
(1)、算法的空间复杂度通过计算算法所需的存储空间实现
(2)、算法的空间复杂度的计算公式:S(n) = O( f(n) ),n为问题规模,f(n) 为语句关于 n 所占存储空间的函数
(3)、通常,用“时间复杂度”来指运行时间的需求,用“空间复杂度”指空间需求
(4)、当直接要求求算法的“复杂度”时,通常求的是“时间复杂度”
(1)、判断某年是否为闰年:
算法1:想一个算法,通过输入的年份的特征,算出是否为闰年
算法2:设立一个2050个元素的数组,所有年份输入,并用0、1标记是否为闰年
分析:
算法1节省空间,仅需算法计算,但消耗时间
算法2节省时间,仅需引用查找,但消耗空间
结论:可以通过时间换取空间,也可空间换取时间