数据结构——单调栈详解

单调栈定义:

从栈底元素到栈顶元素呈单调递增或单调递减,栈内序列满足单调性的栈。

一般用于求解,元素的左边或者右边第一个大于或者小于元素的值

这里通过几道单调栈的例题进行举例分析。

 

单调递减栈:

场景:

  • 在一个队列中针对每一个元素从它右边寻找第一个比它大的元素
  • 在一个队列中针对每一个元素从它左边寻找第一个比它大的元素(从后往前遍历)

这里采用大神labuladong的算法小抄的两个例题举例:

例1.第一个更大的元素

题目:

给你⼀个数组,返回⼀个等⻓的数组,对应索引存储着下⼀个更⼤元素,如果没有更⼤的元素,就存-1。

例: 数组 [2,1,2,4,3],返回数组 [4,2,4,-1,-1]。

解释:第⼀个 2 后⾯⽐ 2 ⼤的数是 4; 1 后⾯⽐ 1 ⼤的数是 2;第⼆个 2 后⾯⽐ 2 ⼤的数是 4; 4 后⾯没有⽐ 4 ⼤的数,填 -1;3 后⾯没有⽐ 3 ⼤的数,填 -1。

 

题目很简单,就是找右边第一个比自己大的数,找到返回找到的数字,没有就返回-1.

 

这就是一个思路,从后往前遍历,栈中存放最大元素在栈底,

 

 /*
    给你⼀个数组,返回⼀个等
    ⻓的数组,对应索引存储着下⼀个更⼤元素,如果没有更⼤的元素,就存
    -1。不好⽤语⾔解释清楚,直接上⼀个例⼦:
    给你⼀个数组 [2,1,2,4,3],你返回数组 [4,2,4,-1,-1]。
    解释:第⼀个 2 后⾯⽐ 2 ⼤的数是 4; 1 后⾯⽐ 1 ⼤的数是 2;第⼆个 2 后⾯
    ⽐ 2 ⼤的数是 4; 4 后⾯没有⽐ 4 ⼤的数,填 -1;3 后⾯没有⽐ 3 ⼤的数,填
    -1。
     */

    //单调递减栈
    public static int[] convert(int[] param){
        int[] result=new int[param.length];
        Stack<Integer> stack=new Stack<>();

        //倒着遍历
        for (int i = param.length - 1; i >= 0; i--) {
            //将最大的元素放入栈底
            while(!stack.isEmpty()&&stack.peek()<param[i]){
                stack.pop();
            }
            //while中pop出所有比param[i]小的元素,栈顶元素即是右边第一个可以满足的元素
            result[i]=stack.isEmpty()?-1:stack.peek();
            //放入,方便下一次比较
            stack.push(param[i]);
        }
        return result;
    }

 

例2.更温暖的气温

给你⼀个数组 T = [73, 74, 75, 71, 69, 72, 76, 73],这个数组存放的是近⼏天的天⽓⽓温(这⽓温是铁板烧?不是的,这⾥⽤的华⽒度)。你返回⼀个数组,计算:对于每⼀天,你还要⾄少等多少天才能等到⼀个更暖和的⽓温;如果等不到那⼀天,填 0 。
举例:给你 T = [73, 74, 75, 71, 69, 72, 76, 73],你返回 [1, 1, 4, 2, 1, 1, 0, 0]

和第一题类似,这里不做过多的解释,直接贴答案了

public static int[] convertT(int[] param){
        int[] result=new int[param.length];
        Stack<Integer> stack=new Stack<>();
        for (int i = param.length - 1; i >= 0; i--) {
            while(!stack.isEmpty()&&param[stack.peek()]<param[i]){
                stack.pop();
            }
            result[i]=stack.isEmpty()?0:stack.peek()-i;
            stack.push(i);
        }
        return result;
    }

 

例3.滑动窗口

题目

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

分析

该题也是一个递减栈的变形,我们只要同过stack存放一个递减栈,让每次取栈底元素就行了,这里使用双端队列,直接取就可以。

 

public int[] maxSlidingWindow(int[] nums, int k) {
        int[] result = new int[nums.length - k + 1];
        Deque<Integer> stack = new LinkedList<>();
        for (int i = 0; i < nums.length; i++) {
            //单调递减栈组装数据
            while(!stack.isEmpty()&&nums[stack.peekLast()]<nums[i]){
                stack.pollLast();
            }
            stack.addLast(i);

            //如果当前-栈顶元素大于个数限制,将栈顶元素出栈
            if(i-stack.peekFirst()>=k){
                stack.pollFirst();
            }
            //避免越界,当i-k+1>=0才有意义
            if (i-k+1>=0){
                result[i-k+1]=nums[stack.peekFirst()];
            }


        }
        return result;
    }

 

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/sliding-window-maximum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

 

 

 

 

 

