单调栈及经典问题

306 阅读3分钟

单调栈

  • 栈和队列的区别在于,一般情况下,队列可以在两端进出元素,而栈只能由一端进出元素
  • 本质上,单调队列的队首禁止进出元素,只能从队尾进出元素时,就变成了一个单调栈
  • 如下图所示,将数组元素依次压入单调栈中
    • 当紫色元素入栈时,蓝色线条左侧元素均大于紫色元素,黄色元素均小于紫色元素
    • 为保持栈内元素单调递减的性质,需要将黄色元素全部出栈,此时
      • 原数组中的紫色元素左侧第一个比自己大的元素为绿色 a 元素
      • 原数组中的黄色元素们右侧第一个比自己大的元素为紫色元素

  • 上面是单调栈的入栈过程,下面总结一下,单调队列与单调栈的性质

    • 单调队列,擅长维护区间最值,若维护的是区间最小值,则队列是单调递增的
    • 单调栈,擅长维护集合中的元素的最近大小关系
      • 从左侧入栈时,维护的是左侧的最近关系

      • 从右侧入栈时,维护的是右侧的最近关系

  • 下面通过代码演示单调栈的能力,现给出数组 list,求原数组中每个元素,左侧最近的比自己小的元素,右侧最近的比自己小的元素

    • 下面代码中 stack 为单调递减栈的数据区域,存放的是原数组中元素的下标值
    • prev 为存放原数组中,对应元素左侧的第一个比自己小的元素的下标值
    • next 为存放原数组中,对应元素右侧的第一个比自己小的元素的下标值
    • 首先将所有元素依次入栈,同时维护栈内元素的单调性,将不符合单调栈规则的元素进行 pop 操作,同时,被出栈的元素右侧比他们更小的且最近的元素在原数组中的下标值 i
    • 一轮操作后,若此时的栈已空,则表示 i 元素左侧没有比它小更小的元素,可以使用虚拟元素索引 -1 表示更小元素索引,否则栈顶元素就是当前元素 i 左侧最近的更小元素,然后将 i 入栈即可
    • 所有元素完成入栈,表示栈内剩余的元素右侧没有比之更小的元素了,可以使用虚拟元素索引 list.length 表示更小元素的索引值
/**
 * @param {*} list 原数组
 */
const ascStack = (list) => {
  const stack = []; 
  const prev = []; 
  const next = []; 

  for (let i = 0; i < list.length; i++) {
    while (stack.length && list[stack[stack.length - 1]] > list[i]) {
      const ind = stack.pop();
      next[ind] = i;
    }
    
    if (!stack.length) prev[i] = -1;
    else prev[i] = stack[stack.length - 1];
    stack.push(i); // i 入栈
  }

  while (stack.length) {
    next[stack.pop()] = list.length;
  }

  console.log(`
    ${list.join('   ')} \n
    ${prev.join('   ')} \n
    ${next.join('   ')} \n
  `);
};

ascStack([6, 7, 9, 0, 8, 3, 4, 5, 1, 2]);
  • 上述代码输出如下
// output:
// list:     6   7   9   0   8   3   4   5   1   2
// ind: -1   0   1   2   3   4   5   6   7   8   9   10
// prev:    -1   0   1  -1   3   3   5   6   3   8
// next:     3   3   3  10   5   8   8   8  10  10

小结

  • 本文介绍了单调栈的特点及其代码示例,并对比总结了单调栈与单调队列的差异特点

“ 本文正在参加「金石计划 . 瓜分6万现金大奖」 ”