前言:
1- 算法的本质就是合理的穷举:无遗漏无冗余; 然后剪枝、空间换时间、空间压缩
2- 回溯算法是在遍历「树枝」,DFS 算法是在遍历「节点」, BFS是从一个点发散,DFS是一个方向深度走下去
一:二分搜索
function binarySearch(arr, target) { let left = 0; let right = arr.length - 1 // 注意 while (left <= right) { let mid = Math.floor((left + right) / 2) // parseInt if (arr[mid] === target) { // 逻辑 return mid } else if(arr[mid] > target) { // 逻辑 left = mid + 1 // 注意 } else if (arr[mid] < target) { // 逻辑 right = mid - 1 // 注意 } } }
1- 找上下边界!边界确定,是否包含边界 2- 然后取pivot 3- 二分逻辑: 大了咋办 小了咋办
细节:
1- 到底要给mid加一还是减一: 看区间开闭
2- while 里到底用<=还是<维护一个窗口,窗口内元素满足一定的条件, 通过移动窗口的左右边界来得到满足条件的子序列、子数组、字串
二:滑动窗口
1- 关联: 节流算法: 固定窗口、滑动窗口、漏桶、令牌桶; 协议:tcp滑动窗口、慢启动
2-
3- 扩张窗口:寻找可行解, 收缩窗口:优化可行解
4- 注意初始窗口
三:贪心
1- np完全问题、近似解,尝试能否通过局部最优,达到整体最优: 举不出明显反例
四:回溯
1- 场景:子集、排列、组合
1.解决一个回溯问题,实际上就是一个决策树的遍历过程,站在回溯树的一个节点上,你只需要思考 3 个问题:
1、路径:也就是已经做出的选择。 path: [ ]
2、选择列表:也就是你当前可以做的选择。[ ]
3、结束条件:也就是到达决策树底层,无法再做选择的条件。 收集结果 result.push(path)
4、递归函数的参数:
a:startIndex
b:结果收集数组
c:当前数组
2- 尝试画决策树:多叉树的遍历/多叉树的遍历: 每一个结点就是一个for循环,从当前选择列表挨个做选择,然后回溯重新选择, n个递归函数向下去递归, 直到path.length === arr.length 叶子节点
3- 树枝去重 树层去重
4- 迭代与递归写法:
// 迭代 private void traversal(int[] nums) { for (int i = 0; i < nums.length; i++) { System.out.println(nums[i]); } } // 递归三部曲: 函数参数、终止条件、单层逻辑 private void traversal(int[] nums, int index) { // 回溯startIndex的理解 if (index == nums.length) return ; //出口 // 处理当前元素 System.out.println(nums[i]); // 递归处理 index + 1 后的元素 traversal(nums, index + 1); // ( 数组,startIndex ) } traversal(nums, 0);
5- 带for循环的递归
五:动态规划
1- 应用场景
a:标准的dp:求最值
b:重复子问题
c:最优子结构
2- 深度手动阀手动阀,画递归树,来找出状态转移关系
3- 推导dp公式: 数学归纳法
4- 空间压缩思路: 二维数组投影到一维数组
一: 自顶向下递归的动态规划 def dp(状态1, 状态2, ...): for 选择 in 所有可能的选择: # 此时的状态已经因为做了选择而改变 result = 求最值(result, dp(状态1, 状态2, ...)) return result 二: 自底向上迭代的动态规划 # 初始化 base case dp[0][0][...] = base case # 进行状态转移 for 状态1 in 状态1的所有取值: for 状态2 in 状态2的所有取值: for ... dp[状态1][状态2][...] = 求最值(选择1,选择2...)
思考:
1- dp与贪心: 无后效性
2- dp与回溯:
六: 优先队列
1- 保证一个元素的插入后数组是有序的, 排序方法:每个元素的优先级来排序! 大顶堆、小顶堆、满二叉树、堆序性、上滤swim、下滤sink, 取优先级最高元素:根节点, 时间复杂度O(1)
2- js简单实现
class PriorityQueue { constructor(compare) { this.queue = []; // 一个数组 this.compare = compare; // 一个排序方法 } offer(element) { // 添加元素: add(E e)和offer(E e)的语义相同,都是向优先队列中插入元素,只是Queue接口规定二者对插入失败时的处理不同,前者在插入失败时抛出异常,后则则会返回false。对于PriorityQueue这两个方法其实没什么差别。 this.queue.push(element); this.queue.sort(this.compare); } poll() { // remove()和poll()方法的语义也完全相同,都是获取并删除队首元素,区别是当方法失败时前者抛出异常,后者返回null。由于删除操作会改变队列的结构,为维护小顶堆的性质,需要进行必要的调整。 return this.queue.shift(); } peek() { // element()和peek()的语义完全相同,都是获取但不删除队首元素,也就是队列中权值最小的那个元素,二者唯一的区别是当方法失败时前者抛出异常,后者返回null return this.queue[0]; } isEmpty() { return this.queue.length === 0; } }
参考: How to use priority queue in javascript (hashnode.dev) javascript - 面试官:请使用JS实现一个优先队列 - 个人文章 - SegmentFault 思否
七:单调栈(数据结构)
八:前缀和数组、差分数组(缓存)
九:并查集(树)
1- 求解连通分量
十:DFS BFS(树 图)
十一:二叉树
1- 前中后序
2- 满二叉、平衡二叉
3 红黑树
4 B树
十二: 散列表
1- 底层实现:arryList LinkedList: 数组、链表实现方案
2- 散列函数:
3- 填装因子: 元素个数 / 地址总数 0.7
4- 用:dns解析、缓存
十三:其他todo
1- 分治
3- 减治
3- 分支界定
4- 图: 有向图、无向图
a:狄克斯特拉:加权图,权重为正, 权重为负:贝尔曼福德
5- knn算法、贝叶斯分类
6- 回文串:马拉车
7- 字符串:kmp
8- 散列表下:布隆过滤器
9- 数学知识复习: 方程、线代、概率论、离散数学、高数
10- 反向索引
11- LIS:二分解法
学习资源备忘:
1- labuladong官网 刷题插件 b站
2- 程序员卡尔官网、b站
3- leetcode:按类型来
4- 微信收藏、b站收藏
5- 电脑pdf、自己买的书:算法导论、图解算法