simpleYang的算法模板(Javascript)

二分查找

注意:为了防止相加之后越界所以先 right-left,这里必须整体用括号包裹,因为前面的 + 号优先级大于 >>

1、左闭右闭区间写法(最常用)
var search = function(nums, target) {
  let left = 0,right = nums.length-1
  while(left<=right){
    let mid = left + ((right-left)>>1)
    if(nums[mid] === target){
      return mid
    }else if(nums[mid] > target ){
      right = mid-1
    }else{
      left = mid + 1
    }
  }
  return -1
};
2、左闭右开区间(可以这么写,但一般直接用上面的更简洁点,注意边界导致的条件修改即可)
var search = function(nums, target) {
    let left = 0,right = nums.length // 1、这里的右边界就是越界的序号了,因为是右开
    while(left<right){ // 2、这里也不能等于了,因为right是无效值,终止条件是 left === right,我们拿不到这个right,等于每次是在[left,right)这个区间进行搜索
        let mid = left + ((right-left)>>1)
        if(nums[mid] === target){
            return mid
        }else if(nums[mid] > target ){
            right = mid // 3、同理right也不需要-1了,下一次区间是[left,right)
        }else{
            left = mid + 1
        }
    }
    return -1
};
3、寻找左侧边界的二分搜索

首先思考:返回值代表啥?
nums = [2,3,5,7],target = 1,算法会返回 0,含义是:nums中小于 1 的元素有 0 个
nums = [2,3,5,7], target = 8,算法会返回 4,含义是:nums中小于 8 的元素有 4 个
也代表着我们的模板值应该插入到数组的位置
于是返回条件就不是直接返回-1了,需要判断下返回值与数组长度的关系

注意点:在查找时,比如 [1,2,3,4,5] 找 -1,那么 left 返回的是 0 ,但是找6的话返回的 left 就是 5,右侧超出是会等于数组长度 arr.length 的,这里要手动减一才行。反之思考寻找右侧边界时,应该左侧最终返回 -1 ,右侧是 arr.length-1
找到 K 个最接近的元素

1)左闭右开写法(比较常见)

var search = function(nums, target) {
  if (nums.length === 0) return -1; // 直接返回

  let left = 0,right = nums.length
  while(left<right){
    let mid = left + ((right-left)>>1)
    if(nums[mid] === target){
      right = mid // 1、这里对比上面的模板,不需要返回了,因为还要继续搜索确定边界
    }else if(nums[mid] > target ){
      right = mid
    }else{
      left = mid + 1
    }
  }
  // 2、检查出界情况(这一步应该可以省略不写,因为正常返回left的话也可以帮我们找到小于target的值有多少个,写这个的话是找最左边那个target的左边有多少个,差不多意思)
  if (left >= nums.length || nums[left] != target) return -1;
  return left // 3、这里返回我们找到的边界序号
};

2)左闭右闭写法(统一)

var search = function(nums, target) {
  if (nums.length === 0) return -1; // 直接返回

  let left = 0,right = nums.length-1
  while(left<=right){  // 3、等于
    let mid = left + ((right-left)>>1)
    if(nums[mid] === target){
      right = mid - 1 // 1、只要是闭合,这里就要-1(相等时必须要-1缩小边界,不然[1,1]这种情况会死循环)
    }else if(nums[mid] > target ){
      right = mid - 1 // 2、只要是闭合,这里就要-1
    }else{
      left = mid + 1
    }
  }
  return left
};
4、寻找右侧边界的二分搜索

返回最后一个小于等于目标值的元素的序号
1)左闭右开写法(了解即可)

var search = function(nums, target) {
  if (nums.length === 0) return -1; // 直接返回

  let left = 0,right = nums.length // 1.注意
  while(left<right){
    let mid = left + ((right-left)>>1)
    if(nums[mid] === target){
      left = mid + 1 // 2、只要是闭合,这里就要+1
    }else if(nums[mid] < target ){
      left = mid + 1 // 3、只要是闭合,这里就要+1
    }else{
      right = mid
    }
  }
  if (left>nums.length || nums[left-1] != target)  return -1; // 5、这里的边界条件如果要返回right-1的话,就要改成right-1>0 || nums[right-1]!==target。建议用left,因为上面left一直+1在缩减边界,更清晰
  return left - 1 // 4、这里要减一,右侧的左闭右开写法专属,因为相等时left会加一,所以结束的时候肯定nums[left]和target不相等。这里的left和right在所有左闭右开都是一样的,因为终止条件就是相等
};

2)左闭右闭写法(统一)

var search = function(nums, target) {
  if (nums.length === 0) return -1; // 直接返回

  let left = 0,right = nums.length-1
  while(left<=right){
    let mid = left + ((right-left)>>1)
    if(nums[mid] === target){
      left = mid + 1 // 1、只要是闭合,这里就要+1
    }else if(nums[mid] < target ){
      left = mid + 1 // 2、只要是闭合,这里就要+1
    }else{
      right = mid - 1
    }
  }
  if (right<0 || nums[right] != target)  return -1;
  return right
};
注意

