Kiner算法刷题记(十四):数据结构中的“渣男”——单调栈(手撕算法篇)

系列文章导引

  • 系列文章导引

开源项目

本系列所有文章都将会收录到GitHub中统一收藏与管理,欢迎ISSUEStar

GitHub传送门:Kiner算法算题记

503. 下一个更大元素 II

解题思路

由于题目要求找到每个元素下一个更大的元素,明显就是求最近大于关系的问题,因此,我们可以使用单调递减栈来帮我们解决这个问题。不过,这道题还有一个特殊的点,就是需要循环搜索,此时,我们可以使用一个编程技巧,就是将目标元素组内的元素copy一份,让新的数组时原始数组的两倍,这样就可以很方便的处理循环搜索问题了。话不多说,来看一下如何编程实现吧。

代码演示

/*
 * @lc app=leetcode.cn id=503 lang=typescript
 *
 * [503] 下一个更大元素 II
 *
 * https://leetcode-cn.com/problems/next-greater-element-ii/description/
 *
 * algorithms
 * Medium (62.86%)
 * Likes:    454
 * Dislikes: 0
 * Total Accepted:    92.6K
 * Total Submissions: 147.1K
 * Testcase Example:  '[1,2,1]'
 *
 * 给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x
 * 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。
 * 
 * 示例 1:
 * 
 * 
 * 输入: [1,2,1]
 * 输出: [2,-1,2]
 * 解释: 第一个 1 的下一个更大的数是 2;
 * 数字 2 找不到下一个更大的数; 
 * 第二个 1 的下一个最大的数需要循环搜索,结果也是 2。
 * 
 * 
 * 注意: 输入数组的长度不会超过 10000。
 * 
 */

// @lc code=start
function nextGreaterElements(nums: number[]): number[] {
    // 由于要求解的是每个元素最近的大于这个元素的数,明显是求解最近大于关系,所以我们可以使用
    // 单调递减栈来辅助我们完成这道题

    // 原始数组长度
    let len = nums.length;

    // 单调递减栈
    const stack: number[] = [];
    // 结果数组
    const res: number[] = new Array(nums.length);
    res.fill(-1);

    // 由于题目让我们循环查找,因此,我们可以使用一个编程技巧,就是将元素组扩大一倍
    nums = [...nums, ...nums];

    // 循环元素组,将原数组中的元素依次入栈
    nums.forEach((item, idx) => {
        // 当栈不为空,并且即将入栈的元素大于栈顶元素时,我们需要将所有小于新加入栈弹出
        // 并且将弹出元素的位置记录他的下一个大于他的树就是当前的item
        while(stack.length && item > nums[stack[stack.length-1]]) {
            res[stack.pop()] = item;
        }
        // 通过上面的步骤维护了我们单调递减栈的单调性,接下来,就直接将当前元素索引压入栈中
        // 由于我们上面将原数组扩大了一倍,因此,放入索引时需与原始数组长度求模
        // 为什么放入索引而不是值呢?因为我们通过索引能够轻松获取值,而通过值,很难反推索引
        stack.push(idx%len);
    });

    // 循环结束之后,res中的就是我们要的答案
    return res;
};
// @lc code=end


901. 股票价格跨度

解题思路

由于我们要找当前元素最近的小于等于当前值的最大连续日数,也就等价于向前找到最近一个大于当前值的日数的位置即可。那么这样就等价于我们在求解一个查找每个元素最近大于关系的问题了,我们可以使用单调递减栈辅助解决问题

代码演示

/*
 * @lc app=leetcode.cn id=901 lang=typescript
 *
 * [901] 股票价格跨度
 *
 * https://leetcode-cn.com/problems/online-stock-span/description/
 *
 * algorithms
 * Medium (53.36%)
 * Likes:    127
 * Dislikes: 0
 * Total Accepted:    16.4K
 * Total Submissions: 30.6K
 * Testcase Example:  '["StockSpanner","next","next","next","next","next","next","next"]\n' +
  '[[],[100],[80],[60],[70],[60],[75],[85]]'
 *
 * 编写一个 StockSpanner 类,它收集某些股票的每日报价,并返回该股票当日价格的跨度。
 * 
 * 今天股票价格的跨度被定义为股票价格小于或等于今天价格的最大连续日数(从今天开始往回数,包括今天)。
 * 
 * 例如,如果未来7天股票的价格是 [100, 80, 60, 70, 60, 75, 85],那么股票跨度将是 [1, 1, 1, 2, 1, 4,
 * 6]。
 * 
 * 
 * 
 * 示例:
 * 
 * 输入:["StockSpanner","next","next","next","next","next","next","next"],
 * [[],[100],[80],[60],[70],[60],[75],[85]]
 * 输出:[null,1,1,1,2,1,4,6]
 * 解释:
 * 首先,初始化 S = StockSpanner(),然后:
 * S.next(100) 被调用并返回 1,
 * S.next(80) 被调用并返回 1,
 * S.next(60) 被调用并返回 1,
 * S.next(70) 被调用并返回 2,
 * S.next(60) 被调用并返回 1,
 * S.next(75) 被调用并返回 4,
 * S.next(85) 被调用并返回 6。
 * 
 * 注意 (例如) S.next(75) 返回 4,因为截至今天的最后 4 个价格
 * (包括今天的价格 75) 小于或等于今天的价格。
 * 
 * 
 * 
 * 
 * 提示:
 * 
 * 
 * 调用 StockSpanner.next(int price) 时,将有 1 <= price <= 10^5。
 * 每个测试用例最多可以调用  10000 次 StockSpanner.next。
 * 在所有测试用例中,最多调用 150000 次 StockSpanner.next。
 * 此问题的总时间限制减少了 50%。
 * 
 * 
 */

