python算法归纳

第二章

2.2.2 交通规则

几种常见的渐近运行时间实例

时间复杂度 相关名称 相关示例及说明
O(1) 常数级 哈希表的查询与遍历
O(lgn) 对数级 二分搜索
O(n) 线性级 列表的遍历
O(nlgn) 线性对数级 任意值序列的最优化排序
O() 平方级 n 个对象相互比较
O() 立方级 Floyd-Warshall
O() 多项式级 基于 n 的 k 层嵌套循环
O() 指数级 每 n 项产生一个子集
O(n!) 阶乘级 对 n 个只看进行全排列操作
2.2.4 三种重要情况

这里有一个 else 的例子,用法并不常见,下面的代码中,如果循环,没有被 break 语句提前终止,那么将会执行 else分支!

 
 
  1. def sort_w_check(seq):
  2. n = len(seq)
  3. for i in range(n-1):
  4. if seq[i] > seq[i+1]:
  5. break
  6. else:
  7. return

排序算法的三种情况:

  1. 最好情况
  2. 最坏情况
  3. 平均情况

2.3 图与树的实现

图结构(graph) 算法学中最强大框架之一。在许多情况下,我们都可以把一个问题抽象为一个图,如果能抽象为一个图的话,那么该问题至少已经接近解决方案了。如果问题实例可以用树(tree)诠释的话,那么我们基本上已经拥有了一个真正有效的的解决方案了。 
下面是一些关于图的术语:

  • 图 G = (V,E) 通常由一组节点 V 及节点间的边 E 共同组成。如果这些边有方向,就称其为有向图。
  • 节点之间通过边来实现彼此相连的。而这些边其实就是节点 v 与其邻居之间的关系。
  • G = (V,E] 的子图结构将由 V 和 E 的子集共同组成。在 G 中,每一条路径(path)是一个子图结构,它们本质上都是一些由多个节点串联而成的边线序列。环路(cycle)的定义与路径基本相同,只不过它的最后一条边所连接的末节点同是是它的首节点。
  • 如果我们将图 G 中的边与某种权值联系在一起,G 就成了一种加权图。在加权图中,一条路径或环路的长度等于各边上的权值之和,对非加权图来说,就直接等于该图的边数。
  • 森林 (forest) 可以被认为是一个无环路图,而这样的连通图就是一棵树。换言之,森林就是由一棵或多棵树构成的。
2.3.1 邻接列表及其类似结构

对于图结构的实现来说,邻接列表是最直观的方式之一。 
接下来,我们就要用数据结构来表示图。 
示例图 
图 2-3

清单 2-1 简单明了的邻接集表示法

 
 
  1. a, b, c, d, e, f, g, h = range(8)
  2. N = [
  3. {b, c, d, e, f}, # a 的所有出边,下同
  4. {c, e}, # b
  5. {d}, # c
  6. {e}, # d
  7. {f}, # e
  8. {c, g, h}, # f
  9. {f, h}, # g
  10. {f, g} # h
  11. ]
  12. In [9]: b in N[a] # b 是 a 的邻居
  13. Out[9]: True
  14. In [10]: len(N[f]) # f 有3个邻居
  15. Out[10]: 3

清单 2-2 邻接列表

 
 
  1. a, b, c, d, e, f, g, h = range(8)
  2. N = [
  3. [b, c, d, e, f], # a 的所有出边,下同
  4. [c, e], # b
  5. [d], # c
  6. [e], # d
  7. [f], # e
  8. [c, g, h], # f
  9. [f, h], # g
  10. [f, g] # h
  11. ]

清单 2-3 加权邻接字典

 
 
  1. a, b, c, d, e, f, g, h = range(8)
  2. N = [
  3. {b:2, c:1, d:3, e:9, f:4}, # a 的所有出边及权值(随机值),下同
  4. {c:4, e:3}, # b
  5. {d:8}, # c
  6. {e:7}, # d
  7. {f:5}, # e
  8. {c:2, g:2, h:2}, # f
  9. {f:1, h:6}, # g
  10. {f:9, g:8} # h
  11. ]

以上三种邻接结构的主容器都属于列表类型,都是以节点编号为索引值。dict 更灵活。

2.3.2 邻接矩阵

图的另一种表示方法就是邻接矩阵了。它的不同之处在于,它不再列出每个节点的所有邻居节点,而是将所有可能的邻居位置排一行(也就是一个数组,用于对应图中每一个节点),然后使用某种值(True or False)来表示其是否为当前节点的邻居。与上面一样,依然使用最简单的列表来表示。同是为了让矩阵有较好的可读性,我们将使用1和0来表示真值。且节点的编号从0开始。