search( [2,3,5,7],6) 返回 2
search( [2,3,5,7],1) 返回 - 1
search( [2,3,5,7],8) 返回 3
相同的数会返回最右边的一个值的索引,没有相等的数就返回边界最后一个比目标值小的值的索引
因为返回的 right 所以结果会比 left 小一位
上述算法找 mid 值都是找的左边,因为向下取整,如果要拿右边的值需要+1,这里遇见过题目的答案是专门找右边的,但是应该也可以用左边做出来,到时候遇见再看


数组螺旋遍历

从左到右存取链表中的值,默认-1。

	let arr = Array(m).fill(0).map(item=> Array(n).fill(-1)) 
    let dx = [-1,0,1,0],dy = [0,1,0,-1]  // 这里的值可以选择遍历方向,index 从 1 开始,选择首先遍历的方向,比如这里是优先从左到右,再从上到下
    let d = 1,x = 0 ,y = 0 // d === 1 是为了方便取模重复赋值
    while(head){
        arr[x][y] = head.val
        let a = x+dx[d],b=y+dy[d] // 下一个点的 x y
        if(a<0 || a>=m || b<0 || b>=n || arr[a][b] !== -1){ // 如果不符合要求,就改变方向,重置正确的下个点的坐标
            d = (d+1) % 4
            a = x+dx[d], b = y +dy[d]
        }
        x = a , y = b // 下一个点没问题就赋值给 x,y
        head = head.next
    }
    return arr

n x n的数组顺时针旋转90,180,270度通用思路

顺时针旋转90:先沿对角线反转矩阵,再沿竖中轴线反转矩阵;
顺时针旋转180:先沿横中轴线反转矩阵,再沿竖中轴线反转矩阵;
顺时针旋转270:先沿对角线反转矩阵,再沿横中轴线反转矩阵;

let len = matrix.length-1, halfLen = len / 2 // 记录最后一个元素的下标和边长的一半

// 按对角线反转矩阵
for(let i=0;i<=len;i++){
	for(let j=0;j<i;j++){
		[matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]]
	}
}

// 按竖中轴线反转矩阵
for(let i=0;i<=len;i++){
	for(let j=0;j<halfLen;j++){
		[matrix[i][j], matrix[i][len-j]] = [matrix[i][len-j], matrix[i][j]]
	}
}

// 按横中轴线反转矩阵
for(let i=0;i<=len;i++){
	for(let j=0;j<halfLen;j++){
		[matrix[len-i][j], matrix[i][j]] = [matrix[i][j], matrix[len-i][j]]
	}
}

tips:如果是 dfs 中使用,可以对 dx 和 dy 取 for (let i=0;i<4;i++){}

滑动窗口

数据是左闭右开格式,因为循环终止的条件是right === s.length
注意点:一般需要相加时有负数也用不了,无法判断左指针是否应该移动

var minWindow = function(s, t) {
  let left = 0,right = 0

  while(right < s.length){ // 这样写就成了右开区间,右区间最后终止时right === s.length,s[right]是没有意义的
    let rval = s[right]
    right ++
    // 进行窗口内数据的一系列更新,比如根据条件对哈希的更新

    // 判断左侧窗口是否要收缩
    while(左窗口需要收缩){
      // 如果需要使用到left指针的值,应该在这里,因为下面要+1了
      let lval = s[left]
      left++
      // 进行窗口内数据的一系列更新,比如根据条件对哈希的更新
    }

  }
  // 返回结果
};

注意点:
在找重复子串题中,一般定义一个 window 哈希记录窗口中的字符出现次数,一个 map 哈希存放我们的子串中字符出现次数,这个是固定的,我们要做的就是哈希动态存入 right 新入窗口字符和删除 left 退出的字符(如果存在的话)



大顶堆、小顶堆(优先队列)

定义:大顶堆的每个父节点都大于子节点,根节点是最大的值。小顶堆相反

如何创建堆
完全二叉树可以使用数组存储,堆又是完全二叉树,所以堆也可以用一个数组表示。
给定一个节点的下标 i (i从0开始) ,那么它的父节点位置为(i-1)/2 ,左子节点为 2i + 1 ,右子节点为 2i+2
于是我们可以创建类

注意点:
1、堆化和出堆操作都要进行down
2、入堆则要进行up
3、主要就是熟悉up、down的操作和原理,其他都是辅助函数写法很多,注意边界即可

class Heap{
    constructor(){
        this.queue = []
        this.size = 0
        this.compare = (a,b) => this.queue[a] > this.queue[b]
    }
    swap(a,b){
        [this.queue[a],this.queue[b]] = [this.queue[b],this.queue[a]]
    }
    shiftDown(index){
        let child =  (index<<1) + 1
        while(child < this.size){
            // 右子节点与左子节点对比,满足函数要求就替换成右子节点,下沉就多了一个这个判断
            if(child+1<this.size && this.compare(child+1,child)){
                child = child+1
            }
            // 如果父元素对比满足要求,不用替换了
            if(this.compare(index,child)){
                break
            }
            this.swap(child,index)
            index = child
            child = (index<<1)+1  // 必须放下面,放最上面会死循环
        }
    }
    shiftUp(index){
        let parent = (index - 1) >> 1// 先找到第一个父节点,判断是否可执行
        while(parent>=0){ // 如果存在 并且 没有break 就一直循环替换
            if(this.compare(parent,index)){ // 父节点 对比 子节点满足 函数的要求就不替换了,跟之前的不一样,这里是正向的
                break
            }
            this.swap(parent,index)
            index = parent
            parent = (index - 1) >> 1
        }
    }
    push(val){
        this.queue.push(val)
        this.shiftUp(this.size++)// 当前的size就是push之后的最后一个元素的下标,然后给size+1就是数组长度
    }
    pop(){
        this.swap(0,--this.size)
        this.shiftDown(0)
        return  this.queue.pop() // 这里可以直接替换、下沉、弹出,不用担心最后一个值再次被往上提,因为上面把size减一了,所以下面down中不会对比最后一个数值,如果用length就是length-1和length-2
    }
}