// @lc code=start
/**
 * 由于我们要找当前元素最近的小于等于当前值的最大连续日数,也就等价于向前找到最近一个
 * 大于当前值的日数的位置即可。那么这样就等价于我们在求解一个查找每个元素最近大于关系
 * 的问题了,我们可以使用单调递减栈辅助解决问题
 */
// 为了方便记录价格与日数的关系,我们定义一个Data数据结构用于存储
type Data = {
    price: number,
    t: number
};
class StockSpanner {
    // 当前日数
    private t: number = 0;
    // 单调递减栈
    private stack: Data[] = [];
    constructor() {
        // 初始时,我们压入一个最大值入栈
        this.stack.push({
            price: Number.MAX_SAFE_INTEGER,
            t: this.t++
        });
    }

    next(price: number): number {
        // 为了维护栈的单调递减的性质,我们需要将栈中所有价格比当前价格小的元素出栈
        while(this.stack.length && price >= this.stack[this.stack.length-1].price){
            this.stack.pop();
        }
        // 通过上述操作,栈顶元素就是我们要找的第一个大于当前元素的元素了,直接日数相减
        // 求出跨度
        const res = this.t - this.stack[this.stack.length-1].t;
        // 跨度求出来后,别忘了将当前的价格压入到栈中,并让日数加1,为下一轮做准备
        this.stack.push({
            price,
            t: this.t++
        });
        return res;
    }
}

/**
 * Your StockSpanner object will be instantiated and called as such:
 * var obj = new StockSpanner()
 * var param_1 = obj.next(price)
 */
// @lc code=end


739. 每日温度

解题思路

这道题题目已经说得很明显了,向后找最近一个大于当前温度的温度,维护最近大于关系,依然使用单调递减栈辅助解决问题

代码演示

/*
 * @lc app=leetcode.cn id=739 lang=typescript
 *
 * [739] 每日温度
 *
 * https://leetcode-cn.com/problems/daily-temperatures/description/
 *
 * algorithms
 * Medium (67.37%)
 * Likes:    822
 * Dislikes: 0
 * Total Accepted:    184.4K
 * Total Submissions: 273K
 * Testcase Example:  '[73,74,75,71,69,72,76,73]'
 *
 * 请根据每日 气温 列表 temperatures ,请计算在每一天需要等几天才会有更高的温度。如果气温在这之后都不会升高,请在该位置用 0 来代替。
 * 
 * 示例 1:
 * 
 * 
 * 输入: temperatures = [73,74,75,71,69,72,76,73]
 * 输出: [1,1,4,2,1,1,0,0]
 * 
 * 
 * 示例 2:
 * 
 * 
 * 输入: temperatures = [30,40,50,60]
 * 输出: [1,1,1,0]
 * 
 * 
 * 示例 3:
 * 
 * 
 * 输入: temperatures = [30,60,90]
 * 输出: [1,1,0]
 * 
 * 
 * 
 * 提示:
 * 
 * 
 * 1 
 * 30 
 * 
 * 
 */

// @lc code=start
function dailyTemperatures(temperatures: number[]): number[] {
    // 这道题题目已经说得很明显了,向后找最近一个大于当前温度的温度,维护最近大于关系,依然
    // 使用单调递减栈辅助解决问题
    const stack: number[] = [];
    // 结果数组设置为与温度数组大小相同,并每个元素初始化为0
    const res: number[] = new Array(temperatures.length);
    res.fill(0);
    for(let i=0;i<temperatures.length;i++) {
        while(stack.length && temperatures[i] > temperatures[stack[stack.length-1]]) {
            const d = stack.pop();
            // 计算时间差,用当前时间减去上面的时间
            res[d] = i - d;
        }
        // 上面维护了单调性之后,将当前元素的索引压入栈中
        stack.push(i);
    }
    return res;
};
// @lc code=end


