时空复杂度
思想
查找
二分查找★
简单版
int bs(int[] arr, int l, int r, int target) { // 找到最靠近target且>=target的indexwhile (l < r) {int mid = (r - l) / 2 + l;if (arr[mid] < target) {l = mid + 1;} else {r = mid;}}return l;}
完整版
int findBoundary(int[] nums, int target, int direction) {int left = 0, right = nums.length - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] < target) {// 更新leftleft = mid + 1;} else if (nums[mid] > target) {// 更新rightright = mid - 1;} else if (nums[mid] == target) {if (direction == 0) {// 找左边界,收缩右边界right = mid - 1;} else {// 找右边界,收缩左边界left = mid + 1;}}}if (direction == 0) {if (left >= nums.length || nums[left] != target)return -1;return left;} else {if (right < 0 || nums[right] != target)return -1;return right;}}
快速选择★
解决数组第K大元素的问题
搜索
这里指在树和图中搜索,综合应用枚举、贪心、回溯、递归、剪枝等思想
DFS★
判出口(终点、越界)->剪枝->扩展->标记->递归->还原
/** Return true if there is a path from cur to target.*/boolean DFS(Node cur, Node target, Set<Node> visited) {return true if cur is target;for (next : each neighbor of cur) {if (next is not in visited) {add next to visted;return true if DFS(next, target, visited) == true;}}return false;}
BFS★
如果不需要确定当前遍历到了哪一层
queue.push(root)while queue 不空:cur = queue.pop()for 节点 in cur的所有相邻节点:if 该节点有效且未访问过:queue.push(该节点)
如果要确定当前遍历到了哪一层
level表示二叉树遍历到哪一层或者图走了几步、size表示在当前层有多少个元素
queue.push(root)level = 0while queue 不空:size = queue.size()while (size --) {cur = queue.pop()for 节点 in cur的所有相邻节点:if 该节点有效且未被访问过:queue.push(该节点)}level ++;
回溯★
常用于找到所有可能路径的题
- 模板要素
- 路径集<路径<选择>>的含义?
- 结束条件是什么?
- 如何枚举选择?
- 选择如何符合要求?
模板
List<List<Integer>> 路径集 = new ArrayList<>();private void backtrack(路径, 选择or选择列表) {if(满足结束条件) {路径集.add(路径); // 深拷贝return;}for (选择: 选择列表) {if(选择符合要求) {路径.add(选择); // 做选择backtrack(路径, 新选择or新选择列表); // 对新的未探索区域递归路径.remove(路径.size() - 1); // 撤销选择}}}
记忆化
搜索过程中需要记忆某些状态便于后续复用
思路
参考https://zhuanlan.zhihu.com/p/108344917
* 初始化open_set和close_set;* 将起点加入open_set中,并设置优先级为0(优先级最高);* 如果open_set不为空,则从open_set中选取优先级最高的节点n:* 如果节点n为终点,则:* 从终点开始逐步追踪parent节点,一直达到起点;* 返回找到的结果路径,算法结束;* 如果节点n不是终点,则:* 将节点n从open_set中删除,并加入close_set中;* 遍历节点n所有的邻近节点:* 如果邻近节点m在close_set中,则:* 跳过,选取下一个邻近节点* 如果邻近节点m在open_set中,则:* 判断节点n到节点m的 F(n) + cost[n,m] 值是否 < 节点m的 F(m) 。来尝试更新该点,重新设置f值和父节点等数据* 如果邻近节点m也不在open_set中,则:* 设置节点m的parent为节点n* 计算节点m的优先级* 将节点m加入open_set中
排序★
简单排序
插入
冒泡
分治排序
快速
void QuickSort(int arr, int left, int right) {if(left >= right)return;int i = left, j = right, pivot = arr[left];while(i < j) {while(i<j && arr[j] >= pivot) j--;arr[i] = arr[j];while(i<j && arr[i] <= pivot) i++;arr[j] = arr[i];}arr[i] = pivot;QuickSort(arr, left, i - 1);QuickSort(arr, i + 1, right);}
归并
树形排序
选择
堆
分配排序
桶
基数
双指针★
查找子字符串
滑动窗口★
求最大窗口大小(for版本)
int maxLen = 0; // 窗口最大值int sum = 0; // 当前状态for(int l = 0, r = 0; r < arr.length; r++) {sum += arr[r]; // 根据加入右指针更新状态if (sum <= maxCost) { // 满足约束maxLen = Math.max(maxLen, r - l + 1); // 更新窗口最大值} else { // 不满足约束sum -= arr[l]; // 根据去除左指针更新状态l++; // 左指针右移}}return maxLen;
求最大窗口大小(双while版本)
模板1
while(r < n){UPDATE STATE(r)while(WRONG){UPDATE STATE(l)l++}MAXORMIN(ans)r++}
模板2
int l = 0, r = 0, sum = 0, maxLen = 0; // 初始化左右指针,状态值,窗口大小while (r < n) { // 右指针不越界sum += arr[r]; // 根据加入右指针更新状态while(sum > maxCost) { // 不符合约束条件sum -= arr[l]; // 根据去除左指针更新状态l++; // 左指针右移}maxLen = Math.max(maxLen, r - l + 1); // 更新窗口最大值r++; // 右指针右移}return maxLen;
模板3
int l = 0, r = 0, sum = 0, maxLen = 0; // 初始化左右指针,状态值,窗口大小while (r < n) { // 右指针不越界sum += arr[r]; // 根据加入右指针更新状态r++; // 右指针右移while(sum > maxCost) { // 不符合约束条件sum -= arr[l]; // 根据去除左指针更新状态l++; // 左指针右移}maxLen = Math.max(maxLen, r - l); // 更新窗口最大值(注意区别)}return maxLen;
窗口不需要减小
int l = 0, r = 0, sum = 0;for(; r < n; r++) {if(sum + arr[r] > maxCost) { // 不满足约束,左右指针同时移动sum += arr[r] - arr[l]; // 更新状态l++;} else { // 满足约束,右指针移动sum += arr[r]; // 更新状态}}return r - l;
复杂样板
public String minWindow(String s, String t) {int left = 0, right = 0; // 滑动窗口前后指针int min = Integer.MAX_VALUE; // 最小子串的长度int start = 0, end = 0; // 最小子串的左右位置int count = 0; // 相同字符的个数Map<Character, Integer> tMap = new HashMap<>(); // target串的字符计数(目标)Map<Character, Integer> sMap = new HashMap<>(); // source串的字符计数(窗口)// 初始化target串的字符计数for (int i = 0; i < t.length(); ++i) {tMap.put(t.charAt(i), tMap.getOrDefault(t.charAt(i), 0) + 1);}while (right < s.length()) {char c = s.charAt(right);// 更新窗口状态if (tMap.containsKey(c)) { // 是所求字符sMap.put(c, sMap.getOrDefault(c, 0) + 1); // 存字符进窗口if (tMap.get(c).compareTo(sMap.get(c)) == 0) { // 看是不是该字符达标count++;}}right++; // 右滑动扩大while (count == tMap.size()) {// 满足条件,更新最值if (min > right - left) {end = right;start = left;min = right - left;}char d = s.charAt(left);// 更新窗口状态if (tMap.containsKey(d)) {sMap.put(d, sMap.get(d) - 1);if (tMap.get(d) > sMap.get(d)) {count--;}}left++; //左滑动缩小}}return min == Integer.MIN_VALUE ? "" : s.substring(start, end);}
快慢指针★
动态规划
通用思考模板
状态定义:一维还是二维?
- 转移方程:有几种状态?状态之间依赖关系?
- 初始条件
- 结果表示
-
计数问题
线性DP
最长公共/上升子序列
-
树状DP
背包DP
定义:i表示物品编号,j表示容量(不超过容量j / 容量恰好是j),质量为w,价值为v
- 朴素方程:f[i][j]=max(f[i-1][j],f[i-1][j-kw[i]]+kv[i])
- 一维优化:f[j]=max(f[j],f[j-w[i]]+v[i]) 【容量维度从小到大遍历】 遍历物品{遍历容量{}}
背包分类
- 01背包:每个元素最多选取一次
- 解法:外循环nums,内循环target,target倒序且target>=nums[i];
- 完全背包(排列数):每个元素可以重复选择
- 解法:外循环nums,内循环target,target正序且target>=nums[i];
- 组合背包:背包中的物品要考虑顺序
- 解法:外循环target,内循环nums,target正序且target>=nums[i];
- 分组背包问题:不止一个背包,需要遍历每个背包
- 解法:外循环背包bags,内部两层循环根据题目的要求转化为以上三种背包类型的模板
问题分类:
- 求解最值
- 解法:dp[i] = max/min(dp[i], dp[i-nums]+1)或dp[i] = max/min(dp[i], dp[i-num]+nums);
- 求解存在(存在… 满足…)
- 解法:dp[i] = dp[i] || dp[i-num];
求解组合(所有满足…的组合(数))
区间最值问题(RMQ)
ST表
矩阵连乘
-
数位DP
-
状压DP
倍增
-
数学
高精度计算
位运算★
计算几何
扫描线
排列组合★
集合与离散数学
概率论
数论
判断质数★
线性代数
论证
博弈
判断先手处于“必胜态”还是“必败态”
2类的思考方式: 经验分析:见过类似的题目,猜一个性质,然后去证明该性质是否可推广。
- 状态分析:根据题目给定的规则是判断「胜利」还是「失败」来决定优先分析「必胜态」还是「必败态」时具有何种性质,然后证明性质是否可推广。
最大公约数gcd★
public int gcd(int x, int y) {return y > 0 ? gcd(y, x % y) : x;}
最小公倍数lcm
摩尔投票法★
求中位数/众数
快速幂★
递归思想