前中后序遍历迭代

function preorderTraverse(pRoot){
    if(!root){ return [] }
    let res = [], stack = []
    while(root || stack.length) {
        while(root){
            res.push(root.val) // 只有保存结果的位置不同,流程和中序一样 
            stack.push(root) 
            root= root.left 
        }
        root= stack.pop() 
        root= root.right
    }
    return res
}function inorderTraverse(root){
    if(!root){return [] }
    let res = [], stack = []
    while(root || stack.length) {
        while(root){
            stack.push(root)
            root= root.left
        }
        root= stack.pop()
        res.push(root.val)
        root= root.right
    }
    return res
}
: 先存根右左,再reverse。可以用上面的前序遍历改一下左右的顺序,也可以用下面这种
function fn(){
	let stack = [], res = []
    root && stack.push(root)
    while(stack.length > 0) {
        let cur = stack.pop()
        res.push(cur.val)
        cur.left && stack.push(cur.left)
        cur.right && stack.push(cur.right) // 因为上面是pop,所以先存的后出来
    }
    return res.reverse()
}

层序遍历

var bfs = function(root) {
  let q = [root] // 队列
  let level = 0 // 初始化层数
  while(q.length>0){
    let len = q.length
    for(let i=0;i<len;i++){
      let root = q.shift()
      if(root.left){
        q.push(root.left)
      }
      if(root.right){
        q.push(root.right)
      }
    }
    level++
  }
};

根据层序数组构建二叉树,arr = [2,3,4,5,6] 只有一维

const buildTree = (arr) => {
    let i = 0; //i每次用完都需要自增1,因为层序构造依赖于数组的索引
    let root = new TreeNode(arr[i++]);
    let NodeList = [root];
    while (NodeList.length) {
        let node = NodeList.shift();
        if (arr[i] !== '0') { //如果是空的就不创建节点
            node.left = new TreeNode(arr[i]); //创建左节点
            NodeList.push(node.left);
        }
        i++; //不管是不是空的,i都需要自增
        if (i == arr.length)
            return root; //如果长度已经够了就返回,免得数组索引溢出
        if (arr[i] !== '0') { //如果是空的就不创建节点
            node.right = new TreeNode(arr[i]); //创建右节点
            NodeList.push(node.right);
        }
        i++; //不管是不是空的,i都需要自增
        if (i == arr.length)
            return root; //如果长度已经够了就返回,免得数组索引溢出
    }
    return root;
}   


排序算法

归并排序

先二分(logn) 再合并(n) 所以是nlogn

	// 从上至下:递归分成两部分数组,通过队列把两个数组合并成有序的数组返回
	function erfen(arr){
        if(arr.length < 2) return arr // base case,只有一个就返回给上层开始合并(测试了这样分不会分成0个)
        let mid = arr.length >> 1
        let left = arr.slice(0,mid)
        let right = arr.slice(mid)
        return merge(erfen(left),erfen(right))
    }
    function merge(left,right){
        let res = []
        while(left.length && right.length){
            if(left[0]<right[0]){
                res.push(left.shift())
            }else {
                res.push(right.shift())
            }
        }
        left.length ? res.push(...left) :res.push(...right)
        return res
    }
    let arr = erfen(nums)

快排

复杂度:O(nlogn),最坏O(n2)

function quickSort(nums, left, right) {
	if (left >= right) return
	let l = left,r = right,base = l
	while (l < r) {
		while (nums[r] >= nums[base] && l < r) r--
		while (nums[l] <= nums[base] && l < r) l++
		[nums[r],nums[l]] = [nums[l],nums[r]] // 循环着左右互换,保证基准值左边的都小于右边的
	}
	[nums[l],nums[base]] = [nums[base],nums[l]] // 基准值与最后一个小于基准值的交换,到了中间
	quickSort(nums, left, l - 1)
	quickSort(nums, l + 1, right)
}


动态规划

「base case」:一般确立了状态转移方程,才能知道对应的base case
「最优子结构」:子问题间必须互相独立,不会影响牵制
「重叠子问题」:动态规划之所以比暴力算法快,是因为动态规划技巧消除了重叠子问题
如何发现重叠子问题? 看是否可能出现重复的「状态」。对于递归函数来说,函数参数中会变的参数就是「状态」

backtrack(i + 1, rest - nums[i]);
backtrack(i + 1, rest + nums[i]); 

这里的 rest 和 i 是可以变的,假设 nums[i] === 0,那么两次遍历其实是干了同一件事,所以我们需要把状态 (i, rest)
存起来,也就是递归终止时返回 0 或者 1,父层把子层的结果相加然后存储。