84. 柱状图中最大的矩形

解题思路

我们都知道,矩形的面积由他的宽和高共同决定,我们可以以每个元素作为矩形的高,然后往前或往后查找最近一个比当前的木板更短的木板(面积大小由短板决定,就算你比较高的木板再高,短板高度为1,实际的高度也只能是1),找到最近一块更短木板后,高度为短木板的高度,宽度为彼此索引差值再减1,这样就可以计算出来矩形面积,然后与上一次计算出来的矩形面积对比取最大值即可。为了方便处理边界问题,我们在数组的最前面和最后面都放一个虚拟木板(可以理解为之前做链表题目时的虚拟头结点和虚拟尾结点)。

代码演示

/*
 * @lc app=leetcode.cn id=84 lang=typescript
 *
 * [84] 柱状图中最大的矩形
 *
 * https://leetcode-cn.com/problems/largest-rectangle-in-histogram/description/
 *
 * algorithms
 * Hard (43.01%)
 * Likes:    1445
 * Dislikes: 0
 * Total Accepted:    163.6K
 * Total Submissions: 380K
 * Testcase Example:  '[2,1,5,6,2,3]'
 *
 * 给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
 * 
 * 求在该柱状图中,能够勾勒出来的矩形的最大面积。
 * 
 * 
 * 
 * 示例 1:
 * 
 * 
 * 
 * 
 * 输入:heights = [2,1,5,6,2,3]
 * 输出:10
 * 解释:最大的矩形为图中红色区域,面积为 10
 * 
 * 
 * 示例 2:
 * 
 * 
 * 
 * 
 * 输入: heights = [2,4]
 * 输出: 4
 * 
 * 
 * 
 * 提示:
 * 
 * 
 * 1 
 * 0 
 * 
 * 
 */

// @lc code=start
// 我们都知道,矩形的面积由他的宽和高共同决定,我们可以以每个元素作为矩形的高,然后往前或往后查找最近一个比当前的木板更短的木板(面积大小由短板决定,就算你比较高的木板再高,短板高度为1,实际的高度也只能是1),找到最近一块更短木板后,高度为短木板的高度,宽度为彼此索引差值再减1,这样就可以计算出来矩形面积,然后与上一次计算出来的矩形面积对比取最大值即可。为了方便处理边界问题,我们在数组的最前面和最后面都放一个虚拟木板(可以理解为之前做链表题目时的虚拟头结点和虚拟尾结点)。
function largestRectangleArea(heights: number[]): number {
    // 根据上面的分析,我们要维护最近小于关系,因此需要一个单调递增栈辅助解决问题
    const stack: number[] = [];
    // 由于既需要往前找,也需要往后找,因此,定义了两个用于存储最近小于关系的数组,用来分别存储左边比当前元素小的元素索引
    // 和右边比当前元素小的索引
    const l: number[] = new Array(heights.length);
    const r: number[] = new Array(heights.length);
    // 为了简化边界问题的处理,我们设置一下虚拟头和虚拟尾,这里有一个小技巧,就是直接将l和r数组的每个元素分别初始化为:
    // -1和height.length即可,这样,可以方便统一处理
    l.fill(-1);
    r.fill(heights.length);

    for(let i=0;i<heights.length;i++) {
        // 首先,需要维护栈的单调性,如果当前元素比栈顶元素小,则将栈顶元素一一弹出,并标记栈顶元素最近一个小于他们的元素就是当前元素
        while(stack.length && heights[i] < heights[stack[stack.length-1]]) {
            r[stack.pop()] = i;
        }
        // 上面维护栈的单调性后,如果栈不为空,那么栈顶元素就是最近一个比当前元素大的元素,换句话说,当前元素最近一个比他小的元素就是栈顶元素
        if(stack.length) l[i] = stack[stack.length-1];
        // 将当前元素压入栈中
        stack.push(i);
    }

    // 经过上面的处理,我们已经得到了每一个高度对应的最近比它矮的木板了,就可以计算出面积并从中选取最大值就是我们要的结果。
    let res = 0;
    for(let i=0;i<heights.length;i++) {
        res = Math.max(res, heights[i] * (r[i] - l[i] -1));
    }
    return res;
};
// @lc code=end


1856. 子数组最小乘积的最大值

解题思路

# 假设数组为:[1,2,3,2]
# 我们可以假设我们元素中的每一位就是子数组的最小值,然后让这个最小值往前找到第一个小于这个值的数,往后找到第一个小于这个值的数,这样找到的两个数之间的数组,就是以当前元素为最小值的子数组了
如: 当前元素为1