清单 2-5

 
 
  1. a, b, c, d, e, f, g, h = range(8)
  2. # a b c d e f g h
  3. N = [
  4. [0, 1, 1, 1, 1, 1, 0, 0], # a
  5. [0, 0, 1, 0, 1, 0, 0, 0], # b
  6. [0, 0, 0, 1, 0, 0, 0, 0], # c
  7. [0, 0, 0, 0, 1, 0, 0, 0], # d
  8. [0, 0, 0, 0, 0, 1, 0, 0], # e
  9. [0, 0, 1, 0, 0, 0, 1, 1], # f
  10. [0, 0, 0, 0, 0, 1, 0, 1], # g
  11. [0, 0, 0, 0, 0, 1, 1, 0] # h
  12. ]

同是,其使用方法也略有不同[1],这里检查的不是 b 是否在 N[a]中,而是检查矩阵单元 N[a][b] 是否为真。同样,要获取相关节点的邻居数,改用 sum 函数即可,因为,所有行的长度是相等的!

 
 
  1. In [6]: N[a][b]
  2. Out[6]: 1
  3. In [7]: sum(N[f])
  4. Out[7]: 3

特点: 
1. 在不允许自循环的状态下,对角线上的值全为假。 
2. 无向图矩阵是一个对称矩阵

加权处理: 
1. 只需在存储真值的地方直接存储相应权值即可。 
2. 出于实践因素考虑,通常把实际不存在的边的权值设为无穷大(float('inf'))

清单 2-6 对不存在的边赋予无限大权值的加权矩阵

 
 
  1. a, b, c, d, e, f, g, h = range(8)
  2. inf = float('inf')
  3. W = [[ 0, 2, 1, 3, 9, 4, inf, inf], # a
  4. [inf, 0, 4, inf, 3, inf, inf, inf], # b
  5. [inf, inf, 0, 8, inf, inf, inf, inf], # c
  6. [inf, inf, inf, 0, 7, inf, inf, inf], # d
  7. [inf, inf, inf, inf, 0, 5, inf, inf], # e
  8. [inf, inf, 2, inf, inf, 0, 2, 2], # f
  9. [inf, inf, inf, inf, inf, 1, 0, 6], # g
  10. [inf, inf, inf, inf, inf, 9, 8, 0]] # h

加权矩阵使得相关加权边的访问变得容易,但需要对相关无穷大值进行检测

 
 
  1. In [23]: W[a][b]
  2. Out[23]: 2
  3. In [24]: W[c][e] < inf
  4. Out[24]: True
  5. In [25]: sum(1 for w in W[a] if w < inf) - 1
  6. Out[25]: 5

这里减去 1 是排除掉对角线的 0

2.3.3 树的实现

一般来说,可以表示成图的方法都能用来表示树,树是图的特殊情况。 
最简单的树: 带根树结构 
树

此树的代码表示:

 
 
  1. In [35]: T = [["a", "b"], ["c"], ["d", ["e", "f"]]]
  2. In [36]: T[0][1]
  3. Out[36]: 'b'
  4. In [37]: T[2][1][0]
  5. Out[37]: 'e'

清单 2-7 二叉树类

 
 
  1. class Tree:
  2. def __init__(self, left, right):
  3. self.left = left
  4. self.right = right
  5. In [39]: t = Tree(Tree('a', 'b'), Tree('c', 'd'))
  6. In [40]: t.right.left
  7. Out[40]: 'c'

上例可描述为下图: 
二叉树类

清单 2-8 多路搜索树 
对于没有 list内置类型的语言,可以采用“先子节点,后兄弟节点的”的表示法!

 
 
  1. class Tree:
  2. def __init__(self, kids, next=None):
  3. self.kids = self.val = kids
  4. self.next = next
  5. 这里的 val 只是为相关的值提供更具描述性的名称,可以自行更换其他你喜欢的。
  6. In [62]: t = Tree(Tree('a', Tree('b', Tree('c', Tree('d')))))
  7. In [64]: t.kids.next.next.val
  8. Out[64]: 'c'
  9. In [77]: t.kids.val
  10. Out[77]: 'a'
  11. In [47]: t.kids.kids
  12. Out[47]: 'a'
  13. In [69]: t.kids.next.val
  14. Out[69]: 'b'
  15. In [72]: t.kids.next.next.next.val
  16. Out[72]: 'd'

