常见算法梳理

前言: 

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- 二分逻辑: 大了咋办 小了咋办
4- 细节:
  a- 到底要给 mid 加一还是减一: 看区间开闭
  b- while 里到底用 大于等于 还是 小于




二:滑动窗口 快慢指针
1- 关联: 节流算法: 固定窗口、滑动窗口、漏桶、令牌桶; 协议:tcp滑动窗口、慢启动
2- 维护一个窗口,窗口内元素满足一定的条件, 通过移动窗口的左右边界来得到满足条件的子序列、子数组、字串
3- 扩张窗口:寻找可行解, 收缩窗口:优化可行解
4- 注意初始窗口
其他双指针技巧:
1- 碰撞指针,相向而行
2- 中心发散




三:贪心
1- np完全问题、近似解,尝试能否通过局部最优,达到整体最优: 举不出明显反例
2- 问题:分饼干、



四:回溯
1- 场景:子集、排列、组合
 1.解决一个回溯问题,实际上就是一个决策树的遍历过程,站在回溯树的一个节点上,你只需要思考 3 个问题: 
  1、路径:也就是已经做出的选择。 path: [ ]
  2、选择列表:也就是你当前可以做的选择。[ ]    arr 
  3、结束条件:也就是到达决策树底层,无法再做选择的条件。 收集结果  result.push(path)
       4、递归函数的参数: 
                  a:当前数组
                  b:startIndex:  选择列表的startIndex,前面的选择过了
                  c:当前路径列表:path   []
                  d:结果集:  result  []
                  e:备忘录: 比如元素哪些使用,哪些没有用
复制代码
result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return
    
    for 选择 in 选择列表:
        做选择              // ----------------->  选1, 选2, 选3, 选。。。n; 然后递归

backtrack(路径, 选择列表)
        撤销选择
==============================================================================================================================
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」
回溯算法的「决策树」;每个节点上都在做决策。
   1- 回溯算法, 通过递归来控制有多少层for循环
   1- 回溯算法是在遍历「树枝」, DFS 算法是在遍历「节点」
   1- 回溯树的深度? 宽度? 
  
  补充: 
  1-  dfs(total, arr, i, sum + arr[i], path, res); 这种就不需要回溯了, 因为sum没有改变,不会影响后续的循环和递归, 如果sum放到dfs函数调用外边就需要回溯了
   2-  排列不需要使用startIndex, 因为1,2和2,1不同, 排列讲究顺序! 所以排列只要求满足:元素不重复选取; if (used[i] == true) continue;
   3- 如果要对树层中前一位去重,就用used[i - 1] == false, 如果要对树枝前一位去重用used[i - 1] == true。 对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!、
   4- 去重必须先排序, 让相同的元素挨在一起;  树层去重: 解决集合有重复元素时,要保证组合、排列不会重复; if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false)
   5- 如何区分我用的哪一个元素呢, 如果两个元素值都一样,但是索引不一样, 这时候只能用used数组来做标记
 
   1- 排列问题: 元素不能重复使用, 元素使用次序无关
    2- 组合问题: 元素不能重复使用, 元素使用次序有关
    3- 回溯算法: 递归模拟for循环嵌套, 递归里嵌套for循环,for循环再递归


复制代码
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循环的递归

 6- 全排列

 7- N皇后

 8- 等和子数组

 


五:动态规划
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- 背包问题: 01背包   完全背包 

2- 斐波那契数列

复制代码
//   O(2^n),指数级别
var fib = function(N) {
    if (N === 1 || N === 2) return 1;
    return fib(N - 1) + fib(N - 2);
};
===============================================================================
// 一: 递归解法    带memo的递归:  时间复杂度是 O(n)
var fib = function(N) {
    // 备忘录全初始化为 0
    let memo = new Array(N + 1).fill(0);   //   为啥N+1???
    // 进行带备忘录的递归
    return dp(memo, N);
};

