前端算法入门之路(十三)(单调队列及经典问题)

454 阅读3分钟

单调队列

单调队列是一个递增或递减的队列,可以用来维护滑动窗口区间的最值,即RMQ问题
入队操作:队尾入队,会把前面破坏单调性的元素删除(维护单调性)
出队操作:如果队首元素超出区间范围,就将队首元素出队
元素性质:队首元素永远是当前区间的最值;元素入队的过程中该元素是当前队列的最值

LeetCode肝题

    1. 滑动窗口最大值
// 利用单调队列的思想,按照题意,用单调递减队列
// 每进一次循环判断当前值是否大于队尾值,如果成立则队列尾部弹出
// 元素进队,判断下标差是否等于窗口大小,如果相等则说明队首元素超出窗口区间范围,队首元素出队
// 最后判断下标小于窗口大小不往结果数组放入答案
var maxSlidingWindow = function(nums, k) {
    let q = [], ans = []
    for(i = 0; i < nums.length; i++) {
        while(q.length && nums[q[q.length - 1]] < nums[i]) q.pop()
        q.push(i)
        if (i - q[0] == k) q.shift()
        if (i + 1 < k) continue
        ans.push(nums[q[0]])
    }
    return ans
};
  1. 剑指 Offer 59 - II. 队列的最大值
// 维护一个单调递减队列,最大值返回单调队列的队首元素
// 存入值的时候判断,有较小值则弹出
// 队首元素出队时判断如果出队的是最大值则单调队列队首元素一起出队
var MaxQueue = function() {
    this.q = []
    this.mq = []
};

MaxQueue.prototype.max_value = function() {
    if (this.mq.length == 0) return -1
    return this.mq[0]
};

MaxQueue.prototype.push_back = function(value) {
    this.q.push(value)
    while(this.mq.length && this.mq[this.mq.length - 1] < value) this.mq.pop()
    this.mq.push(value)
};

MaxQueue.prototype.pop_front = function() {
    if (this.q.length == 0) return -1
    if (this.q[0] == this.mq[0]) this.mq.shift()
    return this.q.shift()
};
    1. 和至少为 K 的最短子数组
// 求区间和需要前缀和数组sum,设置一个单调递减队列q用于寻找较小值,记录上一次符合条件的下标,结果值ans
// 遍历前缀和,如果队首元素都符合题意,将队首元素出队,并记录其下标
// 如果当前下标i-上一次符合条件的下标pos<结果ans,则给ans赋值为i-pos
// 如果队尾元素大于当前值,队尾元素出队
// 最后当前下标进队
var shortestSubarray = function(nums, k) {
    let sum = new Array(nums.length + 1), q = [0], pos = -1, ans = -1
    sum[0] = 0
    for(let i = 0; i < nums.length; i++) {
        sum[i + 1] = sum[i] + nums[i]
    }
    for (let i = 1; i < sum.length; i++) {
        while(q.length && sum[i] - sum[q[0]] >= k) pos = q.shift()
        if(pos != -1 && (ans == -1 || i - pos < ans)) ans = i - pos
        while(q.length && sum[i] < sum[q[q.length - 1]]) q.pop()
        q.push(i)
    }
    return ans
};
    1. 绝对差不超过限制的最长连续子数组
// 维护两个单调队列,一个递增一个递减
// 当最大值减去最小值大于limit时,窗口左边坐标left++
// 否则ans赋值为窗口大小
var longestSubarray = function(nums, limit) {
    let maxQ = [], minQ = [], left = 0, ans = 0
    for(let i = 0; i < nums.length; i++) {
        while(maxQ.length && nums[i] < maxQ[maxQ.length - 1]) maxQ.pop()
        maxQ.push(nums[i])
        while(minQ.length && nums[i] > minQ[minQ.length - 1]) minQ.pop()
        minQ.push(nums[i])
        if (Math.abs(maxQ[0] - minQ[0]) > limit) {
            if (maxQ[0] == nums[left]) maxQ.shift()
            if (minQ[0] == nums[left]) minQ.shift()
            left++
        } else {
            ans = i - left + 1
        }
    }
    return ans
};
    1. 找树左下角的值
// 给树打上层级标记,定义一个数组,在遍历每一层的时候把第一个节点加入进去,最后返回数组最后一位
var findBottomLeftValue = function(root) {
    if (!root.left && !root.right) return root.val
    root.l = 0
    let level = [], q = [root], l = 0
    while(q.length) {
        const cur = q.shift()
        if (cur.l > l) {
            level.push(cur)
            l++
        }
        if (cur.left) {
            cur.left.l = cur.l + 1
            q.push(cur.left)
        }
        if (cur.right) {
            cur.right.l = cur.l + 1
            q.push(cur.right)
        }
    }
    return level[level.length - 1].val
};
// 深搜方法
var findBottomLeftValue = function(root) {
    let val = 0, max_l = -1
    let dfs = (root, l) => {
        if (!root) return
        if (l > max_l) {
            max_l = l
            val = root.val
        }
        dfs(root.left, l + 1)
        dfs(root.right, l + 1)
    }
    dfs(root, 0)
    return val
};
    1. 分发糖果
// 从左右两个方向分别计算应该分配多少糖果,然后取每一位的最大值之和
var candy = function(ratings) {
    let l = Array(ratings.length), r = Array(ratings.length), ans = 0
    for(let i = 0, j = 1; i < ratings.length; i++) {
        if (i > 0 && ratings[i] > ratings[i - 1]) j += 1
        else j = 1
        l[i] = j
    }
    for(let i = ratings.length - 1, j = 1; i >= 0; i--) {
        if (i < ratings.length - 1 && ratings[i] > ratings[i + 1]) j += 1
        else j = 1
        r[i] = j
    }
    for(let i = 0; i < l.length; i++) ans += Math.max(l[i], r[i])
    return ans
};
    1. 水壶问题