「状态转移方程」
解题套路:
看有多少个可变的状态,一般就是几维
1、第一种思路模板是一个一维的 dp 数组

let len = array.length;
let dp = new Array(arr.length).fill(xxxx); // data base看情况定

for (let i = 1; i < len ; i++) {
    for (let j = 0; j < i; j++) {
        dp[i] = 最值(dp[i], dp[j] + ...)
    }
}

典型:「最长递增子序列」——在子数组 array[0…i] 中(更清晰一点叫以 i 结尾的子数组中),我们要求的子序列(最长递增子序列)的长度是 dp[i]

2、第二种思路模板是一个二维的 dp 数组:
注意点:字符对比时一般要 -1 的,因为如果 dp 数组填充了空字符串的位置,构造的空间是 len+1,所以遍历时 -1 才是我们当前两个字符串的字符位置

let len = array.length;
let dp = new Array(arr.length).fill(0).map(item=> new Array(arr.length).fill(xxxx)); // data base看情况定

for (let i = 1; i < len ; i++) {
    for (let j = 0; j < i; j++) {
        if (arr[i] === arr[j]) 
            dp[i][j] = dp[i][j] + ... // 具体看情况
        else
            dp[i][j] = 最值(...)
    }
}

一般有两个状态就是这种,这种思路运用相对更多一些,尤其是涉及两个字符串/数组的子序列

2.1 涉及两个字符串/数组时(比如最长公共子序列),dp 定义
在子数组 arr1[0…i] 和子数组 arr2[0…j] 中,我们要求的子序列(最长公共子序列)长度为 dp[i][j]

2.2 只涉及一个字符串/数组时(比如最长回文子序列),dp 定义
在子数组 array[i…j] 中,我们要求的子序列(最长回文子序列)的长度为 dp[i][j]。

一般来说,处理两个字符串的动态规划问题,都是建立 DP table。
为什么呢,因为易于找出状态转移的关系,比如编辑距离的 DP table 如下图[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HtYzejyZ-1640937077864)(fuck-image/algo/dp-table.png)]

判断回文

暴力法,注意必须从0开始。我会犯的错是在第三个循环里使用当前的循环变量开始判断回文,这样是不对的。因为我们每次找的子串是单独的,所以必须从0开始

 function find(str){
        for(let a=0;a<str.length;a++){
            if(str[a] !== str[str.length-1-a]){
                return false
            }
        }
        return true
    }

双指针法:有两种base case,单个字符自己可以作为开始的子串,单个字符和它的下一个一起可以做子串。find 函数就判断他俩是否相等,然后 start 往左,end 往右从中心往两边散开

var countSubstrings = function(s) {
    let count = 0
    for(let i=0;i<s.length;i++){
        let a = find(i,i,s)
        let b = find(i,i+1,s)
        count+= a+b
    }
    return count
    function find(start,end,str){
        let count = 0
        while(str[start] === str[end] && start>=0 && end<str.length){
            count++
            start --
            end ++
        }
        return count
    }
};

链表相关

反转 [start,end) 之间的链表

function reverse(start,end){
        let cur = start, pre = end
        while(cur!==end){
            let next = cur.next
            cur.next = pre
            pre = cur
            cur = next
        }
        return pre
    }

1、遍历有向无权图(有权就数组多push存一个权值),并拓扑排序
var findOrder = function(num, arr) {
    let map = new Array(num).fill(0).map(item=>new Array()) // 构建邻接表
    // 把对应关系放入邻接表,对应关系的顺序决定了拓扑排序是否需要反转
    for(let item of arr){
        let zero = item[0]
        let one = item[1]
        map[zero].push(one)
    }
    
    let hasCycle = false // 判断有无环
    let visited = new Array(num).fill(false) // 存储走过的点
    let path = new Array(num).fill(false) // 存储当前路径数组
    let res = [] // 存储拓扑排序结果
    // 深度遍历
    function dfs(i){
        if(path[i]){
            return hasCycle = true
        }
        if(visited[i]){
            return 
        }
        path[i] = true
        visited[i] = true
        
        for(let item of map[i]){
            dfs(item)
        }

        path[i] = false // 类似回溯,复原当前路径
        res.push(i) // 拓扑排序只需要在这里后序遍历这里记录经过的点
    }
    // 所有的点都可以作为起始点
    for(let i=0;i<num;i++){
        dfs(i)
    }
    if(hasCycle){
        return []
    }
    return res
};
2、二分图

定义:一个图中任何一条边两端的节点颜色可被涂成不一样
在这里插入图片描述

var isBipartite = function(graph) {
    let vis = new Array(graph.length).fill(false)
    let color = new Array(graph.length).fill(false)
    let ok = true
    // 每一个节点都可以作为开始节点
    for(let i=0;i<graph.length;i++){
        if(!vis[i]){
            dfs(i)
        }
    }
    return ok

    function dfs(i){
        if(!ok) return 
        vis[i] = true // vis 在下面不需要恢复
        for(let item of graph[i]){
            // 如果相邻节点没有被访问过,就把它的颜色与当前节点取反,再递归相邻节点
            if(!vis[item]){
                color[item] = !color[i]
                dfs(item)
            }else {
            // 相邻节点访问过了,就判断颜色是否相等,相等就不可能是二分图
                if(color[item] === color[i]){
                    return ok = false
                }
            }
        }
    }
};
3、构造按字典序排序的邻接表套路
    const map = {};
    for (const ticket of tickets) { // 建表
        const [from, to] = ticket;
        if (!map[from]) {
            map[from] = [];
        }
        map[from].push(to);
    }
    for (const city in map) {
        map[city].sort();
    }
