[算法入门笔记] 13. 单调栈

1. 需求

[问题]
给定一个不含重复值数组的arr,找到每一个i位置左边和右边i位置最近且比arr[i]的位置,时间复杂度为 O ( N ) O(N) O(N)

2. 方法论

准备一个stack,栈中元素放数组位置,开始stack为空。

  • 如果找到每个i位置左边和右边i位置最近且值比arr[i]的位置,那么需要让stack栈顶到栈底的位置所代表的严格递减
  • 如果找到每个i位置左边和右边i位置最近且值比arr[i]的位置,那么需要让stack栈顶到栈底的位置所代表的值是严格递增

典例演示1

初始数组 a r r = { 3 , 4 , 1 , 5 , 6 , 2 , 7 } arr=\{3, 4, 1, 5, 6, 2, 7\} arr={3,4,1,5,6,2,7}stack从栈顶到栈底为 { } \{\} {}

遍历到 a r r [ 0 ] = 3 arr[0]=3 arr[0]=3stack空,直接放入0位置,stack从栈顶到栈底 { 0 位置(值 3 ) } \{0位置(值3)\} {0位置(值3}

在这里插入图片描述

遍历到 a r r [ 1 ] = 4 arr[1]=4 arr[1]=4,直接放入1位置,不会破坏从栈顶到栈底位置所代表的值的递减顺序stack从栈顶到栈底 { 1 位置 , 0 位置 } \{1位置, 0位置\} {1位置,0位置}

在这里插入图片描述

遍历到 a r r [ 2 ] = 1 arr[2]=1 arr[2]=1,直接放入2位置会破坏从栈顶到栈底位置所代表的值的递减顺序,所以从stack开始弹出位置,x位置被弹出,在栈中位于x位置下面的位置,就是x位置左边离x最近且值比arr[x]小的位置

当前遍历到的位置是x位置右边离x位置最近且比arr[x]小的位置,从stack弹出位置在栈中位于1位置下面的是位置0,当前遍历的位置2,所以 a n s [ 1 ] = { 0 , 2 } ans[1]=\{0,2\} ans[1]={0,2}

弹出位置1后,发现放入2位置还会破坏从栈顶到栈底位置所代表的值的递减顺序,所以继续弹出位置0

在栈中位于0位置下面没有位置,说明位置0左边不存在比arr[0]小的值,当前遍历的位置是2,所以 a n s [ 0 ] = { − 1 , 2 } ans[0]=\{-1, 2\} ans[0]={1,2}

stack已为空,所以放入2位置,stack从栈顶到栈底为 { 2 位置 } \{2位置\} {2位置}

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

遍历到 a r r [ 3 ] = 5 arr[3]=5 arr[3]=5,直接放入3位置,不会破坏从栈顶到栈底位置所代表的值的递减顺序stack从栈顶到栈底为 { 3 位置 , 2 位置 } \{3位置, 2位置\} {3位置,2位置}

在这里插入图片描述

遍历到 a r r [ 4 ] = 6 arr[4]=6 arr[4]=6,直接放入4位置,不会破坏从栈顶到栈底位置所代表的值的递减顺序,stack从栈顶到栈底为 { 4 位置 , 3 位置 , 2 位置 } \{4位置, 3位置, 2位置\} {4位置,3位置,2位置}

在这里插入图片描述

遍历到 a r r [ 5 ] = 2 arr[5]=2 arr[5]=2,直接放入2位置,会破坏从栈顶到栈底位置所代表的值的递减顺序,开始弹出位置,弹出位置4,栈中它的下面位置是3,当前位置是5 a n s [ 4 ] = 3 , 5 ans[4]={3,5} ans[4]=3,5

在这里插入图片描述

弹出位置3,栈中它的下面位置是2,当前位置是5 a n s [ 3 ] = 2 , 5 ans[3]={2, 5} ans[3]=2,5

在这里插入图片描述

放入5位置不会破坏单调性

在这里插入图片描述

遍历到 a r r [ 6 ] = 7 arr[6]=7 arr[6]=7,发现直接放入6位置,不会破坏单调性,那么直接放入

在这里插入图片描述

遍历结束后清算剩下位置

弹出6位置,栈中下面是位置5 a n s [ 6 ] = 5 , − 1 ans[6]={5, -1} ans[6]=5,1

弹出5位置,栈中下面是位置2 a n s [ 5 ] = 2 , − 1 ans[5]={2, -1} ans[5]=2,1

弹出2位置,栈中下面没有位置信息, a n s [ 2 ] = − 1 , − 1 ans[2]={-1, -1} ans[2]=1,1

算法证明

证明:在单调栈中,

如果x位置被弹出,在栈中位于x位置下面的位置是x位置左边离x位置最近且值比arr[x]小的位置,

当遍历到的位置就是x位置右边离x位置最近且值比arr[x]小的位置

在这里插入图片描述

假设数组中元素不重复

  • 当前来到j位置,x位置已经在栈中,所以x位置一定在j位置左边:…5(x位置)…4(j位置)…。如果5和4之间存在小于5的数,那么没等到遍历j位置,x位置已经被弹出了,轮不到当前j位置让x位置弹出,所以5和4之间要么没有数,要么比5大,因此x位置右边离x最近的位置且值小于arr[x]的位置是j位置
  • 当前弹出的位置是x位置,x位置下面是位置iix位置早进栈,所以i位置肯定在x位置左边:…1(i位置)…5(x位置)…。如果1和5之间存在小于1的数,那么i位置会被提前弹出,在栈中i位置和x位置不可能在一起。如果1和5之间存在大于1小于5的数,那么栈中i位置和x位置之间会夹上一个别的位置,也不可能贴在一起,所以1和5之间要么没有,要么一定比5大,那么x位置左边离x位置最近且小于arr[x]的位置就是i位置

时间复杂度

整个流程中,每个位置进栈一次、出栈一次,时间复杂度为 O ( N ) O(N) O(N)

算法实现

/**
 * 数组无重复情况
 * @param arr
 * @return
 */
public int[][] getNearLessNoRepeat(int[] arr) {
    int[][] res = new int[arr.length][2];
    Stack<Integer> stack = new Stack<>();
    for (int i = 0; i < arr.length; i++) {
        // 出现破坏从栈顶到栈底值递减单调性的情况
        while (stack.isEmpty() && arr[stack.peek()] > arr[i]) {
            int popIndex = stack.pop();
            // popIndex位置左边离popIndex最近且值比它小的位置
            int leftLessIndex = stack.isEmpty() ? -1 : stack.peek();
            res[popIndex][0] = leftLessIndex;
            // popIndex位置右边离popIndex最近且值比它小的位置
            res[popIndex][1] = i;
        }
        // 没有破坏单调性就压栈
        stack.push(i);
    }
    // 元素遍历结束,处理栈中剩余元素
    while (!stack.isEmpty()) {
        int popIndex = stack.pop();
        int leftLessIndex = stack.isEmpty() ? -1 : stack.peek();
        // popIndex位置左边离popIndex最近且值比它小的位置
        res[popIndex][0] = leftLessIndex;
        // popIndex位置右边离popIndex最近且值比它小的位置
        res[popIndex][1] = -1;
    }
    return res;
}

进阶问题

给定一个可能含有重复值的数组arr,找到每一个i位置左边和右边离i位置最近且值比arr[i]小的位置

典例演示2

初始数组 a r r = { 3 , 1 , 3 , 4 , 3 , 5 , 3 , 2 , 2 } arr=\{3, 1, 3, 4, 3, 5, 3, 2, 2\} arr={3,1,3,4,3,5,3,2,2}stack从栈顶到栈底为 { } \{\} {}

遍历到 a r r [ 0 ] = 3 arr[0]=3 arr[0]=3,发现栈空,直接放入0位置。stack从栈顶到栈底 0 位置 ( 3 ) {0位置(3)} 0位置(3)

遍历到 a r r [ 1 ] = 1 arr[1]=1 arr[1]=1,从栈中弹出0位置, a n s [ 0 ] = { − 1 , 1 } ans[0]=\{-1,1\} ans[0]={1,1},位置1进栈。stack从栈顶到栈底 { 1 位置 ( 1 ) } \{1位置(1)\} {1位置(1)}

遍历到 a r r [ 2 ] = 3 arr[2]=3 arr[2]=3,发现2位置可以直接放入,位置2进栈。stack从栈顶到栈底 { 2 位置 ( 3 ) , 1 位置 ( 1 ) } \{2位置(3), 1位置(1)\} {2位置(3),1位置(1)}

遍历到 a r r [ 3 ] = 4 arr[3]=4 arr[3]=4,发现arr[3]可以直接放入,位置3进栈。stack从栈顶到栈底 { 3 位置 ( 4 ) , 2 位置 ( 3 ) , 1 位置 ( 1 ) } \{3位置(4), 2位置(3), 1位置(1)\} {3位置(4),2位置(3),1位置(1)}

遍历到 a r r [ 4 ] = 3 arr[4]=3 arr[4]=3,发现arr[4]不能直接放入,弹出3位置, a n s [ 3 ] = { 2 , 4 } ans[3]=\{2,4\} ans[3]={2,4}。此时发现栈顶是位置2,值是3,当前遍历位置是4值也是3两个位置压在一起stack从栈顶到栈底 { ∣ 2 位置 , 4 位置 ∣ ( 3 ) , 1 位置 ( 1 ) } \{|2位置,4位置|(3), 1位置(1)\} {∣2位置,4位置(3),1位置(1)}

在这里插入图片描述

遍历到 a r r [ 5 ] = 5 arr[5]=5 arr[5]=5,发现5位置直接放入。stack从栈顶到栈底 { 5 位置 ( 5 ) , ∣ 2 位置 , 4 位置 ∣ ( 3 ) , 1 位置 ( 1 ) } \{5位置(5) ,|2位置,4位置|(3), 1位置(1)\} {5位置(5),∣2位置,4位置(3),1位置(1)}

遍历到 a r r [ 6 ] = 3 arr[6]=3 arr[6]=3,弹出5位置,在栈中位置5下面是 ∣ 2 位置 , 4 位置 ∣ |2位置,4位置| ∣2位置,4位置,选最晚加入的4位置,当前遍历到6位置,所以 a n s [ 5 ] = { 4 , 6 } ans[5]=\{4,6\} ans[5]={4,6}。位置6进栈。stack从栈顶到栈底 { ∣ 2 位置 , 4 位置 , 6 位置 ∣ ( 3 ) , 1 位置 ( 1 ) } \{|2位置,4位置,6位置|(3), 1位置(1)\} {∣2位置,4位置,6位置(3),1位置(1)}

在这里插入图片描述

遍历到 a r r [ 7 ] = 2 arr[7]=2 arr[7]=2,从栈中弹出 ∣ 2 位置 , 4 位置 , 6 位置 ∣ |2位置,4位置,6位置| ∣2位置,4位置,6位置,在栈中这些位置下面是1位置,当前是7位置, a n s [ 2 ] = { 1 , 7 } , a n s [ 4 ] = { 1 , 7 } , a n s [ 6 ] = { 1 , 7 } ans[2]=\{1,7\},ans[4]=\{1,7\},ans[6]=\{1,7\} ans[2]={1,7}ans[4]={1,7}ans[6]={1,7}。位置7进栈,stack从栈顶到栈底 { 7 位置 ( 2 ) , 1 位置 ( 1 ) } \{7位置(2), 1位置(1)\} {7位置(2),1位置(1)}

遍历到 a r r [ 8 ] = 2 arr[8]=2 arr[8]=2,发现位置8可以直接进栈,并且又是相等情况,stack从栈顶到栈底 { ∣ 7 位置 , 8 位置 ∣ ( 2 ) , 1 位置 ( 1 ) } \{|7位置,8位置|(2), 1位置(1)\} {∣7位置,8位置(2),1位置(1)}

遍历完成后进入清算阶段:

弹出 ∣ 7 位置 , 8 位置 ∣ |7位置,8位置| ∣7位置,8位置 a n s [ 7 ] = { 1 , − 1 } , a n s [ 8 ] = { 1 , − 1 } ans[7]=\{1,-1\},ans[8]=\{1,-1\} ans[7]={1,1}ans[8]={1,1}

弹出1位置, a n s [ 1 ] = { − 1 , − 1 } ans[1]=\{-1,-1\} ans[1]={1,1}

算法实现

/**
 * 数组有重复情况
 * @param arr
 * @return
 */
public int[][] getNearLess(int[] arr) {
    int[][] res = new int[arr.length][2];
    Stack<List<Integer>> stack = new Stack<>();
    for (int i = 0; i < arr.length; i++) {
        // 出现破坏从栈顶到栈底值递减单调性的情况
        while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[i]) {
            // 栈顶的列表元素
            List<Integer> popIs = stack.pop();
            // 取位于下面位置的列表中,最晚加入的那个
            int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(
                    stack.peek().size() - 1);
            // 列表重复元素集体清算
            for (Integer popi : popIs) {
                res[popi][0] = leftLessIndex;
                res[popi][1] = i;
            }
        }
        // 处理没有破坏单调性的情况
        if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]) {
            // 当前元素与栈顶元素重复情况
            stack.peek().add(Integer.valueOf(i));
        } else {
            // 构造列表,进队
            ArrayList<Integer> list = new ArrayList<>();
            list.add(i);
            stack.push(list);
        }
    }
    // 元素遍历结束,处理栈中剩余元素
    while (!stack.isEmpty()) {
        List<Integer> popIs = stack.pop();
        // 取位于下面位置的列表中,最晚加入的那个
        int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(
                stack.peek().size() - 1);
        for (Integer popi : popIs) {
            res[popi][0] = leftLessIndex;
            res[popi][1] = -1;
        }
    }
    return res;
}