Bunch模式:

 
 
  1. class Bunch(dict):
  2. def __int__(self, *args, **kwds):
  3. super(Bunch, self).__init__(*args, **kwds)
  4. self.__dict__ = self
  5. In [56]: t = T(left=T(left='a', right='b'), right=T(left='c'))
  6. In [57]: t
  7. Out[57]: {'left': {'left': 'a', 'right': 'b'}, 'right': {'left': 'c'}}
  8. In [60]: t['left']['left']
  9. Out[60]: 'a'
  10. In [61]: 'left' in t['right']
  11. Out[61]: True
  12. In [62]: 'right' in t['right']
  13. Out[62]: False

用处: 
1. 可以以命令行参数的形式创建相关对象并设置属性。 
2. 继承自 dict 可以自然获得大量相关的内容。

2.3.4 多种表示法

图表示法的草见解: 
1. 图可以由邻接列表和邻接矩阵两种方法表示。 
2. 邻接矩阵速度快,但消耗资源多。二者的选择取决于哪种资源更重要。

2.4 请提防黑盒子

“黑盒子”: 项目中非自己所写的依赖。

提防陷阱的方法:

  • 性能很重要时,要着重于实际分析而不是直觉。
  • 正确性很重要时,使用不同的方法进行多次计算并交由不同的程序员编写
2.4.1 隐形平方级操作
 
 
  1. In [63]: from random import randrange
  2. In [64]: L = [randrange(10000) for i in range(1000)]
  3. In [65]: 42 in L
  4. Out[65]: False
  5. In [66]: S = set(L)
  6. In [67]: 42 in S
  7. Out[67]: False

在 list 上构建 set貌似毫无意义。但如果是下面两中情况就会很适用: 
1. 执行多次查询时,list 是线性级,而 set 是常数级的。 
2. 向其中添加新值时,list 是平方级,而 set 是线性级!

 
 
  1. res = []
  2. for lst in lists:
  3. res.extend(lst)

上式要优于

 
 
  1. >>> lists = [[1, 2], [3, 4], [5, 6]]
  2. >>> sum(lists, [])
2.4.2 浮点数的麻烦

不要对浮点数进行比较

第三章 计数初步

3.4.2

递归调用的一般形式:T(n) = a.T(g(n)) + f(n)。其中

a: 递归调用数量
g(n): 递归调用过程中,所要解决的子问题大小。
f(n): 函数的额外操作

                     一些基本递归式的解决方案及其实例
0 递归式 复杂度 实例
1 T(n)=T(n-1)+1 O(n) 序列化处理问题,如归简操作
2 T(n)=T(n-1)+n O() 握手问题
3 T(n)=2T(n-1)+1 O() 汉诺塔问题
4 T(n)=2T(n-1)+n O()  
5 T(n)=T(n/2)+1 O(lgn) 二分搜索
6 T(n)=T(n/2)+n O(n) 随机问题
7 T(n)=2T(n/2)+1 O(n) 树的遍历
8 T(n)=2T(n/2)+n O(nlgn) 分治法排序

如遇到其他的递归式,可以尝试向如上基本式化简。来求得其复杂度!

3.5

侏儒排序

 
 
  1. def gnome_sort(seq):
  2. if len(seq) <= 1:
  3. return seq
  4. i = 1
  5. while i < len(seq):
  6. if i == 0 or seq[i-1] <= seq[i]:
  7. i += 1
  8. else:
  9. seq[i], seq[i-1] = seq[i-1], seq[i]
  10. i -= 1
  11. return seq
  12. if __name__ == '__main__':
  13. seq = [5, 3, 6, 9, 8, 2]
  14. print(gnome_sort(seq))

应该不用做过多解释!使用Python Tutor可以看到此排序执行步数是74次。反正是没有分治法的归并排序速度快。而且在最坏情况下,其复杂度为O()

 
 
  1. In [7]: def mergesort(seq):
  2. ...: mid = len(seq)//2
  3. ...: lft = seq[:mid]
  4. ...: rgt = seq[mid:]
  5. ...: if len(lft) > 1:
  6. ...: lft = mergesort(lft)
  7. ...: if len(rgt) > 1:
  8. ...: rgt = mergesort(rgt)
  9. ...:
  10. ...: res = []
  11. ...: while lft and rgt:
  12. ...: if lft[-1] >= rgt[-1]:
  13. ...: res.append(lft.pop())
  14. ...: else:
  15. ...: res.append(rgt.pop())
  16. ...:
  17. ...: res.reverse()
  18. ...: return (lft or rgt) + res

第四章 归纳、递归及归简

  • 归简法指的是将某一问题转化成另一个问题。
  • 归纳法则被用于证明某个语句对于某种大型对象类是否成立!
  • 递归法则主要被用与函数自我调用时。

