算法总结
Array题目总结
1.题目分类
-
双指针
-
同向双指针
-
相向双指针
-
-
滑动窗口
- 使用hash map进行count
-
前缀和、后缀和
-
利用前缀和降低时间复杂度
-
前缀和 + Hash Table:
- 这类题目其实都是2Sum的变种,利用hash table记录下标,从而如果发现有合法的子数组后,能够直接求出子数组区间
- 325.Maximum Size Subarray Sum Equals k、525.Contiguous Array
-
-
区间问题
- 扫描线算法
-
排序算法的考察
- 归并排序
- 快速排序
- 快速选择
-
和各个数据结构结合
- Hash Table
- Stack
- 单调栈
- Heap
- Queue
2.注意点
Tree题目总结
1.题目分类
-
全局prev变量、全局sum变量
-
树的path sum相关
-
LCA(最近公共祖先)
-
BST相关(遇到BST的题一定要考虑它的性质)
- 删除BST的节点
-
树的遍历
- 非递归前序中序遍历
- 使用Stack实现树的前序、中序遍历
- 树的层序遍历BFS (套模版较简单)
- 求树的最大宽度
- 树的序列化
-
递归左右子树:分治法思想
- 判断镜像树
- 旋转左右节点
-
树中的距离
- 求树的最大直径(最长的任意两点间的距离=>转化为求树高)
- 求树中最长路径(转化为求左右子树路径,然后左右加起来和全局的比较)
-
借助Stack
- 判断某个序列是否是合法的遍历序列
-
树中的路径(树的路径定义为树中任意两个节点间的距离)
- 求最长的路径 :
先分治得出left
和right
,然后更新全局res
的时候是left + right
,但是本次递归返回的是max(left, right)
-
- Binary Tree Maximum Path Sum
-
- Diameter of Binary Tree
-
- Longest Univalue Path
-
- 求最长的路径 :
2.补充知识点
1) BST的性质
二叉查找树要么是一棵空树,要么是一棵具有如下性质的非空二叉树:
- 若左子树非空,则左子树上的所有结点的关键字值均小于根结点的关键字值
- 若右子树非空,则右子树上的所有结点的关键字值均大于根结点的关键字值
- 左、右子树本身也分别是一棵二叉查找树(二叉排序树)
可以看出,二叉查找树是一个递归的数据结构,且对二叉查找树进行中序遍历,可以得到一个递增的有序序列
比如lc 98题,不使用全局变量,采用递归左右子树的形式,判断BST :
class Solution {
public boolean isValidBST(TreeNode root) {
return helper(root, null, null);
}
private boolean helper(TreeNode node, Integer max, Integer min) {
if (node == null) return true;
if (min != null && node.val <= min) return false;
if (max != null && node.val >= max) return false;
return helper(node.left, node.val, min) && helper(node.right, max, node.val);
}
}
3.注意点
DFS 总结
1.DFS问题核心思想
-
将大问题分解为小问题,小问题递归解决:
- 从top-down分解下去,计算却是bottom-up计算上来,最终将结果传到顶层;
-
每一步有多个路径可以走,则有的路径走不通了后就需要回溯,回溯记得将原来改变的状态恢复(擦屁股);
2.一般解题思路
-
棋盘类的题目
- 一般就是按照四个方向扩展,再加上一些条件看能不能继续向某个方向扩展,遍历的起始点可能是内部某点,也可能是边界;
- 具体可参考490. The Maze 、489. Robot Room Cleaner
- 一般就是按照四个方向扩展,再加上一些条件看能不能继续向某个方向扩展,遍历的起始点可能是内部某点,也可能是边界;
-
问题分解类的题目
-
大问题分解为小问题,可能涉及记忆化搜索 memo + dfs,比如字符串分割;:
-
87.Scramble String、140. Word Break、329. Longest Increasing Path in a Matrix、 638. Shopping Offers、935.Knight Dialer
-
比较典型的dfs + memo: 329. Longest Increasing Path in a Matrix
class Solution { int m, n; int[][] dirs = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}}; public int longestIncreasingPath(int[][] matrix) { if (matrix.length == 0 || matrix[0].length == 0) return 0; m = matrix.length; n = matrix[0].length; int[][] memo = new int[m][n]; int res = 0; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { int tmp = helper(matrix, i, j, memo); res = Math.max(res, tmp); } } return res; } private int helper(int[][] matrix, int x, int y, int[][] memo) { if (memo[x][y] != 0) return memo[x][y]; int res = 1; for (int[] d : dirs) { int nx = x + d[0]; int ny = y + d[1]; if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue; if (matrix[nx][ny] <= matrix[x][y]) continue; int tmp = helper(matrix, nx, ny, memo) + 1; res = Math.max(tmp, res); } memo[x][y] = res; return res; } }
- 比较典型的dfs + memo: 935.Knight Dialer
但是书已到这题的memo是二维的,也就是每一步的状态,其实涉及到两个变量,即每一步按到哪个数,和当前是第几个数,所以是
memo[num][index]
,饭容易范这个错误,将memo写成以维的memo[num]
class Solution { int[][] neighbors = {{4, 6}, {6, 8}, {7, 9}, {4, 8}, {0, 3, 9}, {}, {0, 1, 7}, {2, 6}, {1, 3}, {2, 4}}; int MOD = 1_000_000_007; public int knightDialer(int N) { int res = 0; int[][] memo = new int[10][N + 1]; for (int i = 0; i < 10; i++) { res += helper(i, N, memo); res %= MOD; } return res; } private int helper(int num, int index, int[][] memo) { if (index == 1) return 1; if (memo[num][index] != 0) return memo[num][index]; int res = 0; for (int next : neighbors[num]) { res += helper(next, index - 1, memo); res %= MOD; } memo[num][index] = res; return res; } }
-
-
-
Combination类型及其变种:
-
- Matchsticks to Square、698.Partition to K Equal Sum Subsets
-
3.注意点
0.记得加上visited数组,防止重复访问而产生overflow
BFS总结
1.题目分类
-
树、图的层序遍历
-
拓扑排序
- 判断有向图是否有环
-
求最短距离
- 棋盘上的最短路径
2.一般解题思路
3.注意点
DP总结
1.一般解题思路
dynamic programming的适用情况
最优子结构:问题可以被分解为求解子问题的最优解,也就是现在的解依赖于子问题的左右解
子问题重复计算 :子问题具有重复求解行,所以可以事先存储下来,以便于之后直接获取,从而避免子问题对的重复求解
无后效性:子问题的最优解是确定的,且一旦子问题的最优解得到后,原问题的最优解可以用子问题的最优解求解
2.题目分类
-
Matrix DP
-
Sequence DP (单sequence) 一维的问题
-
典型题:LIS (注意LIS的两种姿势)
-
最小调整代价
-
2 sets of subproblems: 原问题的最优解依赖于两个子问题的最优解 (一般从左向右扫描一次,然后再从右向左扫描一次,最后再合并左右的结果)
for (int i = 1; i < A.length; i++) { if (A[i] > A[i - 1]) inc[i] = inc[i - 1] + 1; } for (int i = A.length - 2; i > 0; i--) { if (A[i] > A[i + 1]) dec[i] = dec[i + 1] + 1; } //合并 for (int i = 0; i < A.size(); i++) { if (inc[i] && dec[i]) { res = Math.max(res, inc[i] + dec[i] + 1); } }
- 198. House Robber : 每一步可能存在抢活或者不抢
public int maxProduct(int[] nums) { if (nums == null || nums.length == 0) return 0; int n = nums.length; int[] mins = new int[n]; int[] maxs = new int[n]; int res = nums[0]; mins[0] = nums[0]; maxs[0] = nums[0]; for (int i = 1; i < n; i++) { maxs[i] = mins[i] = nums[i]; if (nums[i] > 0) { maxs[i] = Math.max(maxs[i - 1] * nums[i], nums[i]); mins[i] = Math.min(mins[i - 1] * nums[i], nums[i]); } else if (nums[i] < 0) { maxs[i] = Math.max(mins[i - 1] * nums[i], nums[i]); mins[i] = Math.min(maxs[i - 1] * nums[i], nums[i]); } res = Math.max(res, maxs[i]); } return res; }
/** 题意:求乘积最大的子数组,关键是原数组中有正负数 分析:那么肯定是用动态规划了,本质是用两个dp数组,一个维护至今的最大值,一个维护至今的最小值 ; 想的简单点,就是维护一个至今的最大值和最小值数组,max[i]表示到i为止的最大的,min[i]表示迄今最小值 当然简化了也可以用两个变量max和min,也就是用来个状态来维护,只需要当A[i] < 0的时候交换min和max就行了 如果是负数,则就会使得到当前为止的最大值变为最小值,当前为止的最小值变为最大值; 应该维护两个变量,一个是至今的最大值一个至今的最小值,然后还有一个全局的最大值; */ //两个dp数组版本: public int maxProduct(int[] nums) { if (nums == null || nums.length == 0) return 0; int n = nums.length; int[] mins = new int[n]; int[] maxs = new int[n]; int res = nums[0]; mins[0] = nums[0]; maxs[0] = nums[0]; for (int i = 1; i < n; i++) { maxs[i] = mins[i] = nums[i]; if (nums[i] > 0) { maxs[i] = Math.max(maxs[i - 1] * nums[i], nums[i]); mins[i] = Math.min(mins[i - 1] * nums[i], nums[i]); } else if (nums[i] < 0) { maxs[i] = Math.max(mins[i - 1] * nums[i], nums[i]); mins[i] = Math.min(maxs[i - 1] * nums[i], nums[i]); } res = Math.max(res, maxs[i]); } return res; } //两个状态变量: public int maxProduct(int[] nums) { if (nums == null || nums.length == 0) return 0; int n = nums.length; int res = nums[0], tmpMin = res, tmpMax = res; for (int i = 1; i < n; i++) { if (nums[i] < 0) { int tmp = tmpMin; tmpMin = tmpMax; tmpMax = tmp; } tmpMax = Math.max(tmpMax * nums[i], nums[i]); tmpMin = Math.min(tmpMin * nums[i], nums[i]); res = Math.max(res, tmpMax); } return res; }
-
具有多个状态:
dp[i][0]、dp[i][1] dp[i][2] ...i is problem size
-
-
Two Sequences Converging
- 典型题:LCS
-
背包问题
3.注意点
数据结构总结
1.题目分类
-
Stack
-
单调栈
-
递归问题转为迭代形式(很多递归问题都也可以用stack解决)
-
用栈模拟:根据题目的性质,这时候分析几个例子,查看是否具有栈的性质,比如和栈顶元素关系直接这种情况
-
-
Hash Table
-
HashMap
-
TreeMap
有序key-value,一般按照key有序组织,可以找到第一个比当前key小的:
floorKey()
或者大的key:ceilingKey()
,在有些题目中很有用 -
-
Queue
-
堆Heap / PriorityQueue
-
k largest ...注意O(klogk)解法
-
-
用queue模拟:根据题目的性质,这时候分析几个例子
-
-
Linked List (一般有几个常考的套路)
- 翻转链表模版
private ListNode reverse(ListNode head){ ListNode newNode=null; while(head!=null){ ListNode temp=head.next; head.next=newNode; newNode=head; head=temp; } return newNode; }
-
几个基本操作:类似题目题目234、25可以分解为这几个基本操作:求链表中点(求链表第n个点)、反转链表
-
类似题目24需要前后两个指针prev、cur来交替操作
-
类似题目86、328属于分割链表,借助dummy node
-
Union Find
- 并查集模版
class UF { int[] parent; public UF(int N) { parent = new int[N]; for (int i = 0; i < N; i++) parent[i] = i; } public int find(int x) { if (parent[x] != x) parent[x] = find(parent[x]); return parent[x]; } public void union(int x, int y) { parent[find(x)] = find(y); } }
- 并查集模版
-
Trie
-
Graph
2.一般解题思路
3.注意点
其他重要算法
1.题目分类
-
贪心法
-
分治法
-
Sort
- 手撕快排(这个肯定的)
- 手撕归并排序 (这个必须的)
-
扫描线算法
- Meeting rooms problem:
- 252.Meeting-Rooms (M)
- 253.Meeting-Rooms-II (M+)
- 056.Merge-Intervals (M)
- 057.Insert-Intervals (M)
- 732.My-Calendar-III (M)
- 759.Employee-Free-Time (M+)
- 370.Range-Addition (M+)
-
概率题
-
Segment Tree