2.7 算法 --0 算法基础

算法子目录: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是先调用最后回调时输出。
红框是输出,黑框是调用。


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位以上时,数字会出现浮动,会多一位或者少一位。

你可能感兴趣的:(2.7 算法 --0 算法基础)