// 使用广搜,状态扩展共6种,结束条件为x或者y或者x+y等于目标值,设置vis判重
// 1、给x壶倒满
// 2、给y壶倒满
// 3、给x壶清空
// 4、给y壶清空
// 5、把x壶里的水倒入y壶,直到x空或y满
// 6、把y壶里的水倒入x壶,直到y空或x满
var canMeasureWater = function(X, Y, targetCapacity) {
    let q = [[0, 0]], vis = {}
    while(q.length) {
        const [x, y] = q.shift()
        if (x == targetCapacity || y == targetCapacity || x + y == targetCapacity) return true
        if (vis[`${x}#${y}`]) continue
        vis[`${x}#${y}`] = 1
        q.push([X, y])
        q.push([x, Y])
        q.push([0, y])
        q.push([x, 0])
        q.push([x - Math.min(x, Y - y), y + Math.min(x, Y - y)])
        q.push([x + Math.min(y, X - x), y - Math.min(y, X - x)])
    }
    return false
};
    1. 袋子里最少数目的球
// 可以将问题抽象为:每个袋子装多少球能够正好达到操作次数
// 问题区间为每袋最少为1,最大为给定数组的最大值,所以使用二分查找,不断靠近操作次数
let getOpera = (n, nums) => {
    let ans = 0
    for(let i of nums) {
        ans += parseInt(i / n) + (i % n ? 1 : 0) - 1
    }
    return ans
}
let search = (l, r, n, nums) => {
    if (l == r) return l
    let mid = (l + r) >> 1
    if (getOpera(mid, nums) <= n) r = mid
    else l = mid + 1
    return search(l, r, n, nums)
}
var minimumSize = function(nums, maxOperations) {
    let l = 1, r = 1
    for(let item of nums) r = Math.max(item, r)
    return search(l, r, maxOperations, nums)
};
    1. 跳跃游戏 II
// 设置坐标pre代表跳跃坐标,pos代表能跳跃的最大坐标
// 在数组范围内每次跳跃只需要求得pre+1到pos区间的最大值即可最快到达终点
var jump = function(nums) {
    if (nums.length <= 1) return 0
    let pre = 0, pos = nums[0], cut = 1
    while(pos + 1 < nums.length) {
        let j = pre
        for(let i = pre + 1; i <= pos; i++) {
            if (i + nums[i] > j + nums[j]) j = i
        }
        pre = pos + 1, pos = j + nums[j]
        cut+=1
    }
    return cut
};
    1. 复原 IP 地址
// 递归当前处理的坐标i和‘.’的个数k,如果坐标越界停止当前递归
// 如果k==4,首先判断当前区间的数字是否以0开头,如果以0开头停止递归,否则组装当前区间数字,如果不大于255放入结果数组
// 从i开始遍历,去除0开头的情况和大于255的情况,然后在j+1位插入‘.’,递归查找下标为j+2,层级k+1的情况
// 如果查找到第四级不符合条件,需要回溯的过程中把j+1位置的‘.’去除
function changeStr(str,index,changeStr,t = 0){
    return str.substr(0, index) + changeStr + str.substring(index + t);
}
var search = function(i, k, s, ans) {
    if (i >= s.length) return
    if (k == 4) {
        let num = 0
        if (s.length - i > 1 && s[i] == 0) return
        for(let j = i; j < s.length; j++) {
            num = num * 10 + parseInt(s[j])
            if (num > 255) return
        }
        ans.push(s)
    }
    for(let j = i, num = 0; j < s.length; j++) {
        if (j - i >= 1 && s[i] == 0) return
        num = num * 10 + parseInt(s[j])
        if (num > 255) return
        s = changeStr(s, j + 1, '.', 0)
        search(j + 2, k + 1, s, ans)
        s = changeStr(s, j + 1, '', 1)
    }
}
var restoreIpAddresses = function(s) {
    let ans = []
    search(0, 1, s, ans)
    return ans
};
    1. 全排列
// 全排列就是要路径数等于给定数组的时候添加进结果数组,深搜的时候每次都要从0开始,做一下判重
var permute = function(nums) {
    let ans = [], buff = [], vis = {}
    let dfs = () => {
        if (buff.length == nums.length) {
            ans.push(JSON.parse(JSON.stringify(buff)))
            return
        }
        for(let i = 0; i < nums.length; i++) {
            if (vis[i]) continue
            vis[i] = 1
            buff.push(nums[i])
            dfs()
            vis[i] = 0
            buff.pop()
        }
    }
    dfs()
    return ans
};
    1. 字符串相乘
// 倒序遍历每一个字符串,i坐标和j坐标的乘积个位放入结果数组中的i+j+1位,进位放入i+j位,最后去除结果数组中的前置0
var multiply = function(num1, num2) {
    if (num1 == '0' || num2 == '0') return '0'
    let a = Array(num1.length), b = Array(num2.length), ans = new Array(a.length + b.length).fill(0)
    for(let i = num1.length - 1; i >= 0; i--) {
        for(let j = num2.length - 1; j >= 0; j--) {
            let sum = ans[i + j + 1] + num1[i] * num2[j]
            ans[i + j + 1] = sum % 10
            ans[i + j] += sum / 10|0
        }
    }
    while(ans[0] == 0) ans.shift()
    return ans.join('') + ''
};