# 栈的应用:经典算法与例题解析

[[栈]]
在理解了栈的基本概念和实现后,我们来看看它在算法中的一些经典应用。栈的“后进先出”(LIFO)特性使其成为处理具有递归结构或需要“暂存”和“回溯”状态问题的理想工具。


算法一:括号匹配

1. 算法思想

这是栈最经典的应用之一。给定一个只包含 (), [], {} 的字符串,判断字符串中的括号是否有效匹配。

有效匹配的条件:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。

解决思路:

  • 遍历字符串,当遇到一个左括号时,我们将其压入栈中。这相当于“暂存”了这个待匹配的左括号。
  • 当遇到一个右括号时,我们需要检查栈顶的左括号是否与之匹配:
    • 如果此时栈是空的,说明没有左括号与之对应,直接判定为无效。
    • 如果栈不为空,取出(pop)栈顶元素,判断这两个括号是否是同一类型。
      • 如果是,则匹配成功,继续遍历。
      • 如果不是,则判定为无效。
  • 遍历完整个字符串后,如果栈是空的,说明所有左括号都找到了对应的右括号,判定为有效。如果栈不为空,则说明有未被匹配的左括号,判定为无效。

2. 例题讲解:有效的括号 (LeetCode 20)

题目描述:
给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

示例:

  • 输入: s = "()[]{}"
  • 输出: true

  • 输入: s = "(]"

  • 输出: false

  • 输入: s = "{[]}"

  • 输出: true

代码实现 (C++):

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
45
46
47
48
49
50
51
52
#include <iostream>
#include <string>
#include <stack>
#include <unordered_map>

class Solution {
public:
bool isValid(std::string s) {
// 如果字符串长度为奇数,不可能匹配成功
if (s.length() % 2 != 0) {
return false;
}

std::stack<char> st;
// 使用哈希表存储匹配关系,方便查找
std::unordered_map<char, char> pairs = {
{')', '('},
{']', '['},
{'}', '{'}
};

for (char ch : s) {
// 如果是右括号
if (pairs.count(ch)) {
// 检查栈是否为空,或者栈顶元素是否不匹配
if (st.empty() || st.top() != pairs[ch]) {
return false;
}
st.pop(); // 匹配成功,出栈
} else {
// 如果是左括号,入栈
st.push(ch);
}
}

// 最终栈为空,则表示全部匹配成功
return st.empty();
}
};

int main() {
Solution sol;
std::string test1 = "()[]{}";
std::string test2 = "([)]";
std::string test3 = "{[]}";

std::cout << test1 << " is " << (sol.isValid(test1) ? "valid" : "invalid") << std::endl;
std::cout << test2 << " is " << (sol.isValid(test2) ? "valid" : "invalid") << std::endl;
std::cout << test3 << " is " << (sol.isValid(test3) ? "valid" : "invalid") << std::endl;

return 0;
}

算法二:最小栈

1. 算法思想

设计一个栈,除了支持常规的 push, pop, top 操作外,还需支持一个 getMin 操作,该操作能在常数时间 O(1) 内返回栈中最小的元素。

解决思路: 直接遍历整个栈来找最小值是不可取的,因为这会使 getMin 的时间复杂度变为 O(n)。

为了实现 O(1) 的查询,我们需要用空间换时间。一个常见的方法是使用两个栈

  1. data_stack:一个普通的数据栈,用于存储所有压入的元素,行为和普通栈完全一样。

  2. min_stack:一个辅助的最小栈。这个栈的栈顶永远保存着当前 data_stack 中所有元素的最小值

操作规则:

  • push(x):

    • data_stack 正常压入 x

    • 对于 min_stack

      • 如果 min_stack 为空,或者 x 小于等于 min_stack 的栈顶元素,则也将 x 压入 min_stack。这表示一个新的最小值出现了。
  • pop():

    • data_stack 正常弹出一个元素,记为 value

    • 对于 min_stack

      • 如果 value 等于 min_stack 的栈顶元素,那么 min_stack 也必须弹出一个元素。这表示当前的最小值已经被弹出了,次小值(现在的新最小值)就暴露在了栈顶。
  • getMin():

    • 直接返回 min_stack 的栈顶元素即可。