单调递增栈:

  • 在一个队列中针对每一个元素从它右边寻找第一个比它小的元素
  • 在一个队列中针对每一个元素从它左边寻找第一个比它小的元素(从后往前遍历)

 

例4.力扣真题

https://leetcode-cn.coml/problems/largest-rectangle-in-histogram/description/

穷举

其实可以想到的思路是穷举

以2作为高,计算最大面积;

以1作为高,计算最大面积;

以5作为高,计算最大面积;

以6作为高,计算最大面积;

以2作为高,计算最大面积;

以3作为高,计算最大面积;

每个的结果取大,即最终结果

于是有了如下代码:

public class Leetcode84 {
    public static void main(String[] args) {
        Leetcode84 leetcode84 = new Leetcode84();
        int[] param = {2,1,5,6,2,3};
        System.out.println(leetcode84.largestRectangleArea(param));

    }

    public int largestRectangleArea(int[] heights) {
        //遍历heights,当前元素作为构建形状的最低形状高度,由左和右分别判定可组成的长度。
        int areaMax = 0;
        for (int i = 0; i < heights.length; i++) {
            int length = 1;
            int leftIndex = i - 1;
            int rightIndex = i + 1;
            while (leftIndex >= 0 && heights[leftIndex] >= heights[i]) {
                length++;
                leftIndex--;
            }
            while (rightIndex < heights.length && heights[rightIndex] >= heights[i]) {
                length++;
                rightIndex++;
            }
            if (length * heights[i] > areaMax) {
                areaMax = length * heights[i];
            }
        }
        return areaMax;
    }

}

力扣提交,结果:

 

....

单调栈

单调栈的处理逻辑

还是以题中的数据举例,原来数据如下。

 

为了更形象的使用这个逻辑,在头和尾补上元素0,

这里补上0主要是为了遍历避免判空,方便第一位和最后一位元素的判定。

转换后如下:

比如要找寻2组成的面积,那么就需要找寻左右两边第一个小于2的元素,通过栈,将0入栈,2入栈,当遇到1时,即可以求出左遍大于1的面积,求出后将其出栈。详细分析过程如下

详细过程分析

注意,我们这里举例的stack中存储的元素的value值一直是一个递增栈

1. 源数据结构如下

 

2. 数组头和尾补0,注意这里的stack需要存储的是index值,而不是value值,因为stack用于帮我们找到宽度

3. 开始遍历数组,第一个元素为0

4. 继续遍历,处理index=1的元素

5. 遍历第三个元素,因为现在找到了元素的右边界,所以可以求出大于高度为1的元素的面积

6. 遍历第四个元素index=3

7. 遍历第五个元素index=4

8. 遍历第六个元素index=5,处理第五个元素,pop出index=4,计算高度为6的面积

9. 处理第四个元素,pop出index=3,处理高度为5的面积

10. 此时,由于第五个元素的值大于栈中arr[2]=1的值,此时index=5入栈

 

11.第七个元素入栈

12. 继续处理到第八个元素,发现不满足递增栈,即到了右边界,pop栈,计算高度为3的元素的值

 

13. 出栈后,由于0<arr[5],继续出栈,计算index为5的元素的面积

14. 由于0<arr[2],此时计算index=2的元素的面积

这样我们的所有元素的面积,就已经全部计算完毕。每个取大就可以了。

 

代码实现

package com.learn.mk.leetcode;


import java.util.Stack;

/**
 * 84. 柱状图中最大的矩形
 * 给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
 * <p>
 * 求在该柱状图中,能够勾勒出来的矩形的最大面积。
 * url:https://leetcode-cn.com/problems/largest-rectangle-in-histogram/description/
 */
public class Leetcode84 {

    
    public int largestRectangleArea(int[] heights) {
        int tempMax = 0;
        int[] heightsConvert = new int[heights.length + 2];
        System.arraycopy(heights, 0, heightsConvert, 1, heightsConvert.length - 2);
        Stack<Integer> stack = new Stack<>();
        for (int i = 0; i < heightsConvert.length; i++) {
            while (!stack.isEmpty() && heightsConvert[stack.peek()] > heightsConvert[i]) {
                Integer pop = stack.pop();
                tempMax = Math.max(tempMax, heightsConvert[pop] * (i - stack.peek()-1));
            }
            stack.push(i);
        }
        return tempMax;

    }


    public static void main(String[] args) {
        Leetcode84 leetcode84 = new Leetcode84();
        int[] param = {2,1,5,6,2,3};
        System.out.println(leetcode84.largestRectangleArea(param));
    }

}

 

可以进一步优化代码,减少判定逻辑,如下:

package com.learn.mk.leetcode;


import java.util.Stack;

/**
 * 84. 柱状图中最大的矩形
 * 给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
 * <p>
 * 求在该柱状图中,能够勾勒出来的矩形的最大面积。
 * url:https://leetcode-cn.com/problems/largest-rectangle-in-histogram/description/
 */