// 带着备忘录进行递归
var dp = function(memo, n) {    //  递归三部曲:  递归函数参数, 递归出口, 单层逻辑
    // base case
    if (n == 0 || n == 1) return n;
    // 已经计算过,不用再计算了
    if (memo[n] != 0) return memo[n];
    memo[n] = dp(memo, n - 1) + dp(memo, n - 2);
    return memo[n];
};
复制代码
复制代码
//  二: dp 数组的迭代解法
var fib = function(N) {
    if (N === 0) return 0;
    let dp = new Array(N + 1).fill(0);  //   下边循环从2开始, 所以这里N+1?     因为dp[0]不用, 所以保证索引从1开始? 
    // base case
    dp[0] = 0; dp[1] = 1;
    // 状态转移
    for (let i = 2; i <= N; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];  //  循环过程中: 填充dp数组, 后边的循环计算依赖前面的计算结果, 所以从前往后遍历
    }

    return dp[N];
};
复制代码
复制代码
//  三:空间压缩  空间压缩: 空间复杂度降为 O(1)
var fib = function(n) {
    if (n === 0 || n === 1) {
        // base case
        return n;
    }
    // 分别代表 dp[i - 1] 和 dp[i - 2]      上面dp[i]:  靠dp[i-1] dp[i-2]计算出来
    let dp_i_1 = 1, dp_i_2 = 0;      
    for (let i = 2; i <= n; i++) {
        // dp[i] = dp[i - 1] + dp[i - 2];
        let dp_i = dp_i_1 + dp_i_2;
        // 滚动更新
        dp_i_2 = dp_i_1;
        dp_i_1 = dp_i;
    }
    return dp_i_1;
};
复制代码

 

3- 

 

思考: 

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 思否           

 

 

 

 

 

 

 




七:单调栈、单调队列
(数据结构)








八:前缀和数组、差分数组(缓存)
前缀和:
频繁计算一个区域的元素之和。 所以先算出来做一个缓存,, 以后再用直接O(1)复杂度
复制代码
//  如何求一个数组的【i,j】区间和: 一般思路: 每次计算时间复杂度都是O(n)
let NumArray1 = function(nums) { this.nums = nums;
} NumArray1.prototype.sumRange
= function(left, right) { let res = 0; for (let i = left; i <= right; i++) { res += this.nums[i]; } return res;
}
// 前缀和思路: 第一次计算之后时间复杂度就是O(1)
let NumArray = function(arr) {                  
this.preSum = new Array(arr.length + 1) // 索引+1
this.preSum[0] = 0
for(let i=1; i<arr.length; i++) {
this.preSum[i] = this.preSum[i-1] + arr[i]
}
}
// [left ,right] 区间累加和
NumArray.prototype.sumRange = function(left, right) {
return this.preSum[right+1] - this.preSum[left]
}

应用
1- 求数组左右乘积
复制代码
复制代码
eg:班上有若干同学,每个同学有一个期末考试的成绩(满分 100 分),那么请你实现一个 API,输入任意一个分数段,返回有多少同学的成绩在这个分数段内。
let scores = [8, 9, 6, 10, 5, 5]   //  6名同学
let count = new Array(scores.length + 1).fill(0)  //  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
for(let score of scores) {
    count[score]++        //  落在每个分数上的学生  //   [0, 0, 0, 0, 0, 2, 1, 0, 1, 1, 1]        -----------》 接下来计算落在[left, right]之间的人有多少
}

// 构造前缀和数组:  方便接下来求和
let preSum = new Array(scores.length + 1).fill(0)
for(let i=1; i<count.length; i++) {    // i 从1开始       1  2  3  4  5  6  7  8  9 10
    preSum[i] = preSum[i-1] + count[i]   //          [0, 0, 0, 0, 0, 2, 3, 3, 4, 5, 6]
}
function getScoreSpan(left, right) {  //  [8 , 9]
    return count[right+1] - count[left]   // 6 - 4
}
复制代码

 

 






九:并查集(树)
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、自己买的书:算法导论、图解算法




 











































posted @   风茗  阅读(61)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示