在计算机科学中,栈(Stack)是一种非常重要的数据结构,它遵循"后进先出"(LIFO)的原则。栈在编程语言实现、算法设计、系统调用等方面有着广泛的应用。今天,我们将深入探讨一个关于栈的经典问题:如何验证一个给定的弹出序列是否是某个压入序列的合法弹出序列。这个问题看似简单,却蕴含着栈操作的精髓,也是许多算法面试中的常见题目。
给定两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如:
压入序列:1, 2, 3, 4, 5
弹出序列1:4, 5, 3, 2, 1 → 合法
弹出序列2:4, 3, 5, 1, 2 → 不合法
让我们先仔细阅读并分析代码:
#include
using namespace std;
void Stack(){
const int maxn=1e5+10;
int n,pushed[maxn],poped[maxn];
stack s;
cin>>n;
for(int i=0;i>pushed[i];
for(int i=0;i>poped[i];
int i=0;
for(int j=0;j>q;
while(q--) Stack();
return 0;
}
头文件与命名空间:使用了#include
包含所有标准库,并使用了using namespace std
简化代码。
Stack函数:这是核心功能实现函数。
pushed
和poped
分别存储压入和弹出序列stack
作为辅助数据结构main函数:处理多组测试用例。
算法的主要思路是模拟实际的栈操作过程:
初始化一个空栈和两个指针i,j分别指向压入序列和弹出序列的起始位置。
对于弹出序列中的每个元素poped[j]
:
poped[j]
,如果是则弹出并处理下一个弹出元素poped[j]
或压入序列耗尽如果所有弹出元素都能按上述规则处理完,则判定为合法序列。
该算法的时间复杂度为O(n),其中n是序列的长度。因为每个元素最多被压入和弹出栈各一次。
空间复杂度也是O(n),最坏情况下需要存储整个压入序列。
为了证明这个算法的正确性,我们需要从两个方面考虑:
充分性:如果算法判定为"Yes",则弹出序列确实是合法的。
必要性:如果弹出序列是合法的,算法一定会判定为"Yes"。
这个算法不仅仅是一个理论练习,它在许多实际场景中都有应用:
虽然给出的代码已经相当高效,但我们还可以考虑一些改进:
改进后的代码可能如下:
#include
#include
#include
using namespace std;
bool isPopOrderValid(const vector& pushed, const vector& popped) {
stack st;
int pushIdx = 0;
for (int num : popped) {
// 如果栈顶元素匹配当前弹出元素,直接弹出
if (!st.empty() && st.top() == num) {
st.pop();
continue;
}
// 否则从压入序列中寻找该元素
while (pushIdx < pushed.size() && pushed[pushIdx] != num) {
st.push(pushed[pushIdx++]);
}
// 如果压入序列耗尽仍未找到,返回false
if (pushIdx == pushed.size()) {
return false;
}
// 跳过找到的元素(相当于压入后立即弹出)
pushIdx++;
}
return true;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int q;
cin >> q;
while (q--) {
int n;
cin >> n;
vector pushed(n), popped(n);
for (int i = 0; i < n; ++i) cin >> pushed[i];
for (int i = 0; i < n; ++i) cin >> popped[i];
cout << (isPopOrderValid(pushed, popped) ? "Yes" : "No") << endl;
}
return 0;
}
理解这个算法后,可以尝试解决以下类似问题:
为了更好地理解这个算法,让我们通过一个具体的例子进行可视化:
压入序列:[1, 2, 3, 4, 5]
弹出序列:[4, 5, 3, 2, 1]
执行步骤:
所有元素成功弹出,序列合法。
在实现这类算法时,必须考虑各种边界条件:
从数学角度看,这个问题可以转化为栈排序问题。合法的弹出序列实际上是压入序列的一个排列,满足一定的约束条件。
对于长度为n的压入序列,合法的弹出序列的数量等于第n个卡特兰数(Catalan number):
Cₙ = (1/(n+1)) * (2n choose n)
例如:
n=1: 1种
n=2: 2种
n=3: 5种
n=4: 14种
...
这表明随着n增大,合法序列的比例迅速下降。
除了迭代解法,这个问题也可以用递归解决:
bool isPopOrderValidRecursive(const vector& pushed, const vector& popped,
int pushIdx, int popIdx, stack& st) {
if (popIdx == popped.size()) {
return true;
}
if (!st.empty() && st.top() == popped[popIdx]) {
st.pop();
return isPopOrderValidRecursive(pushed, popped, pushIdx, popIdx + 1, st);
}
while (pushIdx < pushed.size() && pushed[pushIdx] != popped[popIdx]) {
st.push(pushed[pushIdx++]);
}
if (pushIdx == pushed.size()) {
return false;
}
return isPopOrderValidRecursive(pushed, popped, pushIdx + 1, popIdx + 1, st);
}
递归解法虽然直观,但对于大规模数据可能会有栈溢出的风险,迭代解法更为实用。
为了加深理解,我们可以看看其他编程语言中的实现:
Python实现:
def is_pop_order(pushed, popped):
stack = []
push_idx = 0
for num in popped:
if stack and stack[-1] == num:
stack.pop()
continue
while push_idx < len(pushed) and pushed[push_idx] != num:
stack.append(pushed[push_idx])
push_idx += 1
if push_idx == len(pushed):
return False
push_idx += 1
return True
Java实现:
import java.util.Stack;
public class Solution {
public boolean isPopOrder(int[] pushed, int[] popped) {
Stack stack = new Stack<>();
int pushIdx = 0;
for (int num : popped) {
if (!stack.isEmpty() && stack.peek() == num) {
stack.pop();
continue;
}
while (pushIdx < pushed.length && pushed[pushIdx] != num) {
stack.push(pushed[pushIdx++]);
}
if (pushIdx == pushed.length) {
return false;
}
pushIdx++;
}
return true;
}
}
为了验证算法的效率,我们可以设计不同规模的测试用例:
实际测试表明,迭代解法在各种情况下都能保持稳定的O(n)性能。
在实现这个算法时,开发者容易犯以下错误:
这个问题还可以从以下几个角度进行扩展思考:
栈的合法弹出序列验证问题是一个经典的算法问题,它很好地展示了栈数据结构的LIFO特性。通过模拟实际的栈操作,我们可以高效地验证一个弹出序列的合法性。理解这个算法不仅有助于解决类似的问题,还能加深对栈这一基础数据结构的认识。
本文从算法实现、正确性证明、应用场景、优化改进、多语言实现等多个角度进行了深入探讨,希望能帮助读者全面理解这一问题。在实际编程和算法设计中,这类基础问题的理解和掌握是非常重要的,它们往往是解决更复杂问题的基石。