3. 栈与队列
Chinese-language Computer Science 408 note: 3. 栈与队列. Original source available where applicable.
On this page
This note is currently available in Chinese. The original wording, formulas and diagrams are preserved to avoid an unverified technical translation.
ABSTRACT · 本节范围 本笔记逐题覆盖原 PDF 的 5 道代码题:空栈辅助队列逆置、带头单链表中心对称、两栈共享数组、圆括号匹配、
S/H序列重排。所有实现统一采用 C++17,可直接编译;原稿中的边界与语法错误已在对应位置订正。
0. 公共约定
#include <array>
#include <cstddef>
#include <queue>
#include <stack>
#include <string>
#include <string_view>
struct LNode {
char data{};
LNode* next = nullptr;
};
- 栈顶元素通过
top()读取,pop()只负责删除;队头元素通过front()读取,pop()负责出队。 - 除共享栈实现题外,优先使用抽象栈、队列操作,不把底层数组下标混入算法逻辑。
- 设待处理元素数为 $n$。若题目给定“辅助栈为空”,算法结束时也应恢复为空。
1. 用空栈逆置队列
1.1 题目
队列 $Q$ 中已有若干元素,栈 $S$ 为空。编写算法,利用 $S$ 将 $Q$ 中的元素逆置。
1.2 核心思路
设队头到队尾依次为
$$ a_1,a_2,\ldots,a_n. $$
先依次出队并压栈,栈顶到栈底便为 $a_n,a_{n-1},\ldots,a_1$;再依次出栈并入队,队列顺序即被逆置。
1.3 可运行实现
void reverseQueue(std::queue<int>& q, std::stack<int>& s) {
// 前置条件:s 为空。
while (!q.empty()) {
s.push(q.front());
q.pop();
}
while (!s.empty()) {
q.push(s.top());
s.pop();
}
}
1.4 正确性与复杂度
- 第一阶段保持不变式:已经离开队列的元素按原顺序自栈底向栈顶排列。
- 第一阶段结束后,原队尾元素位于栈顶;第二阶段按栈的后进先出次序重新入队,因此结果恰为原序列的逆序。
- 每个元素恰好入栈、出栈、入队、出队各一次,时间复杂度为 $O(n)$。
- 辅助空间为 $O(n)$;空队列和单元素队列无需特殊分支。
WARNING · 易错点 若自行实现循环队列,不能只写
front != rear就默认所有队列约定都相同;必须先明确空队列、满队列以及front、rear分别指向什么位置。
2. 判断带头单链表是否中心对称
2.1 题目
带头结点的单链表依次存放 $n$ 个字符,判断该字符序列是否中心对称,即是否为回文串。
2.2 核心思路
- 从首元结点开始遍历,求表长 $n$。
- 将前 $\lfloor n/2\rfloor$ 个字符依次压栈,此时指针位于后半部分之前。
- 若 $n$ 为奇数,跳过唯一的中间结点。
- 一边遍历后半部分,一边弹出前半部分字符比较;全部相等才是回文。
例如 abcba:栈中依次压入 a、b,跳过 c,再用栈顶 b、a 与后半段比较。
2.3 可运行实现
bool isPalindrome(const LNode* head) {
// 题设为带头结点链表;空指针也按空序列处理。
if (head == nullptr) {
return true;
}
std::size_t length = 0;
for (const LNode* p = head->next; p != nullptr; p = p->next) {
++length;
}
std::stack<char> firstHalf;
const LNode* p = head->next;
for (std::size_t i = 0; i < length / 2; ++i) {
firstHalf.push(p->data);
p = p->next;
}
if (length % 2 == 1) {
p = p->next; // 奇数长度时跳过中间结点。
}
while (p != nullptr) {
if (firstHalf.empty() || firstHalf.top() != p->data) {
return false;
}
firstHalf.pop();
p = p->next;
}
return firstHalf.empty();
}
2.4 正确性与复杂度
- 栈顶始终是“前半部分中尚未比较、且最靠近中心”的字符,恰好对应后半部分当前字符。
- 偶数长度直接比较两半;奇数长度必须跳过中点,因为中点不影响回文性。
- 求长度、压栈和比较均为线性过程,总时间 $O(n)$;栈最多保存 $\lfloor n/2\rfloor$ 个字符,辅助空间 $O(n)$。
DANGER · 原稿错误 奇数判断应为
length % 2 == 1。原稿的length * 2 == 1对非负整数长度永远不成立,因此奇数长度链表会错位比较。
3. 两栈共享一个顺序存储区
3.1 题目
两个栈 $s_0,s_1$ 共用数组 data[0...MAX_SIZE-1]。$s_0$ 从低地址向高地址增长,$s_1$ 从高地址向低地址增长,设计入栈和出栈操作。
3.2 状态与判定
constexpr int MAX_SIZE = 100;
struct SharedStack {
std::array<int, MAX_SIZE> data{};
int top[2] = {-1, MAX_SIZE};
};
| 状态 | 栈 0 | 栈 1 |
|---|---|---|
| 初始栈顶 | top[0] = -1 |
top[1] = MAX_SIZE |
| 入栈方向 | ++top[0] |
--top[1] |
| 出栈方向 | top[0]-- |
top[1]++ |
| 判空 | top[0] == -1 |
top[1] == MAX_SIZE |
两栈共用唯一的空闲区,因此统一判满条件为
$$ top[0]+1=top[1]. $$
3.3 入栈与出栈
bool push(SharedStack& s, int which, int value) {
if (which < 0 || which > 1) {
return false;
}
if (s.top[0] + 1 == s.top[1]) {
return false; // 整个共享数组已满。
}
if (which == 0) {
s.data[++s.top[0]] = value;
} else {
s.data[--s.top[1]] = value;
}
return true;
}
bool pop(SharedStack& s, int which, int& value) {
if (which < 0 || which > 1) {
return false;
}
if (which == 0) {
if (s.top[0] == -1) {
return false;
}
value = s.data[s.top[0]--];
} else {
if (s.top[1] == MAX_SIZE) {
return false;
}
value = s.data[s.top[1]++];
}
return true;
}
这里用布尔返回值区分“操作成功”和“栈满、栈空或编号非法”,避免把 -1 同时当作合法数据和错误标志。
3.4 正确性与复杂度
- 两个栈顶始终从数组两端向中间逼近;区间
top[0]+1 ... top[1]-1是全部剩余空间。 - 当两个栈顶相邻时,剩余空间长度为 0,故必须拒绝任一栈继续入栈。
- 入栈和出栈都只进行常数次判断、赋值和下标移动,时间复杂度均为 $O(1)$;结构本身占用 $O(\text{MAX_SIZE})$。
DANGER · 原稿错误 原稿的两条入栈语句只计算了数组位置,却没有执行
= value;栈 1 的判空也不应写成top[1] != -MAX_SIZE。正确空状态是初始化值top[1] == MAX_SIZE。
4. 判断圆括号是否匹配
4.1 题目
判断一个表达式中的圆括号是否配对。表达式中的非括号字符不参与匹配。
4.2 可运行实现
bool parenthesesMatched(std::string_view expression) {
std::stack<char> leftParentheses;
for (char ch : expression) {
if (ch == '(') {
leftParentheses.push(ch);
} else if (ch == ')') {
if (leftParentheses.empty()) {
return false; // 右括号没有可匹配的左括号。
}
leftParentheses.pop();
}
}
return leftParentheses.empty();
}
4.3 两种失败情形
- 扫描过程中遇到右括号而栈为空,例如
a+b)。 - 扫描结束后栈仍非空,例如
(a+b。
算法对每个字符只访问一次,时间复杂度为 $O(n)$;最坏情况下全部是左括号,辅助空间为 $O(n)$。
TIP · 扩展到三类括号 若还包含
[]和{},遇到右括号时不仅要先判空,还要检查栈顶左括号类型是否对应;任何一次类型不符都应立即返回false。
DANGER · 原稿错误 左圆括号应压入
'(',不能压入'C';处理右括号前必须先判空。原稿还存在return false'等语法错误。
5. 将所有 S 移到 H 之前
5.1 题目
给定只含字符 S 和 H 的序列 HSSHHHS,利用栈将所有 S 移到所有 H 之前,得到 SSSHHHH。
5.2 核心思路
- 从左到右扫描:
S直接追加到结果,H压入辅助栈。 - 扫描结束后,将栈中的所有
H依次弹出并追加到结果。 - 由于题目中的
H完全相同,出栈造成的逆序不可见;若元素还带有不同载荷,这一方法不保证H组内部稳定。
5.3 可运行实现
std::string moveSBeforeH(std::string_view input) {
std::stack<char> hStack;
std::string result;
result.reserve(input.size());
for (char ch : input) {
if (ch == 'S') {
result.push_back(ch);
} else if (ch == 'H') {
hStack.push(ch);
}
}
while (!hStack.empty()) {
result.push_back(hStack.top());
hStack.pop();
}
return result;
}
5.4 正确性与复杂度
- 扫描结束时,结果串恰含输入中的全部
S,栈恰含输入中的全部H。 - 清空栈后,结果串前段全是
S、后段全是H,且两类字符的数量均与输入相同。 - 每个字符至多入栈、出栈或追加一次,时间复杂度 $O(n)$,辅助空间 $O(n)$。
WARNING · C++ 字符串边界 对空
std::string直接写result[j++] = ch会越界;应使用push_back或先调整字符串长度。原稿还把已初始化的栈变量名和类型名混用,均已修正。
6. 源稿纠错汇总
| 题号 | 原稿问题 | 正确处理 |
|---|---|---|
| 1 | 直接操作数组时未给出完整容量与下标约定 | 使用抽象队列、栈操作,并声明辅助栈初始为空 |
| 2 | length * 2 == 1 永远不成立;弹栈变量名不一致 |
用 length % 2 == 1,统一使用同一栈对象 |
| 3 | 栈顶初始化不完整;压栈未赋值;栈 1 判空错误 | {-1, MAX_SIZE};写入 value;用 top[1] == MAX_SIZE 判空 |
| 4 | 左括号压成 'C';空栈直接弹出;返回语句有语法错误 |
压入 '(',右括号先判空,扫描结束再判栈空 |
| 5 | 对空字符串按下标写入会越界;栈变量名混乱 | 用 push_back,统一变量名,并完整清空辅助栈 |
相关知识
7. 本节复盘
- 队列逆置依赖“队列保持次序、栈反转次序”的组合。
- 回文判断要区分奇偶长度:奇数跳过中点,偶数直接比较两半。
- 共享栈只有一个总判满条件,但两个栈各有自己的判空条件。
- 括号匹配既要防止“提前出现右括号”,又要检查“结束后仍有左括号”。
- 写代码题时必须同时交代初始化、边界条件、操作失败的表示方式、时间复杂度与空间复杂度。