栈进阶!单调栈

270 阅读3分钟

这是我参与更文挑战的第 9 天,活动详情查看: 更文挑战

单调栈概念和特点

单调栈的定义:单调栈就是指栈中的元素必须是按照升序排列的栈,或者是降序排列的栈。

我们把升序排列的栈称为递增栈。降序排列的栈称为递减栈

单调栈任何时候都要保持栈的有序性。

因此我们可以总结出单调栈的特点:

递增栈:

  • 小数消除大数
  • 栈中元素递增

递减栈:

  • 大数消除小数
  • 栈中元素递减

经典例子

【题目】一个整数数组 A,找到每个元素:右边第一个比我小的下标位置,没有则用 -1 表示。

输入:[5, 2]

输出:[1, -1]

解释:因为元素 5 的右边离我最近且比我小的位置应该是 A[1],最后一个元素 2 右边没有比 2 小的元素,所以应该输出 -1。

接口:int[] findRightSmall(int[] A);

【分析】

模拟

我们首先拿一个例子来模拟。如用 [1,2,4,9,4,0,5],可以得出 [5,5,5,4,5,-1,-1]

规律

这里我们是照着题意去寻找一个右边比它小的数的下标。可以发现,A[4] = 4 及 A[5] = 0,这两个数字多次被用到。并且:

A[4] 发现有左边 A[3],A[3] 就匹配成功;

结合 A[5] = 0 的例子,我们发现它会把比它大的数都进行匹配成功,但是 A[3] 除外;

A[3] 可以认为是匹配成功之后,被 A[4]消除了。

这时可以总结出:一个数总是想与左边比它大的数进行匹配,匹配到了之后,小的数会消除掉大的数。

匹配

当你发现要解决的题目有两个特点:

  • 小的数要与大的数配对
  • 小的数会消除大的数

你的脑海里应该联想到关于单调栈的特性。下面我们看看如何利用单调栈解决这道题目。

Step 1. 首先将 A[0] = 1 的下标 0 入栈。

Step 2. 将 A[1] = 2 的下标 1 入栈。满足单调栈。

Step 3. 将 A[2] = 4 的下标 2 入栈。满足单调栈。

Step 4. 将 A[3] = 9 的下标 3 入栈。满足单调栈。

Step 5. 将 A[4] = 4 的下标 4 入栈时,不满足单调性,需要将 A[3] = 9 从栈中弹出去。下标 4 将栈中下标 3 弹出栈,记录 A[3] 右边更小的是 index = 4。

Step 6. 将 A[5] = 0 的下标 5 入栈时,不满足单调性,需要将 A[4] = 4 从栈中弹出去。下标 5 将下标 4 弹出栈,记录 A[4] 右边更小的是 index = 5。A[5] = 0 会将栈中的下标 0, 1, 2 都弹出栈,因此也需要记录相应下标右边比其小的下标为 5,再将 A[5] = 0 的下标 5 放入栈中。

Step 7. 将 A[6] = 5 的下标 6 放入栈中。满足单调性。

Step 8. 此时,再也没有元素要入栈了,那么栈中的元素右边没有比其更小的元素。因此设置为 -1.

代码

public static int[] findRightSmall(int[] A) {
  // 结果数组
  int[] ans = new int[A.length];

  // 注意,栈中的元素记录的是下标
  Stack<Integer> t = new Stack();

  for (int i = 0; i < A.length; i++) {
    final int x = A[i];

    // 每个元素都向左遍历栈中的元素完成消除动作
    while (!t.empty() && A[t.peek()] > x) {
      // 消除的时候,记录一下被谁消除了
      ans[t.peek()] = i;

      // 消除时候,值更大的需要从栈中消失
      t.pop();
    }

    // 剩下的入栈
    t.push(i);
  }

  // 栈中剩下的元素,由于没有人能消除他们,因此,只能将结果设置为-1。

  while (!t.empty()) {
    ans[t.peek()] = -1;
    t.pop();

  }
  return ans;
}

时间复杂度:O(n)

空间复杂度:O(n)

小结

到这里我们可以得到一个有趣且非常有用的结论:数组中右边第一个比我小的元素的位置,求解用递增栈。

如果我们进一步归纳,会发现消除的时候,这里仍然是消除一个元素,保留一个元素。弹栈的时候,仍然是一直弹栈,直到满足某个条件为止。只是条件变成了直到元素大于栈顶元素。