第一章:算法入门
1.1 接触算法
算法难以进行精准的定义,因为宽泛的来说,所有的编程代码都可以称为算法,因为它们都有循环、逻辑判断、输入输出、需要计算内存消耗等。从程序员的角度上来看,算法可以用以下公式表示:
数据 + 代码 → 解决具体问题=算法
例如:编写九九乘法表,这个挺简单的,直接附上代码:
for i in range(1, 10):
for j in range(1, i+1):
print('%d*%d=%d' % (j, i, i*j), end=' ')
print('')
这里为了打印的比较好看,使用了两个print。
下面介绍算法的质量:
一个算法质量的好坏主要有四个因素决定:
- 数据:好的数据,可以保证算法得到最佳的输出结果,坏的数据则会使算法产生很大的误差。
- 算法时间效率:解决同一问题可以用不同的算法,不同的算法会产生不同的时间效率问题。
- 算法空间效率:所有的算法都在内存中运行,占用尽量少的内存空间,实现算法的快速计算是算法必须要解决的问题。
- 硬件性能:比如有的算法需要在专门的芯片或开发板上运行,再比如深度学习往往需要借助GPU运行。
1.2 算法的时间
算法计算时间效率是所有coder必须考虑的问题,因此,在算法编程时,必须反复优化设计思路,降低算法时间。
1.2.1 比较累加器算法
例如:要计算1到10000的自然数累加和。
思路分析:这里很明显的一个思路就是直接用for循环进行累加,因为反正是计算机计算,大家也不会觉得累,但用for循环肯定会耗费大量时间。那么另一个思路就是高中时候学过的数列累加公式:
n
(
n
+
1
)
2
\frac{n\left( n+1 \right)}{2}
2n(n+1)
代码展示:
首先是for循环累加代码:
import time
def add_n(n):
result_n = 0
for i in range(1, n+1):
result_n += i
return result_n
if __name__ == '__main__':
start = time.time()
result = add_n(10000)
print('运行时间:', (time.time() - start))
print('计算结果为:', result)
运行时间和结果为:
接下来用数列累加求和公式,看看时间消耗量为多少
import time
def add_n(n):
result_n = (n * (n+1)) / 2
return result_n
if __name__ == '__main__':
start = time.time()
result = add_n(10000)
print('运行时间:', (time.time() - start))
print('计算结果为:', result)
运行结果为:
可以看出,第二种方法时间消耗几乎为0.
1.2.2 算法时间点评
一个算法花费多少时间,可以让计算机进行测试,但有时我们只是需要比较几种算法之间的耗时关系,不需要得出确切的时间,这种情况下可以通过分析算法中语句执行次数进行判断。例如刚刚的算法,for循环累加计算了10000次,而数列公式法只计算了一次,显然后者时间更短。
在计算机中,用大写字母O表示算法复杂度。只执行一次的表示O(1),叫做常数阶;执行n次的表示O(n),叫做线性阶。其他表示方法还有O(logn),叫做对数阶;O(n^2)叫做平方阶;O(n!)叫做阶乘阶等。
O
(
1
)
<
O
(
log
n
)
<
O
(
n
)
<
O
(
n
2
)
<
O
(
n
3
)
<
O
(
n
!
)
O\left( 1 \right) <O\left( \log n \right) <O\left( n \right) <O\left( n^2 \right) <O\left( n^3 \right) <O\left( n! \right)
O(1)<O(logn)<O(n)<O(n2)<O(n3)<O(n!)
如图是算法时间复杂度:
1.3 算法的储存空间
代码必须在内存里面运行。当算法涉及大量的数据计算时,必须考虑算法对内存的使用要求,避免内存不足的问题发生。
1.3.1 算法空间的评估
此处使用抽纸牌算法的对比来说明优化算法对储存空间的使用要求。
例:要求一次抽取5张牌,并记录牌的内容。
Poker=['黑A','黑2','黑3','黑4','黑5','黑6','黑7','黑8','黑9','黑10','黑J','黑Q','黑K',
'红A','红2','红3','红4','红5','红6','红7','红8','红9','红10','红J','红Q','红K',
'梅A','梅2','梅3','梅4','梅5','梅6','梅7','梅8','梅9','梅10','梅J','梅Q','梅K',
'方A','方2','方3','方4','方5','方6','方7','方8','方9','方10','方J','方Q','方K',
'小王','大王']
print('一副牌有%d张'%(len(Poker)))
import random
Show=random.sample(Poker,5) #随机抽取5张,展现
print(Show)
此代码会将列表里面所有牌的名称事先写入内存,然后再随机抽5张。
接下来看高效空间利用方法:
思路:这里默认规则为黑A到黑K顺序编号为1到13,红A到红K编号为14到26,梅A到梅K编号为27到39,方A到方K编号为40到52,小王编号53,大王编号54
shape=['J','Q','K','小王','大王']
Show1=[]
Show2=[]
def getCards(name,r,Show2,shape=None): #从同一花色13张牌里取一张牌
if r==1 :
Show2.append(name+'A')
elif r<=10:
Show2.append(name+str(r))
else:
Show2.append(name+shape[r-11]) #考虑J、Q、K
for x in range(5): #随机抽取5个值
y=random.randint(1,55) #在[1,54]范围里随机抽取一个整数
Show1.append(y)
for r in Show1: #确定5张牌
if r<=13: #取黑桃里的13张牌
getCards('黑',r,Show2,shape)
elif r>13 and r<=26: #取红桃里的13张牌
getCards('红',r-13,Show2,shape)
elif r>27 and r<=39: #取梅花里的13张牌
getCards('梅',r-26,Show2,shape)
elif r>40 and r<=52: #取方块里的13张牌
getCards('方',r-39,Show2,shape)
else:
Show2.append(shape[r%50]) #取小王或大王
print(Show2) #取出的5张牌
高效空间内存法只占用了五个元素的空间,而第一种方法占用了54个元素的空间。(高效空间内存法除了在shape上会产生五个空间,在show1,show2和其他变量上都会产生空间要求,只不过上述对象储存空间总要求比第一种方法所需要的更少,所以在储存空间使用上更占优势)
1.3.2 空间换时间
当具体应用计算时间要求远远高于储存空间要求时,空间换时间也是解决问题的一种思路。
例如:战斗机执行任务,需要较快的计算速度,所以这个时候就应该牺牲空间换取运行时间。
同理,上述第一种方法的空间占用虽高,但时间复杂度只有O(1),而第二种方法的时间复杂度至少为O(15)。所以,当时间要求远远高于存储空间要求时,应选择第一种方法。
1.4 算法实践基础
解决算法问题需要经历四步:确定需求、设计算法、算法代码实现、验证算法。
1.确定需求:
确定需求就是明确确定需求内容的规则要求,即读懂题意。
例如上述求自然数的阶乘的问题,就需要知道什么叫自然数、什么叫阶乘或阶乘的规则、什么叫输入输出规则
2.设计算法
对于简单的算法问题,可以直接别写代码。但对于复杂的算法问题,应该做好算法设计这一步。这里介绍以下人工算法思路图:
人工算法思路图就是通过数据和整体实现的逻辑思路展现,限定算法的实现框架,为算法代码实现提供设计思路,并提供人工计算结果(或代表性部分计算结果)。以求阶乘的问题为例,如下图即为人工算法思路图具体体现:
例如:开始求10的阶乘,依据上述思路编写代码:
def get_factorial(n):
if n == 0:
return 1
else:
m = 1
for i in range(1, n+1):
m *= i
return m
print(get_factorial(10))
求得10的阶乘结果为3628800
4.验证算法
由于步骤2中的图已经计算好了结果,所以算是进行了算法验证。当算法比较复杂时,该图还具有代码调试校对的作用,用来检查代码步骤与设计思路是否一致。
1.5 算法实现技巧
- 算法偏爱循环。绝大多数算法都会涉及循环(递归)操作,当看到迭代、反复、递归、回溯、累加、累乘等字眼,能快速地想到要用循环语句解决问题。
- 人工数据代入测试法。在纸上,用手算或者数据代入可以加深理解,明白算法设计思路。
- 前后对照法。对于需求不清晰的问题,可以对照实现代码和输出过节进行对照。
- 输出中间值法。即学会用debug或者print语句进行算法理解。
- 嵌套或组合法。一些复杂的问题需要考虑算法嵌套或者组合不同的算法解决问题。此时可采用逐个击破的方法,即先解决局部问题,再组合解决整体问题。