最近刷完了剑指offer,刷的比较慢,大概刷了有2,3个星期吧。总结了一下,感觉大部分题都是一些套路上演变一下,背下这些套路可以大大提高做题的速度和敏感度。比如,树相关遍历算法,dfs,bfs及图的dfs,bfs,以及在此基础上演变而来的回溯法,分治归并就是一个套路系列,模版代码也有很大的相似度。说实话,问题的种类繁多,我还没有把其中的关系全部整理清除,网络上大量的书籍和教程个人总觉得缺了点什么,好像有种没有把什么东西说清的感觉。
1.树的DFS和BFS
树的dfs有递归和非递归两种,两种都各有很大的用处。这是我认为入门第一个非常重要的两个模版,对后期理解别的模版有极大作用。
1.1 DFS递归
理解这个对理解递归特别有好处。递归可以与动态规划结合,在递归的过程中维护一张递归表,也可以在递归过程中维护一张记忆表,解决回溯问题。
对于递归,最好背下来,然后再寻求理解。递归程序往往很难去改,一些变种也很难常规方法想到。
先构造一个二叉树,方便我们调试:
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = None
root=TreeNode(1)
root.left=TreeNode(2)
root.right=TreeNode(3)
root.left.right=TreeNode(4)
print(dfs(root))
#1.基本递归dfs
def dfs(root):
if not root:
return
print(root.val)
if root.right:
dfs(root.right)
if root.left:
dfs(root.left)
1.2 非递归DFS和BFS的通用写法
非递归的dfs关键在于创建一个stack。在python中,stack和quene都可以用list来表示。
我想谈谈递归的本质问题,现代编程语言的函数存储实现方式都是类似的,在系统内存中选择一块连续的内存作为栈,然后选择一些分散的内存区域作为堆。在栈中存储函数及变量地址,在堆中放静态变量和变量地址对应的数据。所以,所谓调用一个函数,本质上就是把当前函数放入栈不执行,先去执行被调用的那个函数,在出栈执行当前函数的过程。而递归的过程则是将这个操作系统要做的事情由工程师来实现了。所以,我们实现递归,维护一个栈就行了。
#2.非递归dfs
def dfstree(node):
stack=[]
res=[]
stack.append(node)
while stack:
node=stack.pop()
res.append(node.val)
if node.left:
stack.append(node.left)
if node.right:
stack.append(node.right)
return res
对于bfs,只要记住非递归形式就够了。把上面的非递归dfs中的stack改为quene就行。
#3.非递归bfs
def bfstree(node):
quene=[]
res=[]
quene.append(node)
while quene:
node=quene.pop(0)
res.append(node.val)
if node.left:
quene.append(node.left)
if node.right:
quene.append(node.right)
return res
1.3 递归DFS的变种回溯算法
这个回溯算法非常多见,是由DFS变种来的,能否掌握这个可以看作小白和非小白的标志了。
def dfs2(root):
res=[]
def dfs(root,tmp):
if not root.left and not root.right:
res.append(tmp)
return
if root.left:
dfs(root.left,tmp+[root.left.val])
if root.right:
dfs(root.right,tmp+[root.right.val])
dfs(root,[root.val])
return res
看一下输出:
[[1, 2, 4], [1, 3]]
这个算法会把到每一个叶子节点的路径全部记录下来,很多设计选择,决策的题目都采用这种方法解决。
当然,非递归也是可以的,就是有点啰嗦我感觉。
def dfstree(node):
stack=[]
res=[]
stack.append(node)
tmp=[]
while stack:
node=stack.pop()
tmp.append(node.val)
if not node.left and not node.right:
res.append(tmp)
tmp=tmp[:-1]
if node.left:
stack.append(node.left)
if node.right:
stack.append(node.right)
return res
1.4 DFS递归的变种分治归并
把大问题转换为小问题,把小问题解决之后合并解决大问题,与递归的思想分解的思想不谋而合。这个再想想。
2.图的DFS和BFS
这个用非递归的比较好,完全可以由树的改过来,这就是我说的一个系列的意思。区别在与图需要多一个set(),来判断这个节点是不是已经经过了,因为图和树不一样,图是连在一起的,所以也可以说树是一种特殊的图。
和树一样,我们先构造一个图,方便调试:
graph={
'A':['B','C'],
'B':['A','C','D'],
'C':['A','B','D','E'],
'D':['B','C','E','F'],
'E':['C','D'],
'F':['D']
}
iter_bfs(graph,'A')
2.1图的DFS
可以看到,核心都一样,维护一个stack。
def iter_dfs(graph,s):
stack=[]
stack.append(s)
seen=set()
seen.add(s)
while len(stack)>0:
vertex=stack.pop()
nodes=graph[vertex]
for w in nodes:
if w not in seen:
stack.append(w)
seen.add(w)
print(vertex)
2.2图的BFS
也是一样,stack改成queue完事。
def iter_bfs(graph,s):
queue=[]
queue.append(s)
seen=set()
seen.add(s)
while len(queue)>0:
vertex=queue.pop(0)
nodes=graph[vertex]
for w in nodes:
if w not in seen:
queue.append(w)
seen.add(w)
print(vertex)
动态规划
快排及双指针
二分查找
最后一个不易察觉的问题:堆
总结
背熟套路是解题的基础,但不是全部,关键还是多看多做,不能忽略这点。另外,这个是中等题的套路,初级题虽然让人轻视,其实往往并不简单。因为初级题考察的是能否熟记语法,常常让人阴沟翻船。这其实是另外一种看题的思路,包括位运算,字典操作,collection标准库的使用等,都是平时很少注意的点,我会专门再总结。