常见算法梳理

发布时间 2023-05-09 19:56:58作者: 风茗

前言: 

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、自己买的书:算法导论、图解算法