一、时间复杂度与空间复杂度的特点
1、时间复杂度
(1)时间复杂度的定义、理解
时间复杂度科普介绍:一套图 搞懂“时间复杂度”
(2)时间复杂度的总结
1、渐进时间复杂度(asymptotic time complexity),简称时间复杂度:若存在函数 f(n),使得当n趋近于无穷大时,
T(n)/ f(n)的极限值为不等于零的常数,则称 f(n)是T(n)的同数量级函数。
记作 T(n)= O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
简而言之,时间复杂度函数是类似高等数学中定义“同价无穷小”的定义函数,符号为 O,他不在考虑有穷值的常数系数
O(1)< O(logn)< O(n)< O(n^2)
2、如何推导出时间复杂度呢?有如下几个原则:
如果运行时间是常数量级,用常数1表示;
只保留时间函数中的最高阶项;
如果最高阶项存在,则省去最高阶项前面的系数。
3、时间复杂度低的程序一定比时间复杂度高的程序实际运行的时间短吗?
答案:不一定
原因:程序的运行的实际时间需要结合时间复杂函数中N具体值得大小,O(f(n))指的是渐进时间复杂函数‘同阶无穷小’,换句话
来讲,只有当N足够大的时候,时间复杂度优秀的程序运行时间必定少于时间复杂度复杂的程序。
二、空间复杂度
1、计算一些例子的时间复杂度
int a = 0, b = 0;
for (i = 0; i < N; i++) {
a = a + rand();
b = b + rand();
}
O(N)
----------------------------------------------------------------------------------------------------
for i in range(N):
a = a + 1
b = b + 1
for (j = 0; j < N/2; j++) {
b = b + rand(); 1
}
O(N)
int a = 0; i,j
for (i = 0; i < N; i++) {
for (j = N; j > i; j--) {
a = a + i + j; 1
}
}
O(N^2)
----------------------------------------------------------------------------------------------------
for i in range(N):
for j in range(N/2):
a = a + 1
O(N^2)
j=1
while j <= N:
a = a + 1
j = j * 2
O(LOG N)
计算: j^2 = N ==> j = log N
----------------------------------------------------------------------------------------------------
for i in range(N):
j=1
while j <= N:
a = a + 1
j = j * 2
O(N LOG N)
计算:外层时间复杂度为 N , 内层时间复杂度为 log N , 总体的时间复杂度为 O(N log N)
----------------------------------------------------------------------------------------------------
int i, j, k = 0;
for (i = n / 2; i <= n; i++) { # O(N)
for (j = 2; j <= n; j = j * 2) { # o(logN)
k = k + n / 2;
}
}
O(N LOG N)
----------------------------------------------------------------------------------------------------
for i in range(N/2):
while j <= N:
a = a + 1
j = j/2
O(N LOG N)
时间复杂度低的程序一定比时间复杂度高的程序实际运行的时间短吗?
答案:不一定
原因:程序的运行的实际时间需要结合时间复杂函数中N具体值得大小,O(f(n))指的是渐进时间复杂函数‘同阶无穷小’,换句话
来讲,只有当N足够大的时候,时间复杂度优秀的程序运行时间必定少于时间复杂度复杂的程序。
三、空间复杂度
(1)空间复杂度的定义、理解
时间复杂度科普介绍: 时间复杂度科普介绍
(2)空间复杂度的总结
空间复杂度:当函数执行的过程中,如果遇到其他函数调用,需要将新函数的内存地址入栈,直到新函数执行并得到返回值,新函数
的内存地址才会出栈。如果新函数内继续调用函数,那么这些层层嵌套的函数的函数地址均会入栈,比按照入栈先后顺序逆向出栈。
四、常见的排序算法及其时间复杂度
1、归并排序
归并排序的基本思想:
归并排序时间复杂度分析: 归并排序时间复杂度分析
# merge sort
def merge(array_l,array_r):
"""
两个有序的数组进行按顺序进行合并
:return:
"""
start,end = 0,0
array = []
while start < len(array_l) and end < len(array_r):
if array_l[start] <= array_r[end]:
array.append(array_l[start])
start += 1
else:
array.append(array_r[end])
end += 1
array += list(array_l[start:])
array += list(array_r[end:])
return array
def merge_sort(array):
if len(array) <= 1:
return array
mid = len(array) // 2
array_l = merge_sort(array[:mid])
array_r = merge_sort(array[mid:])
return merge(array_l,array_r)
if __name__ == '__main__':
arr = [6,3,3,4,9,0,10,-1,-3]
print(merge_sort(arr))
2、递归分治算法的一般结构和时间复杂度的计算
(1)递归分治算法的一般结构 递推公式
(2)主定理的推导
推导后的结果
(3)主定理的使用
主定理的结论
利用主定理计算出常用递归算法的时间复杂度
3、多路递归树执行的先后顺序
(1)以斐波那契数列为例
递推公式
f(n) = f(n-1) + f(n-2)
由 递推公式可以看出,斐波那契递归是双路递归,因此其时间复杂度为 O(2^n),空间复杂度为 O(n)
代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
def fei(n):
if n == 1 or n == 2:
return 1
return fei(n-1) + fei(n-2)
print(fei(5))
递归树分析
由于代码的递推公式为:f(n) = f(n - 1) + f(n + 1),所以递归数从左子树开始,且一直到最底部的位置返回值回溯,然后再
从最低根节点的右子树,如图所以依次返回值进行回溯
(2)以归并排序为例
归并排序思想分析:
代码
def merge(array_l,array_r):
"""
两个有序的数组进行按顺序进行合并【思想:利用两个数组的指针比较大小后不断移动指针的位置进行合并】
:return:
"""
start,end = 0,0
array = []
while start < len(array_l) and end < len(array_r):
if array_l[start] <= array_r[end]:
array.append(array_l[start])
start += 1
else:
array.append(array_r[end])
end += 1
array += list(array_l[start:])
array += list(array_r[end:])
return array
def merge_sort(array):
if len(array) <= 1: # 递归的终止条件是:左右子树长度为 1或者0
return array
mid = len(array) // 2
array_l = merge_sort(array[:mid])
array_r = merge_sort(array[mid:])
return merge(array_l,array_r)
if __name__ == '__main__':
arr = [6,3,3,4,9,0,10,-1,-3]
print(merge_sort(arr))
递归树分析
4、冒泡排序
5、快速排序【划分交换排序】
(1)快速排序的基本思想
快速排序的思想:
1、寻找一个mid点,即 pivot,每一次循环需要将数据以mid为基准,使得左边的数据均小于基准点,优点的数据均大于等于基准点。
方法是:定义 start、end两个指针,比较并相互替换,当start >= end 结束,因此每一层递归树的时间复杂度为O(n),这一点
与归并排序相似
2、递归对基准点左右两个子树进行相同的操作【不包含基准点】,不断循环迭代,最终达到排序的目的
快排时间复杂度分析:
从上面的步骤可以看出,每一层递归树的时间复杂度均为o(n),因此时间复杂度取决于递归树的‘深度’,而不像归并排序严格的
二分,我们不能保证每一个基准点都均匀处于数组的中间,因此快排的递规树的‘深度’是不稳定的,最好的结果是每次基准点都
均匀处于数组的中间位置, 此时递归树的层数最小为log(n)层,相反最差的结果是n层,因此时间复杂度在 O(n * log(n)) ~
O(n * n)之间。
(2)快速排序的代码
def quick_sort(array,start,end):
if start >= end: # 递归的终止条件是:左右子树长度为 1或者0,即所以需要用 >= 号。
return
mid = start # 去第一个位置的索引作为 中间值 pivot 的索引
left = start # 定义当前递归层次下 左边的指针
right = end # 定义当前递归层次下 右边的指针
while left < right:
# 因为选择的是最左边的数值作为 pivot,首先从右边开始循环判断
while left < right and array[right] >= array[mid]:
right -= 1
array[left] = array[right]
# 从左边开始判断
while left < right and array[left] < array[mid]:
left += 1
array[right] = array[left]
# 将中间值付给 left 索引
array[left] = array[mid]
# 递归调用 pivot 左右两个子树【注意:由于此时 pivot已经是当前数组的中间值,所有左右两个子树不应该包含pivot点】
quick_sort(array,start,left-1)
quick_sort(array,left+1,end)
if __name__ == '__main__':
array = [30,49,10,20,50,60,30,70]
end = len(array) - 1
print(array)
quick_sort(array,0,end)
print('快排之后的结果:',array)
(3)快速排序过程解析
a、每一次‘划分交换’的过程
b、递归树的执行过程