第零章 核心框架
双指针->单链表
合并两个有序链表
链表的分解
合并k个有序链表
寻找单链表的倒数第k个节点
寻找单链表的中点
判断单链表是否包含环并找出环起点
判断两个单链表是否相交并找出交点
双指针->数组
快慢指针
原地修改数组
对数组某些元素进行原地删除
左右指针
二分查找
两数之和
反转数组
回文串判断
二叉树(纲要篇)
Tips:快速排序就是二叉树的前序遍历,归并排序是二叉树的后序遍历
- 快速排序框架:
1 | def sort(nums, lo, hi){ |
- 归并排序框架:
1 | def sort(nums, lo, hi){ |
二叉树遍历框架:
1 | def traverse(root){ |
前序位置的代码只能从函数参数中获取父节点传递来的数据,而后序位置的代码不仅可以获取参数数据,还可以获取到子树通过函数返回值传递回来的数据
两种解题思路:
遍历一遍二叉树得出答案
分解问题计算出答案
后序位置的特殊之处:
层序遍历:
1 | def levelTraverse(root){ |
BFS算法框架
动态规划(框架)
动态规划框架:
1 | # 自顶向下递归的动态规划 |
解题思路:
暴力穷举,带备忘录的递归和dp数组的迭代(可降空间复杂度为O(1))
斐波那契数列
凑零钱
总结
计算机解决问题其实没有任何奇技淫巧,它唯一的解决办法就是穷举, 穷举所有可能性。算法设计无非就是先思考“如何穷举”,然后再追求“如何聪明地穷举”。
列出状态转移方程,就是在解决“如何穷举”的问题。之所以说它难,一是因为很多穷举需要递归实现,二是因为有的问题本身的解空间复杂,不那么容易穷举完整。
备忘录、DP table 就是在追求“如何聪明地穷举”。用空间换时间的思路,是降低时间复杂度的不二法门,除此之外,试问,还能玩出啥花活?
回溯算法(框架)
Tips:回溯算法和 DFS 算法的细微差别是:回溯算法是在遍历「树枝」,DFS 算法是在遍历「节点」
回溯算法框架
1 | res = [] |
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」
全排列问题
N皇后问题
总结
时间复杂度都不可能低于 O(N!),因为穷举整棵决策树是无法避免的。这也是回溯算法的一个特点,不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高。
写 backtrack
函数时,需要维护走过的「路径」和当前可以做的「选择列表」,当触发「结束条件」时,将「路径」记入结果集。
回溯->排列,组合,子集
无论是排列、组合还是子集问题,简单说无非就是让你从序列 nums
中以给定规则取若干元素,主要有以下几种变体:
- 元素无重不可复选,即
nums
中的元素都是唯一的,每个元素最多只能被使用一次,这也是最基本的形式。
以组合为例,如果输入 nums = [2,3,6,7]
,和为 7 的组合应该只有 [7]
。
- 元素可重不可复选,即
nums
中的元素可以存在重复,每个元素最多只能被使用一次。
以组合为例,如果输入 nums = [2,5,2,1,2]
,和为 7 的组合应该有两种 [2,2,2,1]
和 [5,2]
。
- 元素无重可复选,即
nums
中的元素都是唯一的,每个元素可以被使用若干次。
以组合为例,如果输入 nums = [2,3,6,7]
,和为 7 的组合应该有两种 [2,2,3]
和 [7]
。
但无论形式怎么变化,其本质就是穷举所有解,而这些解呈现树形结构,所以合理使用回溯算法框架,稍改代码框架即可把这些问题一网打尽。
回溯算法核心
子集(元素无重不可复选)
组合(元素无重不可复选)
排列(元素无重不可复选)
子集/组合(元素可重不可复选)
排列(元素可重不可复选)
子集/组合(元素无重可复选)
排列(元素无重可复选)
总结
元素无重复不可复选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25# 组合/子集问题回溯算法框架
def backtrack(nums, start){
for i in range(start, len(nums)){
# 做选择
tarck.addLast(nums[i])
# 注意参数
backtrack(nums, i+1)
# 撤销选择
track.removeLast()
}
}
# 排列问题回溯算法框架
def backtrack(nums){
for i in range(len(nums)){
# 剪枝逻辑
if used[i]: continue
# 做选择
used[i] = true
track.addLast(nums[i])
backtrack(nums)
# 撤销选择
track.removeLast()
used[i] = false
}
}元素可重不可复选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33nums.sort()
# 组合/子集问题回溯算法框架
def backtrack(nums, start){
# 回溯算法标准框架
for i in range(start, len(nums)){
# 剪枝逻辑,跳过相同的相邻树枝
if i>start and nums[i]==nums[i-1]: continue
# 做选择
track.addLast(nums[i])
# 注意参数
backtrack(nums, i+1)
# 撤销选择
track.removeLast()
}
}
nums.sort()
# 排列问题回溯算法框架
def backtrack(nums){
for i in range(len(nums)){
# 剪枝逻辑
if used[i]:continue
# 剪枝逻辑,固定相同的元素在排列中的相对位置
if i>0 and nums[i]==nums[i-1] and not used[i-1]: continue
# 做选择
used[i] = true
track.addLast(nums[i])
backtrack(nums)
#撤销选择
track.removeLast()
used[i] = false
}
}元素无重可复选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22# 组合/子集问题回溯框架
def backtrack(nums, start){
# 回溯算法标准框架
for i in range(start, len(nums)){
# 做选择
track.addLast(nums[i])
# 注意参数
backtrack(nums, i)
# 撤销选择
track.removeLase()
}
}
# 排列问题回溯算法框架
def backtrack(nums){
for i in range(len(nums)){
# 做选择
track.addLast(nums[i])
backtrack(nums)
# 撤销选择
track.removeLast()
}
}
BFS(框架)
BFS框架:
二叉树的最小高度
解开密码锁的最少次数
双向BFS优化
传统的 BFS 框架就是从起点开始向四周扩散,遇到终点时停止;而双向 BFS 则是从起点和终点同时开始扩散,当两边有交集的时候停止。
二分搜索
寻找一个数(基本的二分搜索)
1
2
3
4
5
6
7
8
9
10
11
12
13
14def binarySearch(nums, target){
left, right = 0, len(nums)-1
while(left<=right):
mid = left+(right-left)//2
if(nums[mid]==target):
# 直接返回
return mid
elif nums[mid]>target:
right = mid-1
elif nums[mid]<target:
left = mid+1
# 直接返回
return -1
}寻找左侧边界的二分搜索
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20def left_bound(nums, target){
left, right = 0, len(nums)-1
# 搜索区间为[left, right]
while left<=right:
mid = left + (right-left)//2
if nums[mid]==target:
# 收缩右侧边界
right = mid+1
elif nums[mid]<target:
# 搜索区间变为[mid+1, right]
left = mid+1
elif nums[mid]>target:
# 搜索区间变为[left, mid-1]
right = mid
# 判断target是否存在于nums中
# 此时target比所有数都大,返回-1
if left==len(nums): return -1
# 判断一下nums[left]是不是target
return nums[left]==target ? left : -1
}寻找右侧边界的二分查找
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15def right_bound(nums, target){
left, right = 0, len(nums)-1
while left<=right:
mid = left+(right-left)//2
if nums[mid]<target:
left=mid+1
elif nums[mid]>target:
right = mid-1
elif nums[mid]==target:
# 收缩左边界
left = mid+1
# 最后改成返回left-1
if left-1 < 0: return -1
return nums[left-1]==target ? left-1 :-1
}
总结:
LeetCode34 在排序数组中查找元素的第一个和最后一个位置
最基本的二分查找算法
1
2
3
4
5
6
7因为我们初始化 right = nums.length - 1
所以决定了我们的「搜索区间」是 [left, right]
所以决定了 while (left <= right)
同时也决定了 left = mid+1 和 right = mid-1
因为我们只需找到一个 target 的索引即可
所以当 nums[mid] == target 时可以立即返回寻找左侧边界的二分查找
1
2
3
4
5
6
7
8因为我们初始化 right = nums.length
所以决定了我们的「搜索区间」是 [left, right)
所以决定了 while (left < right)
同时也决定了 left = mid + 1 和 right = mid
因为我们需找到 target 的最左侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧右侧边界以锁定左侧边界寻找右侧边界的二分查找
1
2
3
4
5
6
7
8
9
10
11因为我们初始化 right = nums.length
所以决定了我们的「搜索区间」是 [left, right)
所以决定了 while (left < right)
同时也决定了 left = mid + 1 和 right = mid
因为我们需找到 target 的最右侧索引
所以当 nums[mid] == target 时不要立即返回
而要收紧左侧边界以锁定右侧边界
又因为收紧左侧边界时必须 left = mid + 1
所以最后无论返回 left 还是 right,必须减一
滑动窗口
滑动窗口框架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18left, right = 0,0
while right<len(s):
# c是将移入窗口的字符
window.add(s[right])
# 增大窗口
right += 1
# 进行窗口内数据的一系列更新
...
# debug输出的位置
printf("Window:[%d, %d]\n", left, right)
# 判断左侧窗口是否要收缩
while window needs shrink:
# d是将移出窗口的字符
char d = s[left]
# 缩小窗口
left+=1
# 进行窗口内数据的一系列更新
...最小覆盖子串
字符串排列
找所有字母异位词
最长无重复子串
总结:
1. 什么时候应该扩大窗口?
1. 什么时候应该缩小窗口?
1. 什么时候应该更新答案?
买股票(DP)
状态转移方程
1
2
3
4
5
6
7base case:
dp[-1][...][0] = dp[...][0][0] = 0
dp[-1][...][1] = dp[...][0][1] = -infinity
状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])base case
1
2
3
4
5
6
7
8
9
10
11
12
13dp[-1][...][0] = 0
解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0。
dp[-1][...][1] = -infinity
解释:还没开始的时候,是不可能持有股票的。
因为我们的算法要求一个最大值,所以初始值设为一个最小值,方便取最大值。
dp[...][0][0] = 0
解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0。
dp[...][0][1] = -infinity
解释:不允许交易的情况下,是不可能持有股票的。
因为我们的算法要求一个最大值,所以初始值设为一个最小值,方便取最大值。
打家劫舍(DP)
打家劫舍1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16# 带备忘录
def rob(nums):
n = len(nums)
if n==0: return 0
if n==1: return nums[0]
dp = [0]*n
dp[0], dp[1] = nums[0], max(nums[0], nums[1])
for i in range(2, n):
dp[i] = max(dp[i-1], dp[i-2]+nums[i])
return dp[n-1]
# 优化空间
def rob(nums):
prev, curr = 0, 0
for i in nums:
prev, curr = curr, max(curr, prev+1)
return curr打家劫舍2
打家劫舍3
nSum
两数之和
3Sum
4Sum
第一章 数据结构
链表
1
拉宾-卡普