算法子目录:https://www.jianshu.com/p/02492be3c5f5
什么是算法
算法就是计算过程,解决问题的方法。
如果我们把C,Java,Python比作一门武艺,那么算法就相当于是内功心法。你可以武艺高超,但是内功心法不如人的话,很难达到最高超的境界。
时间复杂度
什么是时间复杂度
用来评估算法运行效率的一个式子
详解
我们比较一下下面几段代码的运行效率:
print("hello world")
for i in range(n):
print("hello world")
for i in range(n):
for j in range(n):
print("hello world")
for i in range(n):
for j in range(n):
for k in range(n):
print("hello world")
这几组代码,哪个运行最快呢?
我们脑子里第一个想法是运行一下看他们运行的时间,但是这样有一个缺点,运行时间和机器的性能有关系。而且还有一个更致命的问题:问题规模,也就是n的大小。
我们想一下我们在现实中来比较一个东西的快慢,是用时间这个固定的单位来比较的,或者和我们知道用多长时间的事情来比较。
比如:
事件 | 用时 |
---|---|
眨眨眼 | 不到一秒 |
心算一道百以内加减法 | 几秒 |
睡一觉 | 几个小时 |
完一个项目 | 几天/几周/几个月 |
如果我们要烧一壶水,肯用的时间比我们睡一觉短,比我们心算快
如果这样的话,我们就可以定一些有特点的事件为固定的单位,来比较算法的运行效率。
#这样直接输出的式子的时间复杂度叫做O(1)
print("hello world") ==========> O(1)
#这样经过n次循环后输出的式子就是1*n,也就是O(n)
for i in range(n):
print("hello world") ==========> O(n)
#这样就是1*n*n,既n^2
for i in range(n):
for j in range(n):
print("hello world") ==========> O(n^2)
#同理
for i in range(n):
for j in range(n):
for k in range(n):
print("hello world") ==========> O(n^3)
O(1),O(n),O(n2),O(n3)这些复杂度有特点的式子,我们把它当做计量时间复杂度的单位。
要是这样的话,下面几个式子的复杂度分别是多少呢?
print("hello world")
print("hello python")
print("hello hanxuan")
for i in range(n):
print("hello world")
for j in range(n):
print("hello world")
for i in range(n):
for j in range(i):
print("hello world")
#大家肯定会和上面我们定下来的单位进行比较,
#一个式子是O(1),那么三个式子就是O(3)了
#第二个式子就是O(n+n^2)
#第三个式子就是O((n^2)/2)
不过这种说法是错误的,举个最简单的例子,我们说一秒,但是不会说三个一秒。我们描述事件的时候,肯定会向最确切的固定单位描述,比如:大约三分钟,一个小时左右。
时间复杂度首先会看基础语句,第一段程序是三个基础语句,所以三个一秒的单位还是一秒。第二段程序我们会接近最靠近他的单位,(n+n2)还是约等于(n2),第三段同理。
所以我们还是把这三段程序的时间复杂度说成O(1),O(n2),O(n2)。
现在我们弄清楚了这些时间复杂度的单位,接下来我们再看一个:
while n >1:
print(n)
n = n // 2
#我们假设n=64:
n=64
while n >1:
print(n)
n = n // 2
----------
输出结果:
64
32
16
8
4
2
我们可以看见,他执行了6次。我们推算,当n=128的时候,执行7次;265就执行8次。然后我们再看一组式子:
26=64
log264=6
也即是说他的复杂度满足log2n,既O(log2n)或O(logn)。
这是一种速度很快的式子,如果我们输入18446744073709551616,因为2**64=18446744073709551616,如果使用O(logn),只需要计算64次,这显然比O(n)快的要多得多。
小结
时间复杂度是用来估计算法运行时间的一个式子(单位)
一般来说,时间复杂度高的式子比复杂度低的算法慢。
常见的时间复杂度顺序:
O(1)2) 2logn) 3) 不常见的时间复杂度:
O(n!),O(2n),O(nn)如何一眼断定时间复杂度:
1.循环减半就是O(logn)
2.几层循环都是n就是n的几次方的复杂度。
空间复杂度
什么是空间复杂度
用来评估算法运行占用内存大小的一个式子
空间换时间。(现在内存的大小只要不是太过分,基本都能满足,而时间才是现在关注的地方)
详解
一般使用下面这对式子表示空间复杂度和时间复杂度。
- S(n)=O(1)
- T(n)=O(n)
这样便表示空间复杂度为O(1),时间复杂度为O(n)。
空间复杂度表示你的程序中使用了多少个临时变量,使用普通变量为O(1),一维链式的变量为O(n),二维变量为O(n2)。
a=1 ==========>O(1)
[1,2,3,4] ==========>O(n)
[[1,2,3],[4,5,6],[7,8,9]] ==========>O(n^2)
重新来看递归
递归的两大特点
调用自身
结束条件
详解
下面四个函数,哪一个是合格的递归函数?为什么?
显然func1和func2,一个没有结束条件,一个结束条件无法结束递归,都pass掉。
就剩下func3和func4,他们都能结束自己,但是唯一的区别是先调用自身还是先输出。
def func3(x):
if x>0:
print(x)
func3(x-1)
def func4(x):
if x>0:
func4(x-1)
print(x)
func3(3)
print("----------")
func4(3)
----------
输出结果:
3
2
1
----------
1
2
3
从输出结果能够看出,func3输出递减,func4输出递加。其实我们分析一下就能看出,func3是先输出在调用,func4是先调用最后回调时输出。
红框是输出,黑框是调用。
接着我们来看递归最经典的运算:斐波那契数列。
A.cat_time.py
-------------
import time
def cal_time(func):
def wrapper(*args,**kwargs):
t1 = time.time()
result = func(*args,**kwargs)
t2 = time.time()
print("%s running time:%s secs."%(func.__name__,t2-t1))
return result
return wrapper
A.zero.py
---------
from A.cat_time import cal_time
def fib(n):
if n == 0 or n == 1:
return 1
return fib(n-1)+fib(n-2)
@cal_time
def fib_mj(n):
return fib(n)
print(fib_mj(20))
print(fib_mj(20))
----------
输出结果:
fib_mj running time:0.0019943714141845703 secs.
10946
fib_mj running time:0.3939497470855713 secs.
1346269
我们明明只增加了10的大小,但是时间从0.002暴涨到了0.4,暴涨了近200倍。
让我们来看一下他的执行流程:
我们可以看到红框部分和蓝框部分重复计算了。
那么我们x=5时呢
红框部分和蓝框部分也重复计算了,而且几乎比x=4时多算了一倍。
我们能粗略的看出fib()这个函数的时间复杂度是O(2n),这是一个很高的复杂度。
现在我们来改写函数:
def fib2(n):
li=[1,1]
for i in range(2,n+1):
li.append(li[-1]+li[-2])
return li[n]
@cal_time
def fib_mj(n):
return fib2(n)
fib_mj(30)
fib_mj(10000)
----------
输出结果:
fib_mj running time:0.0 secs.
fib_mj running time:0.006949186325073242 secs.
一万才有0.006s。
这样改写之后,我们的时间复杂度变成了O(n2),但是空间复杂度变成了O(n)。
那么我们有没有办法把他的空间复杂度在降低呢?
def fib3(n):
a = 1
b = 1
c = 1
for i in range(2,n+1):
c = a + b
a = b
b = c
return c
@cal_time
def fib_mj(n):
return fib3(n)
fib_mj(10000)
----------
输出结果:
fib_mj running time:0.002992391586303711 secs.
相较于我们第二种方法,我们把空间复杂度从O(n)变成了O(1),速度提升了近两倍。
接着我们还有一种方法:矩阵快速幂
import numpy as np
def fib4(n):
a = np.array([[1,0],[0,1]])
b = np.array([[1,1],[1,0]])
n -= 1
while(n > 0):
if (n % 2 == 1):
a = np.dot(b, a)
b = np.dot(b, b)
n >>= 1
return a[0][0]
@cal_time
def fib_mj(n):
return fib4(n)
fib_mj(10000)
----------
fib_mj running time:0.0009975433349609375 secs.
这是二分法求结果,时间复杂度是O(logn)
接着我们再看一种方式:
这是斐波那契数列的通项公式,如果使用他来计算,时间复杂度就会变成O(1),但是当我们计算到270位以上时,数字会出现浮动,会多一位或者少一位。