4、标准的欧拉通路(一笔画)

在这里插入图片描述

const res = [];
     // 构造按字典序排序的邻接表套路,上面第三点
    const map = {};
    for (const ticket of tickets) { // 建表
        const [from, to] = ticket;
        if (!map[from]) {
            map[from] = [];
        }
        map[from].push(to);
    }
    for (const city in map) {
        map[city].sort();
    }
    // 套路结束
  const dfs = (node) => { // 当前城市
    const nextNodes = map[node]; // 当前城市的邻接城市
    while (nextNodes && nextNodes.length) { // 遍历,一次迭代设置一个递归分支
      const next = nextNodes.shift(); // 获取并移除第一项,字母小的城市
      dfs(next);                      // 向下递归
    }                 
    // 当前城市没有下一站,就把他加到res里,递归开始向上返回,选过的城市一个个推入res 
    res.unshift(node); 
  };

  dfs('JFK'); // 起点城市
  return res;

LRU(最近最少使用) 缓存淘汰算法

本质是利用哈希和双向链表实现,借助链表的有序性使得链表元素维持插入顺序,同时借助哈希映射的快速访问能力使得我们可以在 O(1) 时间访问链表的任意元素。
Map 会把老的数据放前面,Map本质是哈希表,表中存储的是指向数据的指针,如果发生哈希碰撞,就会使用一条双向链表存储两条数据,最遭的情况是都碰撞退化成一条链表,查询插入删除就退化到O(n)

var LRUCache = function(capacity) {
    this.capacity = capacity
    this.map = new Map()
};
LRUCache.prototype.get = function(key) {
    if(this.map.has(key)){ // 拿值的时候,先保存这个值,再删除并重新添加这个值
        const temp = this.map.get(key)
        this.map.delete(key)
        this.map.set(key,temp)
        return temp
    }else {
        return -1
    }
};
LRUCache.prototype.put = function(key, value) {
    if(this.map.has(key)) this.map.delete(key) // 已经有了就先删掉再添加
    this.map.set(key,value)
    if(this.map.size > this.capacity){ // 超过容量了就用迭代器把最老的删掉
        this.map.delete(this.map.keys().next().value)
    }
};

自己构造双向链表+对象
注意点:1、链表需要有删除尾部、添加到头部、删除某一个节点、更新使用的节点到头部的方法
2、LRU需要查询元素get、添加元素put(添加到哈希+链表)、删除(哈希+链表都删除)
3、每次都先去哈希找节点,在把节点传给链表的函数处理就行,很简单
4、对象中如果没有,返回的是 undefined 而不是 null

class ListNode {
    constructor(key,value){
        this.key = key
        this.value = value
        this.next = null
        this.prev = null
    }
}

class LRUCache{
    constructor(capacity){
        this.capacity = capacity
        this.hashTable = {}
        this.count = 0
        this.dummyHead = new ListNode()
        this.dummyTail = new ListNode()
        this.dummyHead.next = this.dummyTail
        this.dummyTail.prev = this.dummyHead
    }
    get(key){
        let node = this.hashTable[key]
        if(!node) return -1
        this.moveToHead(node)
        return node.value
    }
    put(key,value){
        let node = this.hashTable[key]
        if(!node){
            let newNode = new ListNode(key,value)
            this.hashTable[key] = newNode
            this.addToHead(newNode)
            this.count++
            if(this.count > this.capacity){
                this.delete()
            }
        }else {
            node.value = value
            this.moveToHead(node)
        }
    }
    delete(){
        delete this.hashTable[this.dummyTail.prev.key] // 删掉哈希表
        this.removeFromList(this.dummyTail.prev) // 删掉链表
        this.count-- // 减少数量
    }
    // 链表的添加
    addToHead(node){
        node.prev = this.dummyHead
        node.next = this.dummyHead.next
        this.dummyHead.next.prev = node
        this.dummyHead.next = node
    }

    // 触发get之后链表移动触发的节点
    moveToHead(node){
        this.removeFromList(node)
        this.addToHead(node)
    }
    // 删除链表的某一个节点
    removeFromList(node){
        node.prev.next = node.next
        node.next.prev = node.prev
    }
}

LFU(最不经常使用) 缓存淘汰算法

原理:2个哈希表+双向链表。节点哈希表中保存节点,方便查找。链表哈希表存储每个频率对应的双向链表,链表中存放出现这个频率的节点,这样方便添加和删除。还需要定义一个最少频率,