4.1 哦,这其实很简单

归简

假设从一个数字列表中找出两个彼此相近但不相等的数。

 
 
  1. In [80]: from random import randrange
  2. In [123]: seq = [randrange(10**10) for i in range(100)]
  3. In [124]: dd = float("inf")
  4. In [125]: for x in seq:
  5. ...: for y in seq:
  6. ...: if x == y: continue
  7. ...: d = abs(x-y)
  8. ...: if d < dd:
  9. ...: xx, yy, dd = x, y, d
  10. ...:
  11. In [126]: xx, yy
  12. Out[126]: (9733435205, 9734468535)

两层嵌套的循环,都对seq遍历,明显的平方级操作。接下来我们就优化下代码,先把列表排序,绝对值最小的两个数必然是相邻。于是代码如下。

 
 
  1. In [87]: seq.sort()
  2. In [124]: dd = float("inf")
  3. In [125]: for x in seq:
  4. ...: for y in seq:
  5. ...: if x == y: continue
  6. ...: d = abs(x-y)
  7. ...: if d < dd:
  8. ...: xx, yy, dd = x, y, d
  9. ...:
  10. In [126]: xx, yy
  11. Out[126]: (9733435205, 9734468535)

这样算法更快了,但解决方案照旧!

归纳法证明了递归法的适用性,而递归法则是直接实现归纳法思维的一种简单方式。

4.3

再来两个排序。(虽然以前写过多次。)

1. 插入排序迭代版
 
 
  1. def ins_sort(seq):
  2. if len(seq) <= 1:
  3. return seq
  4. for i in range(1, len(seq)):
  5. j = i
  6. while j > 0 and seq[j-1] > seq[j]:
  7. seq[j], seq[j-1] = seq[j-1], seq[j]
  8. j -= 1
  9. return seq
  10. if __name__ == "__main__":
  11. seq = [5, 3, 6, 9, 8, 2]
  12. print(ins_sort(seq))

递归版插入排序

 
 
  1. def ins_sort_rec(seq, i):
  2. if i == 0:
  3. return seq
  4. j = i
  5. while j > 0 and seq[j-1] > seq[j]:
  6. seq[j], seq[j-1] = seq[j-1], seq[j]
  7. j -= 1
  8. return ins_sort_rec(seq, i-1)
  9. if __name__ == "__main__":
  10. seq = [5, 3, 6, 9, 8, 2]
  11. print(ins_sort_rec(seq, 5))
2. 选择排序
 
 
  1. def sel_sort(seq):
  2. for i in range(len(seq)-1, 0, -1):
  3. max_j = i # 预设最大索引 max_j
  4. for j in range(i):
  5. if seq[j] > seq[max_j]:
  6. max_j = j # 实际最大的 max_j
  7. seq[i], seq[max_j] = seq[max_j], seq[i] # 交换最大值
  8. return seq
  9. if __name__ == "__main__":
  10. seq = [5, 3, 6, 9, 8, 2]
  11. print(sel_sort(seq))

4~6行类似寻找一组数字中的最大值。这里是找最大的索引值! 
递归版

 
 
  1. def sel_sort_rec(seq, i):
  2. if i == 0: return seq
  3. max_j = i
  4. for j in range(i):
  5. if seq[j] > seq[max_j]:
  6. max_j = j
  7. seq[i], seq[max_j] = seq[max_j], seq[i]
  8. return sel_sort_rec(seq, i-1)
  9. if __name__ == "__main__":
  10. seq = [5, 3, 6, 9, 8, 2]
  11. print(sel_sort_rec(seq, 5))

4.4 基于归纳法(与递归法)的设计

4.4.1 寻找最大排列

有 a, b, c, d, e, f, g, h 八个人。在电影院更换座位的问题!

图 4-4

箭头指向就是他们想要的座位。我们可以从图中找出其映射关系。这里就是各元素(0, ... n-1)与其位置(0, ... n-1)之间的关联性。我们可以用一个简单的列表实现。(各元素选择的座位作为其值。)

 
 
  1. >>> M = [2, 2, 0, 5, 3, 5, 7, 4]

接下来就可以简单的实现下了。

  • 寻找最大排列问题的递归思路的朴素解决方案
 
 
  1. In [8]: def naive_max_perm(M, A=None):
  2. ...: if A is None:
  3. ...: A = set(range(len(M)))
  4. ...: if len(A) == 1:
  5. ...: return A
  6. ...: B = set(M[i] for i in A)
  7. ...: C = A - B
  8. ...: if C:
  9. ...: A.remove(C.pop())
  10. ...: return naive_max_perm(M, A)
  11. ...: return A
  12. In [10]: naive_max_perm(M)
  13. Out[10]: {0, 2, 5}

