继续开根号:
>>> def findRoot1(x, power, epsilon):
low = 0
high = x
ans = (high + low) / 2.0
while abs(ans**power - x) > epsilon:
if ans**power < x:
low = ans
else:
high = ans
ans = (high + low) / 2.0
return ans
>>> findRoot1(25.0, 2, 0.001)
4.9999237060546875
>>> findRoot1(27, 3, 0.001)
2.999988555908203
>>> findRoot1(-27.0, 3, 0.001)
KeyboardInterrupt
注意这里开-27的立方根的时候,程序陷入无限循环。这里如果在while下面增加一个print (ans)语句,可以看到程序运行的过程:
>>> findRoot1(-27.0, 3, 0.001)
-13.5
-20.25
-23.625
-25.3125
-26.15625
-26.578125
-26.7890625
-26.89453125
-26.947265625
可以看到,程序用二分法选取中间值猜测后,发现中间值明显小于-27,根据程序,此时,low = ans,但这里的low原本为0,在负数区域中,0并非最低值,反而是最高值。而这里的high = x,反而是最低值。也就是说,本来因为猜测的中间值小于-27,因此通过二分法选择更大的数值,结果却选择了更小的一段数值,在这个区间内不断二分,自然无法找到近似值。因此这个程序只能用于正数的开根号,而不能用于负数。
修复这个bug其实很简单。这个bug的根源在于,我们先入为主的认为
为high值,0为low值。但
可能是负数,因此需要判定哪个是最高值:
>>> def findRoot2(x, power, epsilon):
low = min(0, x)
high = max(0,x)
ans = (high + low) / 2.0
while abs(ans**power - x) > epsilon:
if ans**power < x:
low = ans
else:
high = ans
ans = (high + low) / 2.0
return ans
>>> findRoot2(-27.0, 3, 0.001)
-2.999988555908203
>>> findRoot2(0.25, 2, 0.001)
KeyboardInterrupt
但是这个改良版在求0.25的平方根的时候,又出现了新的问题。
,然而0.5是在这个二分法算法搜寻范围之外,因为程序确定的范围是
。所以,程序陷入无限循环。正确的做法是,应当将搜寻范围确定在
之间。但是且慢,如果是开
的三次方根呢?那就是在
之间了。
二次优化版本其实也很简单且巧妙,只需将边界从0扩展到1或者-1即可:
>>> def findRoot3(x, power, epsilon):
low = min(-1, x)
high = max(1,x)
ans = (high + low) / 2.0
while abs(ans**power - x) > epsilon:
if ans**power < x:
low = ans
else:
high = ans
ans = (high + low) / 2.0
return ans
>>> findRoot3(0.25, 2, 0.001)
0.5
>>> findRoot3(-27, 3, 0.001)
-2.9999847412109375
>>> findRoot3(25, 2, 0.001)
4.9999237060546875
Using functions in modules:Modularity suggests grouping functions together that share common theme
Place i a single .py file
Use import command to access
将如下代码集成在一个名为circle.py文件中:
pi = 3.14159
def area(radius):
return pi * (radius**2)
def circumference(radius):
return 2 * pi * radius
在python中,import这个文件:
>>> import circle
>>> circle.pi
3.14159
>>> circle.area(1)
3.14159
>>> circle.circumference(1)
6.28318
也可以这样进入:
>>> from circle import *
>>> pi
3.14159
>>> area(1)
3.14159
>>> circumference(1)
6.28318
这里需要注意的是:
>>> pi = 0
>>> pi
0
>>> area(1)
3.14159
从“python小结2”的loop开始,就可以有iteration,还可以有recursion。
>>> def iterMul(a,b):
result = 0
while b>0:
result += a
b -= 1
return result
>>> iterMul(3,4)
12
>>> def recurMul(a,b):
if b == 1: # base case
return a
else:
return a + recurMul(a, b-1)
>>> recurMul(3,4)
12
对比这两个代码,就可以知道,为什么iteration用的是while-loop,而recursion用的是for-loop。recursion需要用到base case,因此每次recursion,都需要判定是否属于base case,那么用if...else...就很合适。而iteration并不需要额外的判定,因此用while更简洁明了。
recursion的背后是数学里面的一种证明方法,就是proof by induction。Base case: we can show that recurMul() must return correct answer.
For recursive case, we can assume that recurMul() correctly returns an answer for problems of size smaller than
, then by the addition step, it must also return a correct answer for problems of size
.
对第二点的inductive step需要进一步说明,原文有点拗口。这里的意思是,假设
为:recurMul(a, b-1)得到的答案是正确的。那么需要证明的是,
是正确的。但这里的
其实就是这一步:
return a + recurMul(a, b-1)
在
的基础上,即在假设recurMul(a, b-1)会返回正确值后,显然
是正确的。
经典recursive problems:Factorial (
)
以下是iteration和recursion两个版本的对比:
>>> recurMul(3,4)
12
>>> def facIter(n):
res = 1
while n>1:
res = res*n
n -= 1
return res
>>> facIter(5)
120
>>> def facRecur(n):
if n == 1:
return n
return n*facRecur(n-1)
>>> facRecur(5)
120
说明:对iteration,最好的理解就是牛顿近似法,两者都是在原有的基础上不断逼近目标值,因此,牛顿近似法自然选择iteration的程序设计。而对于牛顿近似法的理解,最为直观的就是看图(见“python小结2”),从
不断逼近
。
对于recursion,深入理解的话就是背后的数学思想,proof by induction。而直观的把握的话,一个是base case,一个是把问题简化一步,然后不断的recurse,直到base case。
汉诺塔
def Towers(size,fromStack,toStack,spareStack):
if size == 1:
print ('Move disk from ', fromStack, 'to ', toStack)
else:
Towers(size-1,fromStack,spareStack,toStack)
Towers(1,fromStack,toStack,spareStack)
Towers(size-1,spareStack,toStack,fromStack)
汉诺塔只能用recursion,无法用iteration。直观的理解这个代码,if语句下的代码,就是base case;而else语句下,就是把问题简化一步,然后不断递归。具体的说,就是先把size-1个盘子移到spareStack上,然后把最下面的大盘子移到toStack,然后再把size-1个盘子移到toStack上。非常的直观。
但按照我第一次的做法,试图去拆开理解程序每一个步骤,那就非常复杂了。原因是,汉诺塔不像阶乘,每次递归都只是返回一个数字。汉诺塔每次递归,返回的是一系列的操作步骤。而且这个操作步骤,随着盘子数量的增加,步骤数量几乎是指数级上升的。这也正是递归思想独特的魅力所在,通过汉诺塔展现的淋漓尽致。
也就是说,对于汉诺塔这个程序的理解和把握,首先要充分理解递归的思想,建立起一种新的直觉式理解,就是我前面说的,把问题缩小一步,之后不断循环。那么为什么这种循环可以实现呢?那就需要通过数学上的proof by induction方法来证明,这样才算搞明白。而试图通过拆分每个程序运行步骤,反而是不理解递归的表现。在汉诺塔代码中,实际上也不可能做到。