向后找不到比自己小的元素,因此,1是子数组[1,2,3,2]的最小值,最小乘积为:1 * (1+2+3+2) = 8

如: 当前元素为2
向前找找到1比2小,因此,我们的子数组左边界移动到了1的右边,即左边界为2
向后找不到比2小的数,因此,2是子数组[2,3,2]的最小值,最小乘积为:2 * (2+3+2) = 14

如: 当前元素为3
向前找找到2比3小,因此,子数组左边界移动到2的右边,即左边界为3
向后找找到2比3小,因此,子数组右边界移动到2的左边,即右边界为3
因此,以3为最小值的子数组为:[3],最小乘积为:3 * 3 = 6

如: 当前元素为2(数组最后一个元素)
向前找找到小1位小于2的元素,因此,子数组左边界移动至1的右边,即左边界为2
向后找已经没有元素了,因此,以2位最小值的子数组是:[2,3,2],最小乘积为:2*(2+3+2)=14

最终,我们求出几个最小乘积的最大值为14

从上面的解题思路中,我们不难看出,我们需要找出每一个元素前边和后边第一个小于他的元素,以此来确定以当前元素为最小值的子数组边界。要求最近小于关系,我们可以使用单调递增栈来辅助。此外,为了方便求解区间和,这边还会使用前缀和的编程技巧。

代码演示

/*
 * @lc app=leetcode.cn id=1856 lang=typescript
 *
 * [1856] 子数组最小乘积的最大值
 *
 * https://leetcode-cn.com/problems/maximum-subarray-min-product/description/
 *
 * algorithms
 * Medium (35.99%)
 * Likes:    44
 * Dislikes: 0
 * Total Accepted:    4K
 * Total Submissions: 11.1K
 * Testcase Example:  '[1,2,3,2]'
 *
 * 一个数组的 最小乘积 定义为这个数组中 最小值 乘以 数组的 和 。
 * 
 * 
 * 比方说,数组 [3,2,5] (最小值是 2)的最小乘积为 2 * (3+2+5) = 2 * 10 = 20 。
 * 
 * 
 * 给你一个正整数数组 nums ,请你返回 nums 任意 非空子数组 的最小乘积 的 最大值 。由于答案可能很大,请你返回答案对  10^9 + 7
 * 取余 的结果。
 * 
 * 请注意,最小乘积的最大值考虑的是取余操作 之前 的结果。题目保证最小乘积的最大值在 不取余 的情况下可以用 64 位有符号整数 保存。
 * 
 * 子数组 定义为一个数组的 连续 部分。
 * 
 * 
 * 
 * 示例 1:
 * 
 * 
 * 输入:nums = [1,2,3,2]
 * 输出:14
 * 解释:最小乘积的最大值由子数组 [2,3,2] (最小值是 2)得到。
 * 2 * (2+3+2) = 2 * 7 = 14 。
 * 
 * 
 * 示例 2:
 * 
 * 
 * 输入:nums = [2,3,3,1,2]
 * 输出:18
 * 解释:最小乘积的最大值由子数组 [3,3] (最小值是 3)得到。
 * 3 * (3+3) = 3 * 6 = 18 。
 * 
 * 
 * 示例 3:
 * 
 * 
 * 输入:nums = [3,1,5,6,4,2]
 * 输出:60
 * 解释:最小乘积的最大值由子数组 [5,6,4] (最小值是 4)得到。
 * 4 * (5+6+4) = 4 * 15 = 60 。
 * 
 * 
 * 
 * 
 * 提示:
 * 
 * 
 * 1 
 * 1 
 * 
 * 
 */

// @lc code=start
function maxSumMinProduct(nums: number[]): number {
    // 单调递增栈
    const stack: number[] = [];
    const len = nums.length;
    // 存储往前查找每个元素第一个比自己小的数的索引,即子数组的左边界
    const l: number[] = new Array(len);
    // 存储往后查找每个元素第一个比自己小的数的索引,即子数组的右边界
    const r: number[] = new Array(len);
    // 用于大数取模
    const mod = BigInt(1e9+7);
    // 初始l初始化为-1,r初始化为len
    l.fill(-1);
    r.fill(len);
    // 前缀和数组
    const sums: number[] = new Array(len+1);
    // 前缀和数组第一项固定为0
    sums[0] = 0;
    for(let i=0;i<len;i++) {
        // 求解前缀和
        sums[i+1] = sums[i] + nums[i];
        // 如果发现当前元素小于栈顶元素,则违反单调性,将栈顶元素弹出,并将栈顶元素往后找最近一个小于他的元素标记为i
        while(stack.length && nums[i] <= nums[stack[stack.length-1]]) {
            r[stack.pop()] = i;
        }
        // 上面已经维护了栈的单调性了,如果栈中仍存在元素,那么当前元素往前找最近的一个小于当前元素的元素就是栈顶元素
        if(stack.length) l[i] = stack[stack.length-1];
        // 当前元素入栈
        stack.push(i);
    }

    // 注意,因为可能数字超出整型边界,必须使用BigInt,否则无法跑过所有用例
    let res = BigInt(0);
    // 上面已经找到了所有可能的子数组边界,接下来就要求取所有最小乘积的最大值
    // 注意:要用BigInt
    for(let i=0;i<len;i++) {
        let max = BigInt(nums[i]) * BigInt((sums[r[i]] - sums[l[i] + 1]));
        if(res<max) {
            res = max;
        }
    }
    return (res % mod) as unknown as number;
};
// @lc code=end


