Skip to content

栈 && 单调栈

栈是满足后进先出的线性数据结构,即 LIFO 表

简易的数组栈

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#define MAX_SIZE 10000

// 显式使用栈顶指针的写法
// 初始化
int st[MAX_LEN];
int top = -1;       // top = -1 表示为空栈
// 压栈
st[++top] = val;    // cin >> st[++top];
// 弹栈
if(top >= 0) --top;
// 检测是否空栈
bool empty = (top < 0);
// 取栈顶元素
int top_val = st[top];
// 清空栈
top = -1;

// 隐式使用栈顶指针的写法
// 初始化,这里使用 st[0] (即 *st) 代表栈中元素数量,同时也是栈顶下标
// 也就是说 st[0] = top,栈为 1-index,*st = 0 表示为空栈
int st[MAX_LEN];
// 压栈
st[++*st] = var1;
// 弹栈 
if (*st) --*st;
// 检测是否空栈
bool empty = (*st <= 0);
// 取栈顶元素
int top_val = st[*st];
// 清空栈
*st = 0;

简易的封装栈

省去了栈溢出的检测,没有使用动态大小栈

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#define MAX_SIZE 10000

class Stack {
private:
    int data[MAX_SIZE];
    int top;

public:
    // 构造函数 && 析构函数
    Stack() { top = -1; }
    ~Stack(){};

    // 压栈
    void push(int value) {
        data[++top] = value;
    }

    // 弹栈
    void pop() {
        if (!empty())
            --top;
    }

    // 检测是否空栈
    bool empty() {
        return top == -1;
    }

    // 取栈顶元素(引用)
    int& top() {
        if (!empty())
            return data[top];
    }

    // 清空栈
    void clear() {
        top = -1;
    }

    // 获取栈大小
    int size() {
        return top+1;
    }
};

STL <stack>

支持的函数/操作有:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 具体含义参照封装实现
st.push(val);
st.pop();
st.empty();
st.top();
// 没有 clear 操作
st.size();

st1 = st2 // 拷贝操作
stack<int> st2 (st1); // 拷贝初始化

单调栈

单调栈的特性在于,在插入新的元素前,可能需要弹出部分元素以满足栈元素的单调性:

比如单调递增栈 [1 2 3 4 5] <-top 在插入元素 3 时,需要先弹出 4 5 再插入 3,确保插入后栈满足单调性 [1 2 3 3] <-top

以下是实现:

1
2
3
4
5
6
7
8
9
vector<int> nums = {...};
stack<int> st;
for (int i = 0; i < n; i++) {
    while (!st.empty() && nums[i] > st.top()) {
        // int val = st.top()
        st.pop();
    }
    st.push(nums[i]);
}

在实际运用中,我们有时不会将数值存入单调栈,而是将下标存入单调栈(在单调性比较时会比较数值),因为下标记录了数组栈中每个值的位置,这一额外信息在某些题目中非常重要

Examples

这里举三个具体的例子,其中最后一个例子并不是典型的单调栈,但是具有单调栈的思想:

Luogu P5788 【模板】单调栈

给出项数为 $n$ 的整数数列 $a_{1 \dots n}$。

定义函数 $f(i)$ 代表数列中第 $i$ 个元素之后第一个大于 $a_i$ 的元素的下标,即 $f(i) = \min _{i < j \leq n, a_j > a_i} { j }$。若不存在,则 $f(i)=0$。

试求出 $f(1\dots n)$。

这个例子是单调栈模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void solve() {
    int n; cin >> n;
    vector<int> a(n + 1);  // 1-index
    vector<int> f(n + 1, 0);
    stack<int> st;  // 单调栈,题目已经非常明确为存储下标

    for (int i = 1; i <= n; i++) cin >> a[i];

    for (int i = 1; i <= n; i++) {
        // 当 当前元素 大于 栈顶元素 时,说明找到了栈顶元素的下一个更大元素
        while (!st.empty() && a[i] > a[st.top()]) {
            f[st.top()] = i;  // 记录结果
            st.pop();
        }
        st.push(i);  // 将当前下标入栈
    }

    // 栈中剩余的元素都没有右边更大的元素,f[i] 保持为 0
    // 不需要清栈操作
    for (int i = 1; i <= n; i++) cout << f[i] << " ";
}