2. 例题讲解:最小栈 (LeetCode 155)

题目描述: 设计一个支持 pushpoptop 操作,并能在常数时间内检索到最小元素的栈。

代码实现 (C++):

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
45
46
47
48
49
50
#include <iostream>
#include <stack>
#include <algorithm> // for std::min

class MinStack {
private:
std::stack<int> data_stack;
std::stack<int> min_stack;

public:
MinStack() {
}

void push(int val) {
data_stack.push(val);
// 如果最小栈为空,或者新元素不大于当前最小值,则压入最小栈
if (min_stack.empty() || val <= min_stack.top()) {
min_stack.push(val);
}
}

void pop() {
// 如果弹出的元素正好是当前最小值,那么最小栈也要弹出
if (data_stack.top() == min_stack.top()) {
min_stack.pop();
}
data_stack.pop();
}

int top() {
return data_stack.top();
}

int getMin() {
return min_stack.top();
}
};

int main() {
MinStack minStack;
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
std::cout << "Min element: " << minStack.getMin() << std::endl; // 返回 -3
minStack.pop();
std::cout << "Top element: " << minStack.top() << std::endl; // 返回 0
std::cout << "Min element: " << minStack.getMin() << std::endl; // 返回 -2

return 0;
}

算法三:用栈实现队列

1. 算法思想

我们知道,栈是 LIFO (后进先出),而队列是 FIFO (先进先出)。如何用两个栈来模拟队列的行为呢?

解决思路: 使用两个栈,一个作为输入栈 (in_stack),一个作为输出栈 (out_stack)

  • 入队 (Enqueue / push):

    • 将所有新元素直接压入 in_stack
  • 出队 (Dequeue / pop):

    • 检查 out_stack 是否为空。

      • 如果不为空,直接从 out_stack 弹出一个元素。因为 out_stack 里的元素顺序已经是正确的(先进先出)。

      • 如果为空,则需要将 in_stack 的所有元素依次弹出,并压入 out_stack。这个过程会将 in_stack 中“后进先出”的顺序颠倒过来,从而在 out_stack 中形成“先进先出”的顺序。完成这个“倒数据”的操作后,再从 out_stack 弹出一个元素。

      • 如果两个栈都为空,则队列为空。

2. 例题讲解:用栈实现队列 (LeetCode 232)

题目描述: 请你仅使用两个栈实现一个先入先出(FIFO)队列。队列应当支持一般队列的所有功能(push, pop, peek, empty)。

代码实现 (C++):

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <iostream>
#include <stack>

class MyQueue {
private:
std::stack<int> in_stack;
std::stack<int> out_stack;

// 将输入栈的元素转移到输出栈
void transfer() {
while (!in_stack.empty()) {
out_stack.push(in_stack.top());
in_stack.pop();
}
}

public:
MyQueue() {
}

// 入队
void push(int x) {
in_stack.push(x);
}

// 出队
int pop() {
if (out_stack.empty()) {
transfer();
}
int val = out_stack.top();
out_stack.pop();
return val;
}

// 查看队首元素
int peek() {
if (out_stack.empty()) {
transfer();
}
return out_stack.top();
}

// 判空
bool empty() {
return in_stack.empty() && out_stack.empty();
}
};


int main() {
MyQueue myQueue;
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2]

std::cout << "Peek: " << myQueue.peek() << std::endl; // return 1
std::cout << "Pop: " << myQueue.pop() << std::endl; // return 1, queue is [2]
std::cout << "Is empty? " << (myQueue.empty() ? "Yes" : "No") << std::endl; // return false

return 0;
}