Content:
不同算法的优劣,通过以下两个维度来衡量:
- 时间复杂度:是指执行当前算法所消耗的时间
- 空间复杂度:是指执行当前算法需要占用多少内存空间
时间复杂度:
定义:在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数。定性描述该算法的运行时间。
通常使用
大O符号
表示法来衡量其时间复杂度,,可看作是算法的渐进时间复杂度。
公式:T(n) = O (f(n))。
T(n)表示随输入规模n的增大,算法执行时间的增长率。其中f(n) 表示每行代码执行次数之和。
以下面例子为例:
for(i=1; i<=n; i++) #1
{
j = i; #n
j++; #n
}
假设每行代码的执行时间都是一样的,上面的代码总共执行了1+2n个时间单位,即T(n) = O (1+2n)。可以看出算法消耗的时间是随着n的变化而变化的,当n足够大时,即循环次数非常多时,常量1和倍数2的作用已经微乎其微,所以该代码实际为T(n) = O (n),即时间复杂度为 O (n)。
判断一个算法的时间复杂度时,假设n无限大,加法常数项和其他次要项(譬如系数,最高次项相乘的常数)常常可以忽略,最高阶项的阶数的影响通常是最大的。
执行时间的定义
最坏情况:结果在最后的时候才出现。
平均情况:期望的运行时间。
通常情况下,以最极限的情况,即最坏情况作为标准。才能有效评估算法的复杂度。
所以,以下的执行时间均是指最坏情况的运行时间。
推导大O阶方法
- 用常数1取代运行时间中的所有加法常数。
- 在修改后的运行次数函数中,只保留最高阶项。
- 如果最高阶项存在且不是1,则去除与这个项相乘的常数。
常见的时间复杂度量级
- 常数阶O(1)
- 对数阶O(logN)
- 线性阶O(n)
- 线性对数阶O(nlogN)
- 平方阶O(n^2^)
- 立方阶O(n^3^)
- K次方阶O(n^k^)
- 指数阶O(2^n^)
- 阶乘O(n!)
从上至下依次的时间复杂度越来越大,执行的效率越来越低。最优算法是指随着输入规模n的增大,T(n)的增长率最慢。
具体的时间复杂度分析
1. 常数阶O(1):
sum = 0
n =1000
print("1")
print("1")
print("1")
print("1")
sum = (1+n)*n/2
主要看与输入规模有关的语句,这里是sum语句。无论n为多少,执行时间不会随着n的增大而增大,永远只是执行一次就完成。其他的赋值和打印可忽略。
2. 线性阶O(n):
for(i=1; i<=n; i++)
{
j = i;
}
在for循环里面,循环的次数取决于n的大小,即时间复杂度为 O (n)。
3. 对数阶O(logN):
i = 1;
while(i<n)
{
i = i * 2;
}
在while循环里面,每次都将i
乘以 2,每乘一次,i
距离 n 就越近。假设循环x次之后,i
大于n
了,该循环就退出。也就是说 2 的 x 次方等于 n, x = log~2~n,即时间复杂度为 O (logn)。
通常在二分法中见到
4. 线性对数阶O(nlogN)
for(m=1; m<n; m++)
{
i = 1;
while(i<n)
{
i = i * 2;
}
}
将时间复杂度为O(logn)的代码循环N遍,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)。
5. 平方阶O(n^2^)、O(m*n)
将时间复杂度为O(n)的代码循环N遍(嵌套循环),那么它的时间复杂度就是 O(n) * O(n),也就是O(n^2^)。循环的时间复杂度等于循环体的复杂度乘以该循环运行次数。
普通例子:
for(x=1; i<=n; x++)
{
for(i=1; i<=n; i++)
{
j = i;
j++;
}
}
如果将时间复杂度为O(n)的代码循环M遍,那么它的时间复杂度就是 O(m) * O(n),也就是O(m*n)。
for(x=1; i<=m; x++)
{
for(i=1; i<=n; i++)
{
j = i;
j++;
}
}
特殊的循环嵌套,内循环体随着外循环的条件递增而减少。
for(i=0; i<n; x++)
{
for(j=i; l<n; i++)
{
...
}
}
#推导过程:
当i=0时,内循环执行了n次,当i=1时,执行了n-1次...当i=n-1时,执行了一次。
n+(n-1)+(n-2)+...+1=n(n+1)/2≈n²
6. 指数阶O(2^n^)
def func(n):
if n <=1:
return 1
else:
return max(func(n-1),func(n-2))
随着n的增加,计算的次数呈指数性增长。在动态规划中很常见,譬如旅行背包问题。
7. 阶乘O(n!)
def fact(n):
if n==1:
return 1
return n * fact(n - 1)
空间复杂度:
是一个算法在运行中临时占用存储空间大小的量度。不是实际计算程序占用的空间。
公式:S(n) = O (f(n))
执行时所需存储空间包括以下两部分:
固定部分:语言本身的数据结构,指令的空间大小。
变化部分:分配变量,递归栈所需空间,存储结果等等。
因为不同语言结构的固定部分必然使用不同的存储空间,抛开计算机系统差异和编程语言的差异,其算法的空间复杂度主要涉及的就是变化部分。
常见的空间复杂度量级
- 常数阶O(1)
- 线性阶O(n)
- 平方阶O(n^2^)
具体的空间复杂度分析
1. 常数阶O(1):
sum = 0
n =1000
sum = (1+n)*n/2
与时间复杂度类同,已创建的变量的存储空间不会随着n的变化而变化,因此它的空间复杂度 S(n) = O(1)。
2. 线性阶O(n):
sum = []
n =1000
for i in range(n):
s.append(i)
与常数阶不同,这里创建了一个数组变量,其内存空间是会随着n的增大而增大,因此它的空间复杂度 S(n) = O(n)。
3. 平方阶O(n^2^):
sum = []
n =1000
for i in range(n):
for j in i:
s.append(j)
由于有内循环,而且该内循环的次数与n相关,所以其数组的内存空间是会随着n的增大而增大,因此它的空间复杂度 S(n) = O(n^2^)。
4. 指数阶O(2^n^):
def dfs(digits, d, l, cur, ans):
if l == len(digits):
if l > 0: ans.append("".join(cur))
return
for c in d[ord(digits[l]) - ord('0')]:
cur[l] = c
dfs(digits, d, l + 1, cur, ans)
d = [" ", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv","wxyz"]
cur = [' ' for _ in range(len(digits))]
ans = []
dfs(digits, d, 0, cur, ans)
return ans
这个例子是手机的按键点击排列组合。按下若干个按键,输出由这些按键所拼接的所有字符串。
对于绝大多数的按键来说,一个按键会出现3个结果。但最后一个按键的结果是4个,所以按照最坏情况来统计,就必须得假设其底数为4。因此,它的空间复杂度 S(n) = O(4^n^)。
There are 0 comments