1.phython算法基础
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')
- 类比生活中的一些事情,估计时间:
- 眨一下眼 一瞬间/几毫秒
- 口算"26+67" 几秒
- 烧一壶水 几分钟
- 睡一觉 几小时
- 完成一个项目 几天/几星期/几个月
- 飞船从地球飞往月球 几年
几代表大约,分钟代表时间单位
时间复杂度:用来评估算法运行效率的一个东西
o(1) :时间复杂度的基本单位,也是最小单位,o代表大约,1代表时间复杂度单位
比如加减乘除,打印时间复杂度就是o(1)
𝑛^2
找基础语句
再看下面的场景:
print('hello')
print('world')
print('hello world')
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(3)
第二个为:最外层的大循环,循环一次,里层就是1+n,加上最外层的循环n次,最终为n(n+1)
换算为o(n^2+n)
第三个为:当i取0时,里层循环不执行;当i取1时,里层循环执行1次;当i取2时,里层循环执行2次;当i取3时,里层循环执3次;当i取n-1时,里层循环执行n-1次;
0+1+2+3+……+(n-1)=(n-1-0)n/2=(n^2/2-n/2)
那上面就是我们计算出的时间复杂度了吗?
再拿生活的举例
我们会说几分钟,不会说一个3分钟,几个三分钟,3个一分钟,说三分钟又是一个确数了,单位前面不用再乘以一个数
所以第一个时间复杂度最终是o(1)
我们不会说几分钟几秒的,可以说5分钟01秒,但不会说几分钟0几秒,0几秒没有意义
所以大单位后面就不用再挂一个小单位
所以第二个最终的时间复杂度是o(n^2)
同理,第二个,小单位没有必要,n/2就没有了,剩下n2/2=1/2*n2,1/2系数也不要了,所以第三个最终的时间复杂度也是o(n^2)
如果两个代码的时间复杂度是一样的,从我们的推导来看,两个代码的运行时间有可能不一样,因为前面有一个常数系数,后面还有一个小单位
Cn^2+m
随着n的增大,时间复杂度小的会增长的越慢,时间复杂度大的会增长的越快,差异就越来越明显
继续看以下的场景
while n>1:
print (n)
n = n //2
n=64输出: 输出6次
64
32
16
8
4
2
n=128输出: 输出7次
128
64
32
16
8
4
2
2^6=64
log_2^64=6
对数,2的几次方等于64呢
时间复杂度是o(log_2^n)或o(logn)--计算机是以二进制来计算的,可以直接简写成logn
logn就是为了高效解决问题的,出现就是保证高效的
o(1) <o(logn) <o(n)
2**64
18446744073709551616
这么大的数,循环减半,64次就处理完了
时间复杂度是用来估算算法运行时间的一个式子(单位)
一般来说,时间复杂度高的算法比复杂度低的算法慢
常见的时间复杂度(按效率排序)
o(1) < o(logn) < o(n) < o(nlogn) < o(n^2) < o(n^2logn) < o(n^3)
o(n) < o(nlogn) < o(n^2) 除以n
o(n^2) < o(n^2logn) < o(n^3) 除以n
如何一眼判断复杂度?没有递归
- 循环减半的过程-->o(logn)
- 几层循环就是n的几次方的复杂度
空间复杂度
空间复杂度;用来评估算法内存占用大小的一个式子
S(n)=o(n)--space
T(n)=o(n^2)--time
一个变量是o(1),5个变量是o(5),也是o(1)
如果开辟一个长度为n的列表,那就是o(n)
如果开辟一个长度为n的二维列表,那就是o(n^2)
当然空间复杂度讨论不是很多,空间可以买,时间不可以买
一般来说,空间复杂度大,时间复杂度小,反之也成立
空间换时间
递归的两个特点
- 调用自身
- 结束条件
程序要进入还要出去
看下面几个函数,哪个是合法的递归?
x传入一个正数
def func1(x):
print (x)
func1(x-1)
func1没有结束条件
def func2(x):
if x>0:
print (x)
func2(x+1)
fun2结束条件不符合要求
def func3(x):
if x>0:
print (x)
func3(x-1)
进来的时候打印
fun3符合递归 打印1,2,3,...,n
def func4(x):
if x>0:
func4(x-1)
print (x)
出去的时候打印
fun4符合递归 打印n,n-1,n-2,2,1
f(0)要回到f(1)调用它的地方,f(1)要回到f(2)调用它的地方...
斐波那契数列
#斐波那契数列
#1 1 2 3 5 8 ...
#F(n)=F(n-1) + F(n-2) F(0)=1 F(1)=1 求第n项
def fibnacci(n): #o(2^n)
if n==0 or n==1:
return 1
else:
return fibnacci(n-1)+fibnacci(n-2)
print(fibnacci(3))
来统计下时间
编写一个统计时间的装饰器
import time
def call_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
使用装饰器统计运行时间
from timewrap import *
#斐波那契数列
#1 1 2 3 5 8 ...
#F(n)=F(n-1) + F(n-2) F(0)=1 F(1)=1 求第n项
@call_time
def fibnacci(n): #o(2^n)
if n==0 or n==1:
return 1
else:
return fibnacci(n-1)+fibnacci(n-2)
print(fibnacci(3))
执行发现没有统计到时间
原因:给一个递归函数加装饰器会出问题
装饰器统计的是递归函数,装饰器调用的还是递归函数,那怎么办呢?
装饰器的原理是把里面的函数替换成统计的函数,原来的函数递归,替换之后还是递归
解决:
给它套一个马甲
#斐波那契数列
#1 1 2 3 5 8 ...
#F(n)=F(n-1) + F(n-2) F(0)=1 F(1)=1 求第n项
def fibnacci(n): #o(2^n)
if n==0 or n==1:
return 1
else:
return fibnacci(n-1)+fibnacci(n-2)
@call_time
def fib(n):
return fibnacci(n)
print(fib(20))
输出:
D:\soft\python3.8\python.exe "D:/py project/DevTools/算法/python_code_basic.py"
fib running time: 0.003919839859008789 secs:
10946
Process finished with exit code 0
n取30时间
fib running time: 0.3150191307067871 secs:
n取31时间
fib running time: 0.7027747631072998 secs:
2178309
n取32时间
fib running time: 1.1029481887817383 secs:
3524578
n取33时间
fib running time: 1.726712703704834 secs:
5702887
n取34时间
fib running time: 2.6420905590057373 secs:
9227465
当n=4 2重复计算
当n=5 3重复计算
差不多多一倍,重复计算
n=6,f(6)=f(5)+f(4),需要在n=5的基础上,再拷贝一份4,4重复计算
优化--列表追加方式
@call_time
def fib2(n):#o(n)
li=[1,1]
for i in range(2,n+1):
li.append(li[-1]+li[-2])
return li[n]
print(fib2(34))
n取34时间
fib2 running time: 0.0 secs:
9227465
n取1000时间,结果很大,耗时很小
fib2 running time: 4.553794860839844e-05 secs:
70330367711422815821835254877183549770181269836358732742604905087154537118196933579742249494562611733487750449241765991088186363265450223647106012053374121273867339111198139373125598767690091902245245323403501
时间复杂度为o(n)
空间复杂度为o(n)--列表的长度
空间复杂度有点浪费,占用了前面的n-1位
优化--移位方式
@call_time
def fib3(n):
a=1
b=1
c=0
if n==0 or n==1:
return 1
for i in range(2,n+1):
c=a+b
a=b
b=c
return c
n取1000时间,结果很大,耗时很小
fib3 running time: 0.0005872249603271484 secs:
70330367711422815821835254877183549770181269836358732742604905087154537118196933579742249494562611733487750449241765991088186363265450223647106012053374121273867339111198139373125598767690091902245245323403501
时间复杂度为o(n)
空间复杂度为o(1)--空间复杂度大大减少
最终return结果是c,当n0 or n1,return1,也就是return c为1
继续优化代码,使代码变得简洁
@call_time
def fib4(n):
a=1
b=1
c=1
for i in range(2,n+1):
c=a+b
a=b
b=c
return c
n取1000时间,结果很大,耗时很小
fib4 running time: 0.0 secs:
70330367711422815821835254877183549770181269836358732742604905087154537118196933579742249494562611733487750449241765991088186363265450223647106012053374121273867339111198139373125598767690091902245245323403501
走台阶面试题
N个台阶,一次可以走一步或者两步,求走这n个台阶有多少种方法