Lecture 09
二分法
实际上是重复了 Lecture 8 的内容。
Lisp语言存储列表是使用了box pointer diagram,使用的是链表,每一个盒子有两个指针,一个指向下一个位置,一个指向值对象。这种存储链表的方式如果要找到第i个列表元素的话,需要按顺序一个一个的来,即线性查找时间。
Python和Fortran语言改进了这一问题,采用了另一种方式来存储列表。使用一整块来存储数据,每一块中每一个格子都有一个指针,指针指向值对象。
这两种的区别在于Python的方法省去了指向下一个指针,直接分配一整块内存给链表。这两种方法各有优劣,都是对访问时间和空间权衡之后得出的决策。
二分法隐藏的通用思想
- Pick the mid point
- Check to see if this is answer
- If not, reduce to small problem repeat
我们应该在search之前sort吗?
搜索无序表时,复杂度为n。
搜索有序表时,复杂度最快为nlogn。
然而,如果我们要搜索
搜索无序表时,复杂度为
搜索有序表时,复杂度为
代码
#冒泡排序
def bubbleSort1(L):
for j in range(len(L)):
for i in range(len(L)-1):
if L[i] > L[i+1]:
temp = L[i]
L[i] = L[i+1]
L[i+1] = temp
print L
print '*******'
def bubbleSort(L):
swapped = True
while swapped:
swapped = False
for i in range(len(L)-1):
if L[i]>L[i+1]:
temp = L[i]
L[i] = L[i+1]
L[i+1] = temp
swapped = True
print L
print '********'
#选择排序
def selectionSort(L):
for i in range(len(L)):
minIndex = i
minVal = L[i]
for j in range(i,len(L)):
if L[j] < minVal:
minIndex = j
minVal = L[j]
L[minIndex] = L[i]
L[i] = minVal
print L
Lecture 10
分治法
分治算法概要步骤
- 将问题分割为同类型的子问题
- 单独解决这些子问题
- 联合这些解决办法
归并排序
归并排序是分治法的一个重要例子
- 将列表分割成一半
- 继续分割直到每个列表都有一个元素
- 归并这些子列表
def merge(left, right):
result = []
i,j = 0,0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i = i+1
else:
result.append(right[j])
j = j + 1
while i < len(left):
result.append(left[i])
i = i + 1
while j < len(right):
result.append(right[j])
j = j+1
return result
def mergeSort(L):
if len(L)<2:
return L[:]
else:
middle = len(L)/2
left = mergeSort(L[:middle])
right = mergeSort(L[middle:])
together = merge(left,right)
print 'merged',together
return together
哈希
- 哈希算法的意思是给列表中的每个元素一个对应的索引值。
- 给时间以空间
要创建一个完全平均的哈希算法是困难的(完全平均的意思是列表中每个元素被检索到的时间都是一样的)
代码示例
#这是一个整数对应整数索引的哈希算法,哈希表为small-large
def create(smallest,largest):
intSet = []
for i in range(smallest,largest+1):
intSet.append(None)
return intSet
def insert(intSet,e):
intSet[e] = 1
def member(intSet,e):
return intSet[e] == 1
#这是一个字母对应整数的哈希算法,利用计算机内部的ascii创建
def hashChar(e):
return ord(e)
def cSetCreate():
cSet = []
for i in range(0,255):
cSet.append(None)
return cSet
def cSetInsert(cSet,e):
cSet[hashChar(e)] = 1
def cSetMember(cSet,e):
return cSet[hashChar(e)] == 1
Exception异常
这是python另一个语法机制。
可以分为
- Unhandled exception
- Handled exception
代码示例
#try...except被叫做tri-accept block
def readFloat(requestMsg,errMsg):
'''test exception'''
while True:
val = raw_input(requestMsg)
try:
val = float(val)
return val
except:
print errMsg
区分异常(Exception)和断言(Assert)
我的理解是:
断言是为了防止用户输出一些不符要求的值而强行检查退出;
异常则是主要防止程序真正运行时一些乱七八糟的错误。
Lecture 11
这节课几乎是纯理论的东西,讲述的是测试和调试。
- 验证(Validation):是隐藏问题和增加自信心的过程,是测试(testing)和寻因(reasoning)的集合。
- 调试:定位并解决程序失败的问题。
防卫性程式设计
Testing
测试:主要是考察输入/输出的表现
- 主要分为单元测试和集成测试
测试集的选取要合理,要大到可以给予我们信心,也要小到时间合理。
bug
关于bug有两条西式的谬论
- bugs crawl in programs
bugs breed
John认为有两条非常好的调试工具,一个是print语句,另一个是代码阅读。
关于调试,还有一个重要思想:系统化的思想。
如何系统化?
- 研究程序文本
- 研究如何挽回
- 寻找生成此项错误的最简输入
- 二分法查找可能出错的地方
代码示例:
def silly():
res=[]
done=False
while not done:
elem = raw_input('Enter element, return when done')
if elem == '':
done = True
else:
res.append(elem)
#二分法第一步:选择中间部分输出查看,发现前面没问题
#print 'res ',res
#定位问题,‘=’将前后引用指向了统一个对象
tmp = res
#如下代码进行解决
#tmp = res[:]
tmp.reverse()
print 'res ',res,' tmp ',tmp
#第二步,在后面的中间进行print,发现问题
#查到bug所在,bug的原因是tmp和res指向同一个对象
isPal = (res == tmp)
if isPal:
print 'is palindrome'
else:
print 'not palindrome'
Lecture 12
关于调试
编程者经常犯的小错误有:
- 自变量的顺序错误
- 拼写错误
- 初始化错误
- Object vs Values
- 假名错误
副作用
我们需要建立一个自己的犯错模型。
John对于编程者的建议有:
- 记录下你所有的曾经的尝试,不要重复犯错
- 重新考虑代码思路
- 调试代码而非注释
- 寻求他人帮助
- 休息一会再来看
- 慢一点,欲速不达
- 控制代码的行数
保存旧的版本
优化问题
简而言之就是两个部分:
一个需要求极值的函数
- 函数上的约束
经典的优化问题有:最短路径问题,旅行商问题,序列对齐问题等。接下来要介绍的是背包问题。
连续背包问题
假设一个小偷,他只有一个8磅的背包,有三种货物,4磅的金砂,3磅的银砂和10磅的葡萄干。如果这个小偷想拿到最有价值的分配,他该怎么做?
显而易见的做法是:
先装4磅金砂,再装3磅银砂,最后装1磅的葡萄干。
为什么说这个问题是连续背包呢?因为金砂还有银砂可以看做是无限小的。
这个问题的数学描述是
其中
这个问题使用贪婪算法就可以解决。
但是,
局部最优并不能总是保证全局最优。
不连续背包问题
0/1 Knapsack Problem
不连续背包问题中物品的重量是不能再细分的。
同样是一个小偷,他有8磅容量的背包,房间内物品有
- 表,重0.5磅,价值A位,两块
- 收音机,重2磅,价值D位,两块
- Tiffany花瓶,重5磅,价值B位,两个
- Velvet Evis,重6磅,价值C位,两块
现在有三个小偷
- 一个是贪婪的贼,使用贪婪算法(greedy algorithm),他的选择是先拿最贵的,也就是
表∗2+花瓶∗1 - 一个精心思考的慢性子的贼,他会使用蛮力法(Bruce algorithm),他的选择时把每种选择都试一遍,他最后还没试出最优解就被抓到警察局了。
- 还有最后一种使我们这样聪明的贼。
现在用数学公式描述一下上述问题
其中,
动态编程
Dynamin Programming, John说这个名字没有任何意义。
它的重点在于
- overlapping subproblems(重叠子问题)
- optimal substructrure(优化子结构)