算法小抄 | 第1章
其实在今年五六月份就发现了这个Github上很火的一个项目,但当时出于懒惰并没有仔细钻研
一直到上个月(2020.11),因为要准备CSP,所以才开始看这个算法指南
猛然发现,这个指南讲解的真的很人性化。就仿佛以一种向傻子(指我自己)阐述的口吻,来解释算法题和解题套路。
于是乎,这本《labuladong的算法小抄》刚出版两天,我便直接下单了。
作者labuladong仿佛是应届毕业生,就是比我大一届,秋招拿了十二个offer。orz🤕
在此做一些学习笔记,以此督促自己认真学习并加深理解。
由于本人比较熟悉python,所以笔记中的框架及具体代码都是基于python的。
动态规划套路
动态规划的三个关键
base case
DP table的定义
状态转移方程
看清了这三个之后,解决问题就如鱼得水了,直接结合例题来寻找感觉。
例题
假设现在要找的零钱为 amount = 11,硬币面值为coins = [1, 2, 5],尝试找到所需硬币最少的方法
N叉树解法
把大问题分解成小问题
想要知道找11块钱最少需要多少个硬币,我只要知道
- 找10块钱需要的最少硬币数
- 找9块钱需要的最少硬币数
- 找6块钱需要的最少硬币数
即可得出最后答案,那么我们按照这个思路来写递归
1 | def count_coins(amount, coins): |
这样子去解决递归问题,会进行大量的重复计算(想象成一个N叉树)
- 比如我想知道找11块最少需要多少硬币,自然会想知道10,9,6块的解
- 但是我在知道10块的解之后,我又会诞生一个新的求解9块的子问题(11-2=9,10-1=9)
备忘录优化解法
- 通过新建一个哈希表来记录已经求解过的子问题的答案,来减少重复的递归
1 | def count_coins(amount, coins, memo): |
DP Table解法
上面两种解法都是依靠递归,也就是自顶向上的解法,DP table属于是自底向上求解
因为不会用到递归,所以也不会报类似超过最大递归深度的错误
1 | RecursionError: maximum recursion depth exceeded in comparison |
我们用数组来模拟递归,从最简单的问题求解即可
1 | def count_coins(amount, coins): |
回溯法套路
- 关键思想:递归求解问题,每次尝试一种可能性,尝试到底之后再回溯
三个关键问题
- 路径:已经做出的选择
- 选择列表:下一步可以进行的选择
- 结束条件:要知道什么时候退出递归
回溯法的算法框架
1 | result = [] |
全排列问题
假设我要求[1,2,3]的全排列,那么针对三个关键问题
- 路径:用一个数组来记录我已经选择了哪些数字
- 选择列表:既然是全排列,下一步能选择的数字就是[1,2,3]中我还没有加入路径的数字
- 结束条件:当我路径的长度为3时,我就知道已经实现了一种排列方式
依据这三点可以很容易写出一个递归函数
1 | res = [] |
- 回溯法就是纯暴力穷举,复杂度一般都很高
N皇后问题
给定一个NxN的棋盘,需要在每一行放一个皇后,最后保证
- 每一列只有一个皇后
- 每根和对角线平行的斜线上只有一个皇后
回溯法三个关键
- 路径:就是记录我已经放了哪些皇后,一个二维数组来代表棋盘
- 选择列表:下一步能放的位置肯定是某一行的N个位置
- 结束条件:如果我现在的摆放已经无法满足上述条件,就直接退出递归;如果我把N行摆满了,且满足规则要求,说明这是一种合理的摆法。
思路
为了方便描述棋盘的状态,我们将棋盘设置成一个N*N的二维数组
某格为0表示这格不放皇后,若为1表示这格放置皇后
- 逐行放置皇后棋子
- 为了能快速确定选择列表,我们定义了三个集合
- cols:已经放过的皇后的列数
- left:和从左下↙到右上↗的对角线的平行线中哪些已经被放置了皇后
- 我们用皇后所在位置的行数-列数来标明这是哪根线
- 比如9*9的棋盘中,只要是行数-列数 = 8的都在中间的那根斜对角线上
- right:和left同理,标记的是从左上↖到右下↘的平行线
代码实现
1 | res = [] |
BFS算法框架
本质就是找起点到终点的最短路径。
一般框架如下:
1 | def bfs(root, target): |
二叉树的最小高度
解决一个问题即可套上面的模板:
起点和终点分别是什么?
起点就是根节点,终点就是叶节点
OK,直接套模板(说是模板,其实就是把整个过程模拟出来)
1 | def bfs(root): |
理解了队列,二叉树和BFS思路之后,在脑子里边模拟边写即可
这里不需要visited集合来去重是因为我们是一直向下遍历,二叉树向下遍历并不会有重复结点
关键问题理解
为什么BFS可以找到最短路径,DFS不能找到?
BFS可以找到最短路径是因为它所尝试的每一步都是这一步的所有可能,它借助队列,实现了一步一步齐头并进,每一次都是二叉树往下扩展一层。
DFS其实也可以找到最短路径,但是需要把二叉树的每一个树枝走到底,最后才能找到最短的记录。相比以行的扩展方式而言,这种操作会浪费不少时间。
DFS相比BFS而言有优势吗?
BFS虽然能找到最短路径,但是空间复杂度高,因为每次都是尝试的一层的全部可能性。
如果只是单纯需要找到路径,BFS的空间复杂度为O(N),但是DFS的空间复杂度就是O(logN)。
一般在找最短路径的时候会使用到BFS,其他一般情况下都是用DFS(也可以说成是回溯法)
解开密码锁的最少次数
一道相对较难的BFS实践
有一个带有四个圆形拨轮的转盘锁,每个拨轮都有0-9共10个数字,每个拨轮都可以上下旋转,例如0→9,9→0
一开始的时候转盘初始为4个0,用字符串"0000"表示,现在有一个列表
deadends
和一个字符串target
deadends
这个列表中会放一些死亡组合,要避免播出这个组合,返回播出target
的最少次数。当然也会有无法播出的情况,返回-1即可。
按照思路套模板即可。
1 | def bfs(deadends, target): |
- 看起来很简单,但是我们需要更深入地研究。
传统的BFS(也就是我上面的这个递归函数)只是单纯地自顶向下求解,事实上,我们可以使用双向BFS来优化整体的过程。
双向BFS会从起点和终点两边开始扩散,只需要遍历半棵树就能找到交集。
要注意,使用双向BFS的前提是要知道终点的状态
1 | def bfs(deadends, target): |
- 针对双向BFS的优化
- 双向BFS中,会从两边交换进行扩散,队列中的元素越多,扩散出来的新元素也就越多
- 针对这一点,我们可以控制每次扩散的队列是元素较少的队列
- 使得以尽可能小的空间代价,来扩散两个队列找到交点
1 | while queue1 and queue2: |
双指针技巧套路框架
快慢指针可以实现
判断链表中是否有环
- 相当于追及问题,快指针每次走两步,慢指针每次走一步
- 如果快慢指针相遇,说明快指针超了慢指针一圈(超了一个环的长度)
1
2
3
4
5
6
7
8def has_cycle(head):
fast, slow = head, head
while fast and fast.next:
fast = fast.next.next
slow = slow.next
if fast == slow:
return True
return False已知链表中有环,找到环的起始位置
- 首先跟上文类似,先用追及问题的方式,找到两个指针第一次相遇的位置
- 此时慢指针走了k步,快指针走了2k步
- 假设两者的相遇点和环的起点距离为m
- 那么环的起点和头节点head的距离就是k-m
- 也就是说,如果从头节点开始走,走k-m步就会到达环的起始位置
- 巧的是,如果从当前相遇的位置出发,走k-m步也会到达环的起始位置
- 此时,让慢指针回到起点,重新出发,快指针不动
- 两者以同样的速度,每次前进一格出发
- 两者相遇的位置就是环的起点
1
2
3
4
5
6
7
8
9
10
11
12
13
14def has_cycle(head):
fast, slow = head, head
while fast and fast.next:
fast = fast.next.next
slow = slow.next
if fast == slow:
# 找到两者相遇的位置
break
slow = head
while slow != fast:
# 找到两者第二次相遇的位置
fast = fast.next
slow = slow.next
return slow真的神奇!✨
寻找无环单链表的中点
快指针每次前进两步,慢指针每次前进一步
当快指针走到链表尽头的时候,慢指针就恰好在链表的中点了
如果链表长度为奇数,慢指针就是在正中间,如果链表长度为偶数,慢指针就是在中点偏右
1
2
3
4
5
6def find_mid_node(head):
fast, slow = head, head
while fast and fast.next:
fast = fast.next.next
slow = slow.next
return slow寻找单链表中的倒数第k个结点
让快指针先前进k步,然后慢指针从head出发,两个指针同时每次前进一格
快指针走到底,慢指针就指向倒数第k个结点了
1
2
3
4
5
6
7
8def find_kth_node(head, k):
fast, slow = head, head
for i in range(k):
fast = fast.next
while fast:
fast = fast.next
slow = slow.next
return slow