LeetCode 84. 柱状图中最大的矩形

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

这个例子中,由于我们不能保证一轮扫描后栈能够被清空,需要为单调栈构造结束标识符 / 对遗留在栈中的元素进行清理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        // 一个矩形的确定方式是:选定一个柱子作为固定高度,尝试向两边延伸宽度,宽乘高得到面积
        // 考虑维护一个由高度值决定的单调递增栈,栈本身存储下标值:
        // 对于栈中的每一个下标值,其左侧的值就是 “左边界下标 l”
        // 当一个下标被弹出时,使它被弹出的下标就是 “右边界下标 r”
        // 对每一个高为 h 的柱子,其向两边延伸的最大宽度为 (r-l-1)
        // (为了确保每根柱子都被弹出,最后面要加一个高度为 0 的柱子作为结束标识符
        // 或者在一轮扫描结束后加上额外的清空栈操作)
        int ans = 0;
        heights.push_back(0);
        stack<int> st; st.push(0);
        for(int i = 1; i < heights.size(); i++){
            while (!st.empty() && heights[i] < heights[st.top()]){
                int h = heights[st.top()];
                st.pop();
                // 注意空栈的特殊处理
                int width = i - (st.empty() ? -1 : st.top()) - 1;
                ans = max(ans, h * width);
            }
            st.push(i);
        }

        // 如果不加上高度为 0 的柱子
        // while (!st.empty()) {
        //     int h = heights[st.top()];
        //     st.pop();
        //     int width = heights.size() - (st.empty() ? 0 : st.top() + 1);
        //     ans = max(ans, h * width);
        // }

        return ans;
    }
};


Luogu P1175 表达式的转换

给定一个仅包含 0123456789+-*/^() 的中缀表达式,将其转换为后缀表达式后输出,并且进一步输出计算后缀表达式的过程,要求输出的每一个数据间都留一个空格。

规定 / 为整除,乘方计算 ^ 为右结合运算且幂次不为负,表达式中的数字都是一位数,不出现 2*-3 的形式

比如:8-(3+2*6)/5+4 这个输入的正确输出为:

1
2
3
4
5
6
8 3 2 6 * + 5 / - 4 + 
8 3 12 + 5 / - 4 + 
8 15 5 / - 4 + 
8 3 - 4 + 
5 4 + 
9

这道题分为两步:

  1. 中缀表达式转后缀表达式(⭐)
  2. 后缀表达式计算

对于第一步转换,我们准备一个存运算符的 stack 和一个存后缀表达式的 vector ,当遇到数字的时候直接将数字存入 vector,否则将符号插入 stack

在符号入栈时,为了满足运算顺序,我们要为每种运算定义优先级,常规符号入栈时(括号属于特殊运算),结合单调栈的思想,维护一个优先级不严格单调递减的栈:

注意所有的元素都入栈之后,需要像上一题一样,使用右括号清空栈,或者加上清空栈的函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 优先级定义
int pr(char op) {
    switch (op) {
        case '+':
        case '-': return 1;
        case '*':
        case '/': return 2;
        case '^': return 3;
        default: return -1;
    }
}

// 这里是插入非括号运算符的逻辑,很符合单调栈的模式
if (isOperator(c)) {
    while (!st.empty() && isOperator(st.top())) {
        char top = st.top();
        // 若栈顶运算符优先级高于当前运算符
        // 或同级但左结合,则先输出(注意左右结合符号在此处的逻辑不同)
        if ((pr(top) > pr(c)) || (pr(top) == pr(c) && !isRight(c))) {
            output.push_back(top);  // 弹出栈顶符号,存入答案数组
            st.pop();
        } else break;
    }
    st.push(c);
}

完整的 sol 不放出了,另外对于第二问,维护一个数字栈和一个符号栈,每次弹出两个数字一个符号计算后插入数字栈,每一次操作输出一次结果,直到最终数字栈只有一个元素,符号栈清空