public class Leetcode84 {


    public int largestRectangleArea(int[] heights) {
        int tempMax = 0;
        int[] heightsConvert = new int[heights.length + 2];
        System.arraycopy(heights, 0, heightsConvert, 1, heightsConvert.length - 2);
        Stack<Integer> stack = new Stack<>();
        stack.push(0);
        for (int i = 0; i < heightsConvert.length; i++) {
            while (heightsConvert[stack.peek()] > heightsConvert[i]) {
                Integer pop = stack.pop();
                tempMax = Math.max(tempMax, heightsConvert[pop] * (i - stack.peek()-1));
            }
            stack.push(i);
        }
        return tempMax;

    }


    public static void main(String[] args) {
        Leetcode84 leetcode84 = new Leetcode84();
        int[] param = {2,1,5,6,2,3};
        System.out.println(leetcode84.largestRectangleArea(param));
    }

}

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
稀疏矩阵是指其中大部分元素为零的矩阵。由于大多数矩阵都是稠密的,即大多数元素都不为零,因此通常情况下,我们使用一个二维数组来表示一个矩阵。但是,对于稀疏矩阵来说,这种方法会造成很大的浪费,因为大量的空间被用来存储零元素。为了解决这个问题,我们可以使用稀疏矩阵三元组表示法。 稀疏矩阵三元组表示法是将稀疏矩阵中每个非零元素的行、列和值存储在一个三元组中。其数据结构如下所示: ``` struct Triple{ int row, col; double value; }; ``` 其中,row表示非零元素所在的行,col表示非零元素所在的列,value表示非零元素的值。我们可以使用一个数组来存储所有的非零元素,这个数组就是稀疏矩阵的三元组。 稀疏矩阵三元组表示法的优点是它可以节省存储空间,缺点是它不方便进行矩阵运算。因此,在进行矩阵运算时,我们需要将稀疏矩阵转换成其他更方便进行矩阵运算的表示方法,如压缩矩阵和坐标矩阵等。 对于稀疏矩阵的求解,可以使用稀疏矩阵三元组表示法结合三元组高斯消元算法来进行求解。三元组高斯消元算法是一种针对稀疏矩阵的高斯消元算法,其基本思想是将矩阵化为上三角矩阵或下三角矩阵,然后通过回代或者前代求解方程。由于矩阵中大部分元素为零,因此在进行高斯消元时,我们只需要考虑非零元素,这样可以大大提高计算效率。 三元组高斯消元算法的基本步骤如下: 1. 将稀疏矩阵转换成三元组表示法; 2. 对三元组按照行和列的顺序进行排序; 3. 从第一个非零元素开始,进行高斯消元,将矩阵化为上三角矩阵或下三角矩阵; 4. 通过回代或者前代求解方程。 具体实现可以参考以下代码: ``` void SparseTripletGaussElimination(SparseTriplet& triplet, vector<double>& b) { int n = triplet.rows; vector<Triple> A(triplet.data, triplet.data + triplet.num); sort(A.begin(), A.end(), [](const Triple& a, const Triple& b){ return a.row < b.row || (a.row == b.row && a.col < b.col); }); vector<int> row(n+1), col(triplet.num), diag(n); vector<double> val(triplet.num); for (int i = 0; i < triplet.num; i++) { row[A[i].row]++; } for (int i = 1; i <= n; i++) { row[i] += row[i-1]; } for (int i = 0; i < triplet.num; i++) { int r = A[i].row, c = A[i].col; double v = A[i].value; int k = row[r]++; // 获取 r 行中下一个非零元素的位置 col[k] = c; val[k] = v; if (r == c) { diag[r] = k; // 记录对角线元素的位置 } } for (int k = 0; k < n-1; k++) { if (val[diag[k]] == 0) { // 对角线元素为零,无法消元 throw runtime_error("zero pivot encountered"); } for (int i = diag[k]+1; i < row[k+1]; i++) { int r = col[i]; double factor = val[i] / val[diag[k]]; for (int j = diag[k]+1; j < row[k+1]; j++) { if (col[j] == r) { val[j] -= factor * val[diag[k]]; } } b[r] -= factor * b[k]; } } if (val[diag[n-1]] == 0) { // 对角线元素为零,无法消元 throw runtime_error("zero pivot encountered"); } for (int k = n-1; k >= 0; k--) { double sum = 0; for (int i = diag[k]+1; i < row[k+1]; i++) { sum += val[i] * b[col[i]]; } b[k] = (b[k] - sum) / val[diag[k]]; } } ``` 其中,SparseTriplet是稀疏矩阵三元组表示法的数据结构,b是待求解的方程的右侧向量。在实现中,我们首先将三元组按照行和列的顺序进行排序,然后将其转换成压缩矩阵的形式,接着进行高斯消元,并通过回代或者前代求解方程。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值