// 定义节点,多了频率
class Node{
    constructor(key,value){
        this.key = key
        this.value = value
        this.prev = null
        this.next = null
        this.freq = 1
    }
}
// 双向链表,添加节点和删除节点和 LRU 是一样的,对比 LRU 没有移到头部这个操作,而是移动节点到另一条链表
class doublyLinkedList {
    constructor(){
        this.head = new Node()
        this.tail = new Node()
        this.head.next = this.tail
        this.tail.prev = this.head
    }
    removeNode(node){
        node.prev.next = node.next
        node.next.prev = node.prev
    }
    addNode(node){
        node.next = this.head.next
        node.prev = this.head
        this.head.next.prev = node
        this.head.next = node
    }
}
// LFU 类:查找、添加、增加某个节点的频率并移动到另一条链表
class LFUCache {
    constructor(capacity){
        this.capacity = capacity
        this.size = 0
        this.minFreq = 0 // 最小使用频率
        this.cacheMap = new Map() // 找节点用
        this.freqMap = new Map() // 记录频率
    }
    get(key){
        if(!this.cacheMap.has(key)) return -1
        const node = this.cacheMap.get(key)
        this.addFreq(node)
        return node.value
    }
    put(key,value){
        if(this.capacity === 0) return  // 不能少
        const node = this.cacheMap.get(key)
        if(node){
            node.value = value
            this.addFreq(node)
        }else {
            // 容量已经用完,删除最小频率链表中最后一个节点
            if(this.capacity === this.size){
                const minFreqLinkedList = this.freqMap.get(this.minFreq)
                this.cacheMap.delete(minFreqLinkedList.tail.prev.key) // 尾部的节点是旧的
                minFreqLinkedList.removeNode(minFreqLinkedList.tail.prev)
                this.size --
            }
            const newNode = new Node(key,value)
            this.cacheMap.set(key,newNode)
            let linkedList = this.freqMap.get(1)
            if(!linkedList){
                linkedList = new doublyLinkedList()
                this.freqMap.set(1,linkedList)
            }
            linkedList.addNode(newNode)
            this.size++
            this.minFreq = 1
        }
    }
    addFreq(node){
        let freq = node.freq
        let linkedList = this.freqMap.get(freq)
        linkedList.removeNode(node)
        // 当前节点频率在最小的链表上并且删除后链表空了,最小频率应增加
        if(freq === this.minFreq && linkedList.head.next === linkedList.tail){
            this.minFreq = freq+1
        }
        node.freq++
        linkedList = this.freqMap.get(node.freq)
        if(!linkedList){
            linkedList = new doublyLinkedList()
            this.freqMap.set(node.freq,linkedList)
        }
        linkedList.addNode(node)
    }
}

前缀树

原理:每一个节点代表一个对象。对象中的值也是很多个对象,表示下一个可以到达的字符有哪些
注意点:返回值时记得二次取反,以免属性为 undefined 或者节点为 null

    class Trie {
        constructor(){
            this.children = {}
        }
        insert(word){
            let nodes = this.children
            // 循环给每个字符创建一个字符对象
            for(const char of word){
                if(!nodes[char]) nodes[char] = {}
                nodes = nodes[char]
            }
            nodes.isEnd = true // 最后一个字符对象有一个 isEnd 属性
        }
        // 搜索某个前缀
        searchPrefix(prefix){
            let nodes = this.children
            for(let char of prefix){
                // 若没有当前字母的属性,表明树上没这个前缀,返回false
                if(!nodes[char]) return false
                nodes = nodes[char]
            }
            return nodes
        }
        // 当需要匹配通配符时,dfs搜索某个前缀,递归搜索每一个存在的子树。
        searchPrefixAll(word){
            function dfs(index, node){
                if (index === word.length) {
                    return node.isEnd;
                }
                const ch = word[index];
                if (ch !== '.') {
                    const child = node[ch]
                    if (child && dfs(index + 1, child)) {
                        return true;
                    }
                } else {
                    for (const key in node) {
                        if (key && dfs(index + 1, node[key])) {
                            return true;
                        }
                    }
                }
                return false;
            }
            return dfs(0, this.children);
        }
        // 搜索某个单词
        search(word){
            const nodes = this.searchPrefix(word)
             // 树上有这个单词的条件:存在这个单词的前缀,且标记了end结束
            return !!nodes && !!nodes.isEnd
        }
        // 搜索某个前缀是否存在于树
        startsWith(word){
            return !!this.searchPrefix(word)
        }
        // 获取拥有某个前缀的单词所有权值,前提是插入时isEnd改为统计val值
        getAllSum(node){
            let base = 97,sum = 0
            for(let i=0;i<26;i++){
                if(node[String.fromCharCode(base+i)]){
                    sum+=this.getAllSum(node[String.fromCharCode(base+i)])
                }
            }
            return sum+=node.val || 0
        }
    }


区间和问题

区间和问题通常分为以下几种:

  1. 数组不变,求区间和:「前缀和」、「树状数组」、「线段树」
  2. 多次修改某个数(单点),求区间和:「树状数组」、「线段树」
  3. 多次修改某个区间,输出最终结果:「差分」
  4. 多次修改某个区间,求区间和:「线段树」、「树状数组」(看修改区间范围大小)
  5. 多次将某个区间变成同一个数,求区间和:「线段树」、「树状数组」(看修改区间范围大小)
    6

差分数组

第一步: 求出 diff 差分数组

总结:对于改变区间 [i, j] 的值,只需要进行如下操作 diff[i] += val; diff[j + 1] -= val
差分数组的前缀和就是原数组 这一性质

  const n = s.length;
  const diff = new Array(n).fill(0);
  
  for (let i = 0; i < arr.length; ++i) {
    const start = arr[i][0];
    const end = arr[i][1];
    const diffVal = 1 // 假设每次变化值是1
    diff[start] += diffVal ; // 开始
    if (end + 1 < n) {
      diff[end + 1] -= diffVal ; // 注意这里是减号!!!!!!
    }
  }