这段代码中,函数naive_max_perm收到一个代表剩余人数的集合A, 并创建一个被指向座位的集合B。然后此函数会找出并删除集合A中某个不属于集合B的元素。之后,递归解决剩余人员。直至 A = B。 
此程序是平方级操作,最浪费的操作就是每次递归时集合B都要重复创建。为此能解决这个问题,我们也就可以将其复杂度变成线性级了!

我们可以为各元素设置一个计数器,当有指向x座位的人被淘汰时,就递减该座位的计数器,并当x的计数器为0时,将编号为x的人和座位一同淘汰掉即可。

  • 迭代版实现
 
 
  1. def max_perm(M):
  2. n = len(M)
  3. A = set(range(n))
  4. count = [0]*n
  5. for i in M:
  6. count[i] += 1 # 相当于 count = collections.Counter(M)
  7. Q = [i for i in A if count[i] == 0]
  8. while Q:
  9. i = Q.pop()
  10. A.remove(i)
  11. j = M[i]
  12. count[j] -= 1
  13. if count[j] == 0:
  14. Q.append(j)
  15. return A

计数排序

 
 
  1. from collections import defaultdict
  2. def count_sort(seq, key=lambda x: x):
  3. L, D = [], defaultdict(list)
  4. for i in seq:
  5. D[key(i)].append(i)
  6. # D -- {0: [0], 2: [2, 2], 3: [3], 4: [4], 5: [5, 5], 7: [7]})
  7. #这里顺序恰巧是排序好的,仅仅是巧合而已
  8. for key in range(min(D), max(D)+1):
  9. L.extend(D[key]) # 根据key来排序,而key的大小和value是对应的。当key不在D中时,返回 []
  10. # key的值从小到大,不受D中元素的顺序影响
  11. return L
  12. M = [2, 2, 0, 5, 3, 5, 7, 4]
  13. print(count_sort(M))

defaultdict 简化了处理不存在的键的场景。

 
 
  1. w = ['a', 'b', 'w', 'r']
  2. d = {}
  3. for i in w:
  4. if i in d:
  5. d[i] += 1
  6. else:
  7. d[i] = 1
  8. # use defaultdict
  9. d = defaultdict()
  10. for i in w:
  11. d[i] += 1

下面是python官方文档的例子。

 
 
  1. >>> s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]
  2. >>> d = defaultdict(list)
  3. >>> for k, v in s:
  4. ... d[k].append(v)
  5. ...
  6. >>> d.items()
  7. [('blue', [2, 4]), ('red', [1]), ('yellow', [1, 3])]
4.4.2 明星问题

所谓明星简单说: 就是别人都认识ta,而ta不认识别人。此算法有以下几种用处:

  1. 处理 Linux 中各种软件包的依赖问题!
  2. 多线程的死锁问题
 
 
  1. def naive_celeb(G): # 寻找 G 中的明星, G 是一个二维数组.
  2. n = len(G)
  3. for u in range(n): # 遍历每个数组
  4. for v in range(n): # 遍历数组中的每个元素
  5. if u == v: continue # 相同的人,跳过
  6. if G[u][v]: break # 明星认识路人, 结束此次循环
  7. if not G[v][u]: break # 路人不认识明星, 结束
  8. else:
  9. return u # u 是明星
  10. return None

可以很明显的看出, naive_celeb 函数的两次循环致使此程序的复杂度 O().现在我们就要使用归简法将问题的规模从 n 归简到 n-1,其实,对每个 u 都进行遍历是多余的,因为非明星认识不需再往下进行了,所以要做的就是排除掉非明星人士即可.下面就要解决这个问题,将其变为一个线性级的算法.

 
 
  1. def celeb(G):
  2. n = len(G)
  3. u, v = 0, 1 # 假设 u 是明星
  4. for c in range(2, n+1):
  5. if G[u][v]: u = c # u 认识 v,说明 u 不是明星,循环变量 c 赋值给 u,遍历下一组.
  6. else: v = c # u 是明星, c 赋值给 v 遍历G(u)中的下一个元素
  7. if u == n: c = v # 最后一次遍历后,u == n 说明 u 不是明星,则 v 是.
  8. else: c = u # u是明星,这是的 c 不是循环变量了.只是一个中间值,可以换成其他已声明的变量
  9. for v in range(n):
  10. if c == v: continue # 同一个人, 跳过
  11. if G[c][v]: break # 明星 c 认识路人, 卡
  12. if not G[v][c]: break # 路人不识明星, 卡
  13. else:
  14. return c
  15. return None