问题扩展

定义:数组中累积和与最小值的乘积,假设叫做指标A。给定一个数组,请返回子数组中,指标A最大的值。

在这里插入图片描述

在这里插入图片描述

/**
 * 单调栈处理
 * 该位置左边离他最近且值比他小的,是不能扩的位置
 * 该位置右边离他最近且值比他小的,是不能扩的位置
 * @param arr
 * @return
 */
public int max(int[] arr) {
    int size = arr.length;
    int[] sums = new int[size];
    sums[0] = arr[0];
    // 求前缀和
    for (int i = 1; i < size; i++) {
        sums[i] = sums[i - 1] + arr[i];
    }
    int max = Integer.MIN_VALUE;
    Stack<Integer> stack = new Stack<>();
    for (int i = 0; i < size; i++) {
        // 破坏单调性的情况,找到不能扩的位置边界
        while (!stack.isEmpty() && arr[stack.peek()] >= arr[i]) {
            // 以j位置作为最小值
            int j = stack.pop();
            // sums[stack.peek()] j左边不能扩的位置的前缀和
            // sums[i - 1] - sums[stack.peek()] j位置为最小值开始右扩  --> 累加和
            max = Math.max(max, (stack.isEmpty() ? sums[i - 1] : (sums[i - 1] - sums[stack.peek()])) * arr[j]);
        }
        // 遵循单调性,压栈
        stack.push(i);
    }
    // 遍历完成处理剩余元素
    while (!stack.isEmpty()) {
        // 以j位置作为最小值
        int j = stack.pop();
        max = Math.max(max, (stack.isEmpty() ? sums[size - 1] : (sums[size - 1] - sums[stack.peek()])) * arr[j]);
    }
    return max;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cyan Chau

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值