907. 子数组的最小值之和

解题思路

依据题意,我们最重要的找出min(b),那么,我们第一时间可能会想到使用RMQ(求解区间最大/最小值)的问题
在这里其实就可以转换为求取区间最小值的问题,而这个区间,其实就是我们的子数组。即:RMQ(x,y)=b
此时,如果我们把每一个元素作为区间的右边界,即把R(x,y)=b问题转换成固定末尾的RMQ问题。即:RMQ(x, i)=b
那么我们就可以以当前元素作为末尾,往前找到第一个小于当前元素的数k,那么,我们要找的子数组起始就在k与i之间。
而这个子数组的最小值,就是我们的末尾元素(因为我们往前找到第一个小于末尾元素时就不再往前找了,因此末尾元素永远是子数组的最小值)
最后,我们只需要把这些最小值累加起来就可以了。

# 假设原始数组为:[3,1,2,4]

以3为结尾的RMQ(x, 0)问题:子数组:[3],最小值:3 和值为:3
以1为结尾的RMQ(x, 1)问题:子数组:[1], [3,1],[1,2],[1,2,4],[3,1,2],[3,1,2,4],最小值:1,和值为1*6 = 6
以2为结尾的RMQ(x, 2)问题:子数组:[2],[2,4], 最小值为:2 和值为:2 * 2 = 4
以4为结尾的RMQ(x, 3)问题:子数组:[4],最小值为4
                               固定末尾
┏━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━┓
┗━━━━━━━┻━━━━━━━━┻━━━━━━━━┻━━━━━━━┛
        3        1        2       4
# 设我们要求的min(b)的总和为:sum(n),sum(0)为0
sum(n) = sum(n-1) + S(RMQ(x, i))
# 即,假设我们已经求出来了最后一个元素之前的和值,那么所有元素的目标和值应该是除了最后一个元素贡献的和值之外其他元素贡献的和值加上最后一个元素贡献的和值。而最后一个元素的和值又可以看成是最后一个元素与满足条件的子数组个数的乘积
sum(1) = sum(0) + 3 = 3
sum(2) = sum(1) + 1 * 6 = 3 + 1 * 6 = 9
sum(3) = sum(2) + 2 * 2 = 9 + 2 * 2 = 13
sum(4) = sum(3) + 4 = 13 + 4 = 17

代码演示

/*
 * @lc app=leetcode.cn id=907 lang=typescript
 *
 * [907] 子数组的最小值之和
 *
 * https://leetcode-cn.com/problems/sum-of-subarray-minimums/description/
 *
 * algorithms
 * Medium (31.12%)
 * Likes:    259
 * Dislikes: 0
 * Total Accepted:    10.1K
 * Total Submissions: 32.2K
 * Testcase Example:  '[3,1,2,4]'
 *
 * 给定一个整数数组 arr,找到 min(b) 的总和,其中 b 的范围为 arr 的每个(连续)子数组。
 * 
 * 由于答案可能很大,因此 返回答案模 10^9 + 7 。
 * 
 * 
 * 
 * 示例 1:
 * 
 * 
 * 输入:arr = [3,1,2,4]
 * 输出:17
 * 解释:
 * 子数组为 [3],[1],[2],[4],[3,1],[1,2],[2,4],[3,1,2],[1,2,4],[3,1,2,4]。 
 * 最小值为 3,1,2,4,1,1,2,1,1,1,和为 17。
 * 
 * 示例 2:
 * 
 * 
 * 输入:arr = [11,81,94,43,3]
 * 输出:444
 * 
 * 
 * 
 * 
 * 提示:
 * 
 * 
 * 1 
 * 1 
 * 
 * 
 * 
 * 
 */