第二步: 根据 diff 差分数组,还原出原数组中每项值总的变化值

  let preSum = [diff[0]];
  for (let i = 1; i < n; ++i) {
    preSum[i] = preSum[i - 1] + diff[i];
  }

第三步:在原数组中应用 preSum 的值即可,这样只用了2次遍历即可统计变化

树状数组

因为线段树很长很费劲,所以总结优先级:

  1. 简单求区间和,用「前缀和」,适用于更新操作不多的情况
  2. 多次将某个区间变成同一个数,用「线段树」(第四类不得不写的时候才写)
  3. 其他情况,用「树状数组」
    在这里插入图片描述
    a1也就是c1=[a1,a1] c3=[a3,a3],把一个大区间,分成了若干个小区间
    举例:
    在这里插入图片描述
    等式左侧的数 x 看做是区间 [1, x],等式右边看做从 x 开始每个区间的长度
    [1, 11] = [11, 11] + [10, 9] + [8, 1]
    [11, 11] 、[10, 9]、[8, 1] 长度分别是 1、2、8
    F[1,11] = V[11] + V[10] + V[8]
    看黑色图可以知道二进制最右边一个 1 的幂次值,就是数组长度,所以需要 lowbit 函数去找区间长度。如果 len = i & -i ,那么 V[i] = F[i,i-1,i-2, … i-len+1]
    注意:因为数组的下标是从 0 开始的,上边的区间范围是从 1 开始的(2的幂次最少为1),所以我们在原数组开头补一个 0 ,这样区间就是从 1 开始了
class Bit {
    constructor(arr) {
        this.bit= new Array(arr.length + 1).fill(0);
        for (let i = 0; i < arr.length; i++) {
            this.update(i + 1, arr[i]);
        }
    }
    // 找出二进制最右边一个1所在位置代表的大小,1010 返回 4 ,1011返回1。也就是i这个区间的长度
    lowbit(x) {
        return x & -x;
    }
    // 序号从小区间往大区间更新区间和(如下解释),注意这里的val应该是更新前后的差值
    update(index, value) {
        for (let i = index; i < this.bit.length; i += this.lowbit(i)) {
            this.bit[i] += value;
        }
    }
    // 序号从大到小求区间和
    sum(index) {
        let ans = 0;
        // 例如11,先求出 11&-11的子区间和长度1,再11-1求出10&-10的长度2的区间和,再10-2求出8&-8的长度8
        // 把这些区间的和相加就是index===11时候的区间和了
        for (let i = index; i > 0; i -= this.lowbit(i)) {
            ans += this.bit[i];
        }
        return ans;
    }
}

更新值需要看图,当我们改变序号3的值时,由于序号4~1的大区间使用了序号3的值,所以需更新,8 ~1的大区间又使用的区间4 ~1值,所以也需要更新,而通过换算二进制可以看到每次都是加上当前数最右边的 1 表示的幂次值(也就是lowbit)可以得到上层区间,所以函数循环的 i 就可以知道如何增加了
在这里插入图片描述

线段树

动态开点:此模板表示为 区间和加减 更新操作
「区间和」:更新节点值的时候『需要✖️左右孩子区间叶子节点的数量 (注意是叶子节点的数量)』
「区间最值」:更新节点值的时候『不需要✖️左右孩子区间叶子节点的数量 (注意是叶子节点的数量)』,并且 pushup 是求左右节点的最大值
对区间进行覆盖:下推懒惰标记的时候『不需要累加』
对区间进行加减:下推懒惰标记的时候『需要累加』

区间和查询:就是现在的模板中的,+= 符号
区间最大值查询,求左右两节点的最大值返回即可

const N = 1e9
class SegNode {
  constructor() {
    this.left = this.right = null // 左右孩子节点
    this.val = this.add = 0 // 懒惰标记 add,当前节点值val
  }
}

class SegmentTree {
  constructor() {
    this.root = new SegNode()
  }
  // 更新
  update(node, start, end, l, r, val) {
    // 找到满足要求的区间
    if (l <= start && end <= r) {
      // 区间节点加上更新值
      node.val += val * (end - start + 1) // 
      node.add += val
      return
    }
    const mid = start + end >> 1

    // 下推标记
    // mid - start + 1:表示左孩子区间叶子节点数量
    // end - mid:表示右孩子区间叶子节点数量
    this.pushDown(node,mid - start + 1, end - mid)
    // [start, mid] 和 [l, r] 可能有交集,遍历左孩子区间
    if (l <= mid) { this.update(node.left, start, mid, l, r, val) }
    // [mid + 1, end] 和 [l, r] 可能有交集,遍历右孩.子区间
    if (r > mid) { this.update(node.right, mid + 1, end, l, r, val) }
    // 向上更新
    this.pushUp(node)
  }
  // 查询
  query(node, start, end, l, r) {
    if (l <= start && end <= r) { return node.val }
    let ans = 0, mid = start + end >> 1
    this.pushDown(node, mid - start + 1, end - mid)
    if (l <= mid) { ans += this.query(node.left, start, mid, l, r) }
    if (r > mid) { ans += this.query(node.right, mid + 1, end, l, r) }
    return ans
  }
  pushUp(node) {
    node.val = node.left.val + node.right.val;
  }
  pushDown(node, leftNum, rightNum) {
    // 动态开点
    if (node.left === null) {node.left = new SegNode()}
    if (node.right === null) {node.right = new SegNode()}
    if (node.add === 0) {return}
    // 注意:当前节点加上 标记值✖该子树所有叶子节点的数量
    node.left.val += node.add * leftNum
    node.right.val += node.add * rightNum
    // 对区间进行「加减」的更新操作,下推懒惰标记时需要累加起来,不能直接覆盖
    node.left.add += node.add
    node.right.add += node.add
    // 取消当前节点标记,已经用过了
    node.add = 0
  }
}