接下来,就要构建一个随机图,来验证函数的正确与否!

 
 
  1. In [273]: from random import randrange
  2. ...: n = 100
  3. ...: G = [[randrange(2) for i in range(n)] for _ in range(n)]
  4. ...: x = randrange(n) # 设置一个明星 x
  5. ...: for i in range(n):
  6. ...: G[i][x] = True # 所有人都认识明星 x
  7. ...: G[x][i] = False # 明星 x 不认识任何人
  8. ...:
  9. In [274]: x
  10. Out[274]: 19
  11. In [275]: naive_celeb(G)
  12. Out[275]: 19
  13. In [276]: celeb(G)
  14. Out[276]: 19

4.4.3 拓扑排序问题 #看不太懂, mark

定义:

在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序(英语:Topological sorting)。 
1. 每个顶点出现且只出现一次; 
2. 若A在序列中排在B的前面,则在图中不存在从B到A的路径。

DAG

  • 解决任务之间的依赖关系

当我使用 Debian 安装软件时,系统会提示缺少某个或某些组件,需要安装它们。对于这一类工作,相关组件必须按照一定的拓扑顺序来安装!接下来就实现下此算法。

清单 4-9 朴素版拓扑排序法

 
 
  1. def naive_topsort(G, S=None):
  2. if S is None: S = set(G) # all the nodes
  3. if len(S) == 1: # single node, return
  4. return list(S)
  5. v = S.pop()
  6. seq = naive_topsort(G, S)
  7. min_i = 0
  8. for i, u in enumerate(seq):
  9. if v in G(u):
  10. min_i = i + 1
  11. seq.insert(min_i, v)
  12. return seq

这是一个平方级算法,当每次任意选择一个节点时,其都要检查其余节点是否符合后续递归调用(线性操作)。因此,我们只要在递归调用之前找出被移除的节点即可。

 
 
  1. def topsort(G):
  2. count = dict((u, 0) for u in G)
  3. for u in G:
  4. for v in G(u):
  5. count[v] += 1
  6. Q = [u for u in G if count[u] == 1]
  7. S = []
  8. while Q:
  9. u = Q.pop()
  10. S.append(u)
  11. for v in G[u]:
  12. count[v] -= 1
  13. if count[v] == 0:
  14. Q.append(v)
  15. return S

第五章 遍历:算法学中的万能钥匙

清单 5-1 遍历一个表示为邻接集的图结构的连通分量

 
 
  1. def walk(G, s, S=set()): # G 是一邻接集的字典表示
  2. P, Q = dict(), set()
  3. P[s] = None
  4. Q.add(s)
  5. while Q:
  6. u = Q.pop()
  7. for v in G[u].difference(P, S): # v 是 set 对象
  8. Q.add(v)
  9. P[v] = u
  10. return P

我们初开始可以找一个出发点作为根节点,然后从此节点开始走,我们可以往左走,可以往右走,随你。当我们走到一个死胡同时,就要进行回溯。

walk 函数所遍历的只是单个分量,下面这个该图的所有连通分量。 
测试一下:

 
 
  1. In [16]: G2 = {
  2. ...: 0:{1, 2},
  3. ...: 1:{1, 3},
  4. ...: 2:{2, 4},
  5. ...: 3:{0, 3},
  6. ...: 4:{4, 5},
  7. ...: 5:{2, 3}
  8. ...: }
  9. In [26]: walk(G2, 1)
  10. Out[26]: {0: 3, 1: None, 2: 0, 3: 1, 4: 2, 5: 4}
  11. In [27]: walk(G2, 0)
  12. Out[27]: {0: None, 1: 0, 2: 0, 3: 1, 4: 2, 5: 4}

清单 5-2 找出图的连通分量

 
 
  1. def component(G):
  2. comp = []
  3. seen = set()
  4. for u in G:
  5. C = walk(G, u)
  6. seen.update(C)
  7. comp.append(C)
  8. return comp

测试一下:

 
 
  1. In [18]: component(G2)
  2. Out[18]:
  3. [{0: None, 1: 0, 2: 0, 3: 1, 4: 2, 5: 4},
  4. {0: 3, 1: None, 2: 0, 3: 1, 4: 2, 5: 4},
  5. {0: 3, 1: 0, 2: None, 3: 5, 4: 2, 5: 4},
  6. {0: 3, 1: 0, 2: 0, 3: None, 4: 2, 5: 4},
  7. {0: 3, 1: 0, 2: 5, 3: 5, 4: None, 5: 4},
  8. {0: 3, 1: 0, 2: 5, 3: 5, 4: 2, 5: None}]