// @lc code=start
function sumSubarrayMins(arr: number[]): number {
    // 依据题意,我们最重要的找出min(b),那么,我们第一时间可能会想到使用RMQ(求解区间最大/最小值)的问题
    // 在这里其实就可以转换为求取区间最小值的问题,而这个区间,其实就是我们的子数组。即:RMQ(x,y)=b
    // 此时,如果我们把每一个元素作为区间的右边界,即把R(x,y)=b问题转换成固定末尾的RMQ问题。即:RMQ(x, i)=b
    // 那么我们就可以以当前元素作为末尾,往前找到第一个小于当前元素的数k,那么,我们要找的子数组起始就在k与i之间。
    // 而这个子数组的最小值,就是我们的末尾元素(因为我们往前找到第一个小于末尾元素时就不再往前找了,因此末尾元素永远是子数组的最小值)
    // 最后,我们只需要把这些最小值累加起来就可以了
    const stack: number[] = [];
    const n = arr.length;
    const sums: bigint[] = new Array<bigint>(n+1);
    sums[0] = 0n;
    const mod = BigInt(1e9 + 7);
    let res: bigint = 0n;

    for(let i=0;i<n;i++) {
        // 维护单调递增栈的单调性
        while(stack.length && arr[i] <= arr[stack[stack.length-1]]) stack.pop();
        // 获取栈顶元素所在的索引
        const idx = stack.length?stack[stack.length-1]:-1;
        stack.push(i);
        // 计算以当前元素作为最小值的子数组元素的总和
        // 其中:i - idx代表当前元素与最近一个小于栈顶元素的元素的差值,用于计算需要累加多少次最小值(由于我们都是以当前元素作为最小值来处理的,
        // 因此,累加当前最小值总和时只需用当前元素与该差值相乘即可)
        // 此处使用到了递推的编程技巧。从sum[n-1]递推出sum[n]
        sums[stack.length] = (sums[stack.length-1] + BigInt(arr[i] * (i - idx))) % mod;
        
        // 将结果累加值结果数组中
        res += sums[stack.length];
        // 取模
        res %= mod;

    }

    return res as unknown as number;

};
// @lc code=end


496. 下一个更大元素 I

解题思路

依据题意,我们要求的是最近大于关系,所以可以使用单调递减栈辅助,由于nums1是nums2的子集,因此,我们可以直接找出nums2每个元素最近大于它的元素并将元素与最近大于它的元素的关系存储在哈希表中。然后在用nums1中的元素一一匹配即可。

代码演示

/*
 * @lc app=leetcode.cn id=496 lang=typescript
 *
 * [496] 下一个更大元素 I
 *
 * https://leetcode-cn.com/problems/next-greater-element-i/description/
 *
 * algorithms
 * Easy (68.31%)
 * Likes:    454
 * Dislikes: 0
 * Total Accepted:    88.5K
 * Total Submissions: 129.4K
 * Testcase Example:  '[4,1,2]\n[1,3,4,2]'
 *
 * 给你两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。
 * 
 * 请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值。
 * 
 * nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。
 * 
 * 
 * 
 * 示例 1:
 * 
 * 
 * 输入: nums1 = [4,1,2], nums2 = [1,3,4,2].
 * 输出: [-1,3,-1]
 * 解释:
 * ⁠   对于 num1 中的数字 4 ,你无法在第二个数组中找到下一个更大的数字,因此输出 -1 。
 * ⁠   对于 num1 中的数字 1 ,第二个数组中数字1右边的下一个较大数字是 3 。
 * ⁠   对于 num1 中的数字 2 ,第二个数组中没有下一个更大的数字,因此输出 -1 。
 * 
 * 示例 2:
 * 
 * 
 * 输入: nums1 = [2,4], nums2 = [1,2,3,4].
 * 输出: [3,-1]
 * 解释:
 * 对于 num1 中的数字 2 ,第二个数组中的下一个较大数字是 3 。
 * ⁠   对于 num1 中的数字 4 ,第二个数组中没有下一个更大的数字,因此输出 -1 。
 * 
 * 
 * 
 * 
 * 提示:
 * 
 * 
 * 1 
 * 0 
 * nums1和nums2中所有整数 互不相同
 * nums1 中的所有整数同样出现在 nums2 中
 * 
 * 
 * 
 * 
 * 进阶:你可以设计一个时间复杂度为 O(nums1.length + nums2.length) 的解决方案吗?
 * 
 */