位运算

去除最后一个1

x & (x−1)

判断是不是2的整数次幂

x & (x−1) === 0

lowbit 算法

主要用于树状数组中,本质是可以快速求出二进制中最后一位1所代表的值是多少

x & -x

从这个也可以计算有多少个1,每次找到最后一个1的值,然后减掉就变成0了

let count = 0
while(x){
	x-=x&-x;
	count++;
}

常见技巧

1) 可以通过循环32位右移,来让每一位进行一些运算,比如下面判断哪一位是1,数组保留离右边最远的1的位置

  for(let j=0;j<32;j++){
      if((nums[i] >> j) & 1 === 1 ){
            dp[j] = i
        }
    }

背包问题

在这里插入图片描述

分类解题模板

背包问题大体的解题模板是两层循环,分别遍历物品nums和背包容量target,然后写转移方程

根据背包的分类我们确定物品和容量遍历的先后顺序
根据问题的分类我们确定状态转移方程的写法

首先是背包分类的模板:
1、0/1背包:外循环nums,内循环target,target倒序且target>=nums[i];
2、完全背包:外循环nums,内循环target,target正序且target>=nums[i];
3、组合背包:外循环target,内循环nums,target正序且target>=nums[i];
4、分组背包:这个比较特殊,需要三重循环:外循环背包bags,内部两层循环根据题目的要求转化为1,2,3三种背包类型的模板

然后是问题分类的模板:.
69
1、最值问题: dp[i] = max/min(dp[i], dp[i-nums]+1)或dp[i] = max/min(dp[i], dp[i-num]+num);
2、存在问题(bool):dp[i]=dp[i]||dp[i-num];
3、组合问题:dp[i]+=dp[i-num];

完全背包中:
如果求组合数就是外层for遍历物品,内层for遍历背包。都是正序
如果求排列数就是外层for遍历背包,内层for遍历物品。都是正序
求装满背包有几种方法,递推公式一般都是dp[i] += dp[i - nums[k]]; i是背包,k是物品

最长xxxxx

在这里插入图片描述
42是41的简单版,只需要一个循环即可
43是44的简单版,因为数组连续,所以两个数组不相等的时候不需要管,如果是序列就要管,因为需要从dp[i-1][j],dp[i][j-1]转移过来配合后面的值

单调栈

栈底到栈顶:
递增栈找第一个比当前小的
递减栈找第一个比当前大的
为什么要递增栈呢,因为这样可以保证栈顶元素是栈中最大的,后面的元素如果比栈顶大就一定栈顶是第一个小的,比栈顶小的话,就弹栈同时进行比较,这样弹出去的肯定不会是后面的值的第一个最小值

for(let i=0;i<heights.length;i++){
		// 为0直接入栈
        if(stackLeft.length===0){
            stackLeft.push(i)
            continue
        }
        //核心,找左右两边第一个比当前小的,那么[left+1,right-1]的元素都是大于等于当前高度的,都是可使用的。递增栈(栈底到栈顶)
        // 比栈顶大,放进去
        if(heights[i] > heights[stackLeft[stackLeft.length-1]]){
            left[i] = stackLeft[stackLeft.length-1]
            stackLeft.push(i)
        }else {
         	// 弹栈的时候循环
            while(stackLeft.length && heights[i] <= heights[stackLeft[stackLeft.length-1]]){
            	if(stackLeft.length){
                	left[i] = stackLeft[stackLeft.length-1]
           		 }
                stackLeft.pop()
            }
			// 弹完了当前元素入栈
            stackLeft.push(i)
        }
    }

最大公约数gcd

function gcd(a, b) {
  if (!b) {
    return a;
  }

  return gcd(b, a % b);
}

组合排列

used[i - 1] == true,说明同⼀树⽀nums[i - 1]使⽤过
used[i - 1] == false,说明同⼀树层nums[i - 1]使⽤过

 // 例子:如果同⼀树层nums[i - 1]使⽤过则直接跳过
 if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
     continue;
 }

代码输出

async function async1() {
    console.log(1);
    await async2(); // async2立即执行,1之后立刻打印3
    console.log(2);
}
async function async2() {
    console.log(3);
}
async1();
setTimeout(() => console.log(4), 0);
new Promise(resolve => {
    resolve();
    console.log(5);
}).then(() => {
    console.log(6);
    Promise.resolve().then(()=>{
    console.log(7);
    });
});
console.log(8);
// 1 3 5 8 2 6 7 4 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值