techniques
双栈法
“双栈法”(Two Stacks)是计算机科学中极其经典且优雅的技巧。它的核心思想通常可以归结为两点:“负负得正(顺序反转)” 或 “状态隔离(主辅分离)”。
以下是双栈法最常见的五种适用场景以及具体的使用方法:
1. 场景一:二叉树的之字形(ZigZag)层序遍历
-
适用情况: 当你需要按层遍历树或图,且相邻两层的遍历方向完全相反时。
-
为什么用双栈: 栈的特性是后进先出 (LIFO),它天生自带“反转”属性。如果你把一层节点压入栈,弹出来的顺序就是反的。我们需要两层反转,所以需要两个栈。
void zigzagPrint(tree *a){
if(!a) return;
push(a, &S2);
int isFirst = 1;
while(!emptyS(&S1) || !emptyS(&S2)){
while(!emptyS(&S2)){
tree *curr = pop(&S2);
if(!isFirst) printf(" ");
printf("%d", curr->element);
isFirst = 0;
if(curr->right) push(curr->right, &S1);
if(curr->left) push(curr->left, &S1);
}
while(!emptyS(&S1)){
tree *curr = pop(&S1);
if(!isFirst) printf(" ");
printf("%d", curr->element);
isFirst = 0;
if(curr->left) push(curr->left, &S2);
if(curr->right) push(curr->right, &S2);
}
}
}
2. 场景二:用两个栈实现一个队列 (Queue via Stacks)
-
适用情况: 在只有栈(LIFO)这种数据结构的情况下,如何模拟出队列(FIFO, 先进先出)的行为。
-
使用方法:
-
定义
StackIn(输入栈) 和StackOut(输出栈)。 -
入队 (Enqueue): 永远直接把数据压入
StackIn。 -
出队 (Dequeue):
-
如果
StackOut里面有元素,直接从StackOut弹出顶部元素。 -
如果
StackOut是空的,就把StackIn里的所有元素依次弹出,并压入StackOut中。然后再从StackOut弹出顶部元素。
-
-
3. 场景三:表达式求值 / 简易计算器 (Expression Evaluation)
-
适用情况: 解析带有括号和不同优先级运算符的数学表达式(如
3 + 5 * (2 - 8))。 -
为什么用双栈: 数字和运算符的处理逻辑不同,且运算符存在优先级(乘除高于加减),需要分离状态并暂存。
-
使用方法:
-
操作数栈 (Operand Stack): 专存数字。
-
运算符栈 (Operator Stack): 专存
+ - * / ( )。 -
遍历表达式:遇到数字直接进数字栈;遇到运算符,与运算符栈顶比较优先级。如果当前优先级低或相等,就从数字栈弹出两个数,从运算符栈弹出一个符号,计算结果后重新压回数字栈。
-
4. 场景四:包含 Min/Max 函数的栈 (Min Stack)
-
适用情况: 设计一个栈,除了具备正常的
push和pop,还需要能在 \(O(1)\) 的时间复杂度内获取栈内的最小值或最大值。 -
为什么用双栈: 普通栈无法记住“历史最小值”。我们需要一个辅助栈来同步记录每个状态下的极值。
-
使用方法:
-
主栈 (Main Stack): 正常存取所有数据。
-
辅助栈 (Min Stack): 存对应时刻的最小值。
-
当新元素压入主栈时:如果新元素 \(\le\) 辅助栈顶元素,也把它压入辅助栈。
-
当弹出元素时:如果弹出的元素等于辅助栈顶元素,辅助栈也跟着弹出一个。
-
5. 场景五:软件的 撤销/重做 (Undo/Redo) 或 浏览器前进/后退
-
适用情况: 需要在一个线性的历史记录中来回穿梭。
-
为什么用双栈: 历史是“过去”,未来是“重做”。当前状态刚好夹在两个时间轴中间。
-
使用方法:
-
撤销栈 (Undo Stack): 每做一次新操作,把状态压入此栈。点击“撤销”时,把栈顶弹出。
-
重做栈 (Redo Stack): 点击“撤销”时,把弹出的状态压入这个重做栈。如果此时做了一个新操作,重做栈必须全部清空(因为未来改变了)。
-