// @lc code=start
function nextGreaterElement(nums1: number[], nums2: number[]): number[] {
    // 这道题要求找到第一个比当前元素大的元素,最近大于关系,可以用单调递减栈来辅助解决
    const stack: number[] = [];
    const tmp: Map<number, number> = new Map();
    // 首先求解出num2每一个元素的最近大于元素,并将这个关系存储在哈希表中
    for(let i=0;i<nums2.length;i++) {
        while(stack.length && nums2[i] > stack[stack.length-1]) {
            tmp.set(stack.pop(), nums2[i]);
        }
        stack.push(nums2[i]);
    }
    let res: number[] = [];
    // 在用nums1扫描一遍,看在哈希表中是否存在nums1中的元素,如果不存在,则为-1,否则,将哈希表的值赋值给结果数组
    for(let i=0;i<nums1.length;i++) {
        if(tmp.has(nums1[i])) {
            res[i] = tmp.get(nums1[i]);
        } else {
            res[i] = -1;
        }
    }
    return res;
};
// @lc code=end


456. 132 模式

解题思路

根据题意,其实我们可以先针对每一个元素j,往前找到在这个元素前面的最小值,这样,我们就已经确定了最小值nums[i]和最大值nums[j]了,我们只需要在j的后面再找一下是否有这样一个元素k,满足nums[i]

代码演示

/*
 * @lc app=leetcode.cn id=456 lang=typescript
 *
 * [456] 132 模式
 *
 * https://leetcode-cn.com/problems/132-pattern/description/
 *
 * algorithms
 * Medium (36.04%)
 * Likes:    533
 * Dislikes: 0
 * Total Accepted:    52.8K
 * Total Submissions: 146.6K
 * Testcase Example:  '[1,2,3,4]'
 *
 * 给你一个整数数组 nums ,数组中共有 n 个整数。132 模式的子序列 由三个整数 nums[i]、nums[j] 和 nums[k]
 * 组成,并同时满足:i < j < k 和 nums[i] < nums[k] < nums[j] 。
 * 
 * 如果 nums 中存在 132 模式的子序列 ,返回 true ;否则,返回 false 。
 * 
 * 
 * 
 * 示例 1:
 * 
 * 
 * 输入:nums = [1,2,3,4]
 * 输出:false
 * 解释:序列中不存在 132 模式的子序列。
 * 
 * 
 * 示例 2:
 * 
 * 
 * 输入:nums = [3,1,4,2]
 * 输出:true
 * 解释:序列中有 1 个 132 模式的子序列: [1, 4, 2] 。
 * 
 * 
 * 示例 3:
 * 
 * 
 * 输入:nums = [-1,3,2,0]
 * 输出:true
 * 解释:序列中有 3 个 132 模式的的子序列:[-1, 3, 2]、[-1, 3, 0] 和 [-1, 2, 0] 。
 * 
 * 
 * 
 * 
 * 提示:
 * 
 * 
 * n == nums.length
 * 1 
 * -10^9 
 * 
 * 
 */

// @lc code=start
function find132pattern(nums: number[]): boolean {
    // 首先,找出每个元素前面最小的元素,并记录于mins数组中
    const mins: number[] = new Array(nums.length);
    mins[0] = Number.MAX_SAFE_INTEGER;
    for(let i=1;i<nums.length;i++) mins[i] = Math.min(mins[i-1], nums[i-1]);
    // 有了当前元素左边最小值之后,我们就已经得到了一个比当前当前值小的数nums[i]和当前值nums[j]
    // 接下来,我们只要找出j之后是否存在一个数,满足nums[i] < nums[k] < nums[j]即可,即找出是否存在这个k
    // 那么,我们要如何高效查找是否存在这个k呢?我们可以向后查找第一个大于当前元素nums[k]的元素j,那么k~j这个区间
    // 内的所有元素便都是小于nums[k]的
    const stack: number[] = [];
    // 我们从后往前扫描数组,并将每个元素压入到一个单调递减栈当中,这期间,为了维护栈的单调性,会不断弹出来元素,
    // 而最后一个被弹出来的元素,就是当前元素向后最近一个大于当前元素的区间内所有小于当前元素的元素的最大值,
    // 我们拿这个最大值作为题目中的k尝试看能否满足条件,如果满足,说明找到了这个k,返回true
    for(let i = nums.length-1;i>=0;--i) {
        // 初始时让k复制为当前元素的值,之后会被比当前元素小的值不断覆盖,直到变成k变成比当前元素小的最大值
        let k = nums[i];
        // 当前元素前面的最小值
        const min = mins[i];
        // 为了维护单调递减栈的单调性,不断将比当前元素小的元素充栈中弹出,并不断覆盖k的值,以找到在j后面第一个比
        // j大的元素之间众多比j小的元素中的最大值。
        while(stack.length && nums[i] > stack[stack.length-1]) k = stack.pop();
        // 将当前元素入栈
        stack.push(nums[i]);
        // 判断找到的k是否是合法的,如果是,说明找到了一组满足132模式的数据,返回true
        if(min<nums[i]&&k<nums[i]&&k>min) return true;
    }
    return false;
};
// @lc code=end


42. 接雨水

解题思路