挺好的。

5.1 公园漫步

5.1.1 不允许出现环路

迷宫

遍历一个没有环路的迷宫,其基本结构是一棵数。保持单手扶墙走就能遍历整个迷宫。其迷宫只有一面内墙,只要不出现环路,我们总能到达一个确定的地方。我们初开始可以找一个出发点作为根节点,然后从此节点开始走,我们可以往左走,可以往右走,随你。当我们走到一个死胡同时,就要进行回溯。顺着这样的策略,我们就能探索到所有节点,而且每条通道都会经过两次。下面是一个最基本的的实现。 
清单 5-3 递归的树遍历算法

 
 
  1. def tree_walk(T, r): # 从 r 开始遍历
  2. for u in T[r]: # 对于每一子节点...
  3. tree_walk(T, u) # ...遍历子树
5.1.2 停止循环遍历的方式

每次进入或离开一个十字路口时留下一个标志就行了。避免重复走同一条通道。下面是一个 Tremaux 算法。所使用的是深度优先搜索。(最基本最重要的遍历策略之一) 
清单 5-4 递归版的深度优先搜索

 
 
  1. def rec_dfs(G, s, S=None):
  2. if S is None:
  3. S = set()
  4. S.add(s)
  5. for u in G[s]:
  6. if u in S:
  7. continue
  8. rec_dfs(G, u, S)

下面将递归操作转换成迭代版,以此来避免调用栈被塞满带来的问题。 
清单 5-5 迭代版深度优先搜索

 
 
  1. def iter_dfs(G, s):
  2. S, Q= set(), []
  3. Q.append(s)
  4. while Q:
  5. u = Q.pop()
  6. if u in S: # 遍历过的就跳过
  7. continue
  8. S.add(u)
  9. Q.extend(G[u])
  10. yield u

用图2-3 的图结构测试一下测试看看

 
 
  1. In [71]: a, b, c, d, e, f, g, h = range(8)
  2. ...: G = {
  3. ...: a: [b, c, d, e, f], # a 的所有出边,下同
  4. ...: b: [c, e], # b
  5. ...: c: [d], # c
  6. ...: d: [e], # d
  7. ...: e: [f], # e
  8. ...: f: [c, g, h], # f
  9. ...: g: [f, h], # g
  10. ...: h: [f, g] # h
  11. ...: }
  12. In [73]: list(iter_dfs(G, 0))
  13. Out[73]: [0, 5, 7, 6, 2, 3, 4, 1]

我们刚才是在一个有向图上进行的 DFS 。DFS或者其他遍历算法都适用于有向图上,但如果在有向图上进行遍历时,就不能指望它探索所有的连通分量了。比如下面:

 
 
  1. In [74]: list(iter_dfs(G, 1))
  2. Out[74]: [1, 4, 5, 7, 6, 2, 3

因为 a 节点不存在入边, 从 a 节点以外的任何一点出发都不能到达 a 节点!但我们可以有以下三种方法来找出有向图中的连通分量:

  1. 将有向图转换为无向图
  2. 直接为图中的所有边添加反向边
  3. 选择合适的起始节点

清单 5-6 通用性的图遍历函数

 
 
  1. def traverse(G, s, qtype=set):
  2. S, Q = set(), qtype()
  3. Q.add(s)
  4. while Q:
  5. u = Q.pop()
  6. if u in S:
  7. continue
  8. S.add(u)
  9. for v in G[u]:
  10. Q.add(v)
  11. yield u

这里默认类型为 set ,我们也可以将其轻松定义成 stack 类型。使用的是一个是 List 的 append 和 pop 方法来模栈。

 
 
  1. class stack(list):
  2. add = list.append

测试下看看喽:

 
 
  1. In [82]: list(traverse(G, 0, stack))
  2. Out[82]: [0, 5, 7, 6, 2, 3, 4, 1]

5.2

深度优先的时间戳与拓扑排序

在 DFS 树中,任意 u 节点下的所有节点都应该在 u 被访问并完成回溯操作之间这段时间内处理完成。为此,我们为清单 5-6 的版本中的每个节点添加时间戳。

  • d 代表它被探索的时间
  • f 代表回溯的时间
 
 
  1. def dfs(G, s, d, f, S=None, t=0):
  2. if S is None:
  3. S = set()
  4. d[s] = t
  5. t += 1
  6. S.add(s)
  7. for u in G[s]:
  8. if u in S:
  9. continue
  10. t = dfs(G, u, d, f, S, t)
  11. f[s] = t
  12. t += 1
  13. return t

我们还可以此 DFS 来进行拓扑排序,根据其递减的完成时间对节点进行排序。 
清单 5-8

 
 
  1. def dfs_topsort(G):
  2. S, res = set(), []
  3. def recurse(u):
  4. if u in S:
  5. return
  6. S.add(u)
  7. for v in G[u]:
  8. recurse(v)
  9. res.append(u)
  10. for u in G:
  11. recurse(u)
  12. res.reverse()
  13. return res

用 G 测试一下:

 
 
  1. In [5]: G
  2. Out[5]:
  3. {0: [1, 2, 3, 4, 5],
  4. 1: [2, 4],
  5. 2: [3],
  6. 3: [4],
  7. 4: [5],
  8. 5: [2, 6, 7],
  9. 6: [5, 7],
  10. 7: [5, 6]}
  11. In [14]: dfs_topsort(G)
  12. Out[14]: [0, 1, 2, 3, 4, 5, 6, 7]

5.3

无限迷宫与最短(不加权)路径问题

清单5-9 
清单 5-10 广度优先搜索

 
 
  1. def bfs(G, s):
  2. P, Q = {s: None}, deque([s])
  3. while Q:
  4. u = Q.popleft()
  5. for v in G[u]:
  6. if v in P:
  7. continue
  8. P[v] = u
  9. Q.append(v)
  10. return P

测试一下:

 
 
  1. In [22]: from queue import deque
  2. ...: for i in range(8):
  3. ...: print(bfs(G, i))
  4. ...:
  5. {0: None, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 5, 7: 5}
  6. {1: None, 2: 1, 4: 1, 3: 2, 5: 4, 6: 5, 7: 5}
  7. {2: None, 3: 2, 4: 3, 5: 4, 6: 5, 7: 5}
  8. {3: None, 4: 3, 5: 4, 2: 5, 6: 5, 7: 5}
  9. {4: None, 5: 4, 2: 5, 6: 5, 7: 5, 3: 2}
  10. {5: None, 2: 5, 6: 5, 7: 5, 3: 2, 4: 3}
  11. {6: None, 5: 6, 7: 6, 2: 5, 3: 2, 4: 3}
  12. {7: None, 5: 7, 6: 7, 2: 5, 3: 2, 4: 3}

5.4 强连通分量

5.5 本章小结

通常情况下,一个图的遍历过程主要包括: 
维护一个用来存放待探索节点的 to-do 列表,并从中除去已访问的节点,从遍历的起点开始,每次都访问 (并除去) 其中一个节点,并将其邻居节点加入到该列表中。在此列表中,项目的 (调度) 顺序很大程度上决定了我们实现的遍历类型。 
比如:

  • 采用 LIFO 队列执行的就是 DFS
  • 采用 FIFO 队列执行的就是 BFS

第六章 分解、合并、解决

6.2 经典分治算法

清单 6-1 分治语义的一种通用性实现
 
 
  1. def divide_and_conquer(S, divide, combine):
  2. if len(S) == 1:
  3. return S
  4. L, R = divide(S)
  5. A = divide_and_conquer(L, divide, combine)
  6. B = divide_and_conquer(R, divide, combine)
  7. return combine(A, B)

6.3 折半搜索

清单 6-2 二分搜索树的插入与搜索
 
 
  1. class Node:
  2. lft = None
  3. rgt = None
  4. def __init__(self, key, val):
  5. self.key = key
  6. self.val = val
  7. def insert(node, key, val):
  8. if node is None:
  9. return Node(key, val)
  10. if node.key == key:
  11. node.val = val
  12. elif node.key < key:
  13. node.rgt = insert(node.rgt, key, val)
  14. else:
  15. node.lft = insert(node.lft, key, val)
  16. def search(node, key):
  17. if node is None:
  18. raise KeyError
  19. if node.key == key:
  20. return node.val
  21. elif node.key < key:
  22. return search(node.rgt, key)
  23. else:
  24. return search(node.lft, key)
  25. class Tree:
  26. root = None
  27. def __setitem__(self, key, val):
  28. self.root = insert(self.root, key, val)
  29. def __getitem__(self, key):
  30. return search(self.root, key)
  31. def __contains__(self, key):
  32. try:
  33. search(self.root, key)
  34. except KeyError:
  35. return False
  36. return True
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值