首先,我们来思考一个问题:怎样的结构才能接到雨水呢?是不是只有中间低两边高的“V型结构”才能接到雨水。假设我们的高度数组如下:

# [3,1,2,3,0]
# 下图中□代表箱子,○代表水
3 1 2 3 0
□ ○ ○ □
□ ○ □ □
□ □ □ □
0 1 2 3 4
# 从上图我们很明显可以知道,我们能盛多少水,主要取决于宽度和高度,宽度从左到右不断递增,而高度则是取决于左右两边高度中较小的一个
# 上图中由于第一个元素为3,前面没有能和它构成低洼的箱子,因此继续往后走
# 第二个元素为1,因为高度比上一个元素3小,不影响低洼的形成,继续往后走
# 第三个元素为2,比上一个元素1要高,此处会形成低洼,而此处低洼的高度为:
h1=Math.min(height[0],height[2])-height[1]=Math.min(3, 2) - 1 = 2 - 1 = 1
# 因此,此处产生的低洼高度为1;宽度为0~2之间的跨度,即1,所以低洼面积为:1*1=1
# 继续往后走,遇到元素高度为3,由于3大于上一个元素2,因此也会产生低洼,此处低洼的高度为:
h2=Math.min(height[0],height[3])-height[2] = Math.min(3,3) - 2 = 1;
# 因此,此处低洼的高度也为1,宽度为0~3的跨度,即2,所以低洼面积为:2*1=2
# 继续往后走,此处由于高度比上一个元素3小,可能产生低洼,但因为已经没有下一个元素了,无法留住积水,因此,此处实际不可能产生低洼
# 因此此处谁的面积为:h3=0;

# 因此上述低洼的总面积为sum=h1+h2+h3=1+2+0=3

从上面的示例中,我们不难发现,我们每次遇到比上一个元素高的元素时,就开始计算之前的低洼面积,而低洼面积的计算公式为:

width = (当前索引 - 最近一个大于或等于当前元素的索引 - 1)

height = Math.min(当前元素的高度,最近第一个大于或等于当前元素的高度) - 上一个元素的高度

area = width + height

带入上面的公式,我们来验证一下上面的结果:

当前元素为2时:

area = width + height = (2 - 0 - 1) * (Math.min(2,3) - 1) = 1 * 1 = 1

当前元素为3时:

area2 = width + height = (3 - 0 - 1) * (Math.min(3,3) - 2) = 2 * 1 = 2

因此总面积为:area = 1 + 2 + 0 = 3;

由于上面需要招最近第一个大于等于当前元素的高度,我们可以使用单调递减栈来辅助

代码演示

/*
 * @lc app=leetcode.cn id=42 lang=typescript
 *
 * [42] 接雨水
 *
 * https://leetcode-cn.com/problems/trapping-rain-water/description/
 *
 * algorithms
 * Hard (56.30%)
 * Likes:    2512
 * Dislikes: 0
 * Total Accepted:    279.5K
 * Total Submissions: 493.5K
 * Testcase Example:  '[0,1,0,2,1,0,1,3,2,1,2,1]'
 *
 * 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
 * 
 * 
 * 
 * 示例 1:
 * 
 * 
 * 
 * 
 * 输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
 * 输出:6
 * 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 
 * 
 * 
 * 示例 2:
 * 
 * 
 * 输入:height = [4,2,0,3,2,5]
 * 输出:9
 * 
 * 
 * 
 * 
 * 提示:
 * 
 * 
 * n == height.length
 * 0 
 * 0 
 * 
 * 
 */

// @lc code=start
function trap(height: number[]): number {
    // 首先可以思考一下,怎样的结构才能接到雨水呢?答案肯定是中间低,四周高的“V型结构”, 
    let res = 0;
    const stack: number[] = [];
    // 1 0 2 3 0
    for(let i=0;i<height.length;i++) {
        while(stack.length && height[i] > height[stack[stack.length-1]]) {
            // 弹出的高度小于准备压入栈中的高度
            const cur = stack.pop();
            // 如果弹出之后,发现栈已经空了,则进入下一次循环
            if(stack.length===0) continue;
            // 计算宽度
            // width=(当前索引 - 最近一个大于或等于当前元素的索引 - 1)
            const w = (i - stack[stack.length-1] - 1);
            // 计算高度
            // height = Math.min(当前元素的高度,最近第一个大于或等于当前元素的高度) - 上一个元素的高度
            const h = Math.abs(Math.min(height[stack[stack.length-1]], height[i]) - height[cur]);
            // 计算本层面积并累加
            res += w * h;
        }
        stack.push(i);
    }
    return res;
};
// @lc code=end


你可能感兴趣的:(知识梳理,数据结构,前端基础,单调栈,数据结构,算法,刷题)