数据结构之栈与队列的应用

选择题

01. 栈的应用不包括( )

  • 选项
    • A. 递归
    • B. 进制转换
    • C. 迷宫求解
    • D. 缓冲区
  • 答案:D. 缓冲区
  • 解释:缓冲区通常使用队列实现,而不是栈。选项 A、B、C 都是栈的典型应用。

02. 表达式 \(a*(b+c)-d\) 的后缀表达式是

  • 选项
    • A. \(abc*+d-\)
    • B. \(ab+c*d-\)
    • C. \(abc+*d-\)
    • D. \(abc+d*-\)
  • 答案:C. \(abc+*d-\)
  • 解释:后缀表达式是指操作符在操作数之后的表达形式。在将中缀表达式转换为后缀表达式时,可以按照操作符的优先级进行转换。最终得到的后缀表达式为 \(abc+*d-\)

03. 下面( )用到了队列。

  • 选项
    • A. 括号匹配
    • B. 迷宫求解
    • C. 页面替换算法
    • D. 递归
  • 答案:C. 页面替换算法
  • 解释:页面替换算法中使用队列(如 FIFO)来管理页面。其余选项 A、B 和 D 都是栈的典型应用。

04. 利用栈求表达式的值时,设立运算数栈 OPEN。假设 OPEN 只有两个存储单元,则在下列表达式中,不会发生溢出的是( )。

  • 选项
    • A. \(A-B*(C-D)\)
    • B. \((A-B)*C-D\)
    • C. \((A-B*C)-D\)
    • D. \((A-B)*(C-D)\)
  • 答案:B. \((A-B)*C-D\)
  • 解释:在求值过程中,栈的深度会根据操作符和操作数的入栈情况变化。在选项 B 中,运算符的计算使栈深度维持在一个可接受的范围内,而选项 A、C 和 D 的栈深度可能达到 4,导致溢出。

05. 执行完下列语句段后,i 的值为( )。

1
2
3
4
5
int f(int x) {
return ((x > 0) ? x * f(x - 1) : 2);
}
int i;
i = f(f(1));
  • 选项
    • A. 2
    • B. 4
    • C. 无限递归
    • D. 3
  • 答案:B. 4
  • 解释:根据递归函数定义:
    • \(f(0) = 2\)
    • \(f(1) = 1 * f(0) = 2\)
    • \(f(f(1)) = f(2) = 2 * f(1) = 2 * 2 = 4\)

因此最终结果是 4。

06. 对于一个问题的递归算法求解和其相对应的非递归算法求解,()。

  • 选项
    • A. 递归算法通常效率高一些
    • B. 非递归算法通常效率高一些
    • C. 两者相同
    • D. 无法比较
  • 答案:B. 非递归算法通常效率高一些
  • 解释:通常情况下,递归算法在计算机实际执行的过程中包含很多的重复计算和额外的函数调用开销,因此效率会低于相应的非递归算法。

07. 执行函数时,其局部变量一般采用()进行存储。

  • 选项
    • A. 树形结构
    • B. 静态链表
    • C. 栈结构
    • D. 队列结构
  • 答案:C. 栈结构
  • 解释:在执行函数时,系统会为调用者构造一个由参数表和返回地址组成的活动记录,并将记录压入系统提供的栈中。如果被调用函数有局部变量,也会被压入栈中。

08. 执行()操作时,需要使用队列作为辅助存储空间。

  • 选项
    • A. 查找散列(哈希)表
    • B. 广度优先搜索图
    • C. 前序(根)遍历二叉树
    • D. 深度优先搜索图
  • 答案:B. 广度优先搜索图
  • 解释:图的广度优先搜索(BFS)类似于树的层序遍历,都需要借助队列作为辅助存储空间,以保证按照层次逐层访问节点。

09. 下列说法中,正确的是()。

  • 选项
    • A. 消除递归不一定需要使用栈
    • B. 对同一输入序列进行两组不同的合法入栈和出栈组合操作,所得的输出序列也一定相同
    • C. 通常使用队列来处理函数或过程调用
    • D. 队列和栈都是运算受限的线性表,只允许在表的两端进行运算
  • 答案:A. 消除递归不一定需要使用栈
  • 解释:消除递归可以使用不同的方法(如尾递归、迭代等),不一定需要使用栈。而其他选项都是不正确的描述。

10. 为解决计算机主机与打印机之间速度不匹配的问题,通常设置一个打印数据缓冲区,主机将要输出的数据依次写入该缓冲区,而打印机则依次从该缓冲区中取出数据。该缓冲区的逻辑结构应该是()。

  • 选项
    • A. 栈
    • B. 队列
    • C. 树
    • D. 图
  • 答案:B. 队列
  • 解释:在提取数据时必须保持原来数据的顺序,因此缓冲区的特性是先进先出(FIFO),答案选 B。

11. 已知操作符包括+、-、*、/、(和)。将中缀表达式a+b-a*((c+d)/e-f)+g转换为等价的后缀表达式时,用栈来存放暂时还不能确定运算次序的操作符。栈初始时为空时,转换过程中同时保存在栈中的操作符的最大个数是()。

A. 5
B. 7
C. 8
D. 11

答案:A
解析: 在将中缀表达式转换为后缀表达式的过程中,操作符的优先级影响栈的最大深度。经过详细分析,我们可以得出,最多同时在栈中的操作符个数为5。

image-20241006212159427

12. 假设栈初始为空,将中缀表达式a/b+(c*d-e*f)/g 转换为等价的后缀表达式的过程中,当扫描到f时,栈中的元素依次是()。

A. +(*- B. +(-* C./+(*-
D./+-*

答案:B
解析:

image-20241006212029167
image-20241006212056770

13. 已知程序如下:

1
2
3
4
5
6
int S(int n) {
return (n <= 0) ? 0 : s(n - 1) + n;
}
void main() {
cout << s(1);
}

程序运行时使用栈来保存调用过程的信息,自栈底到栈顶保存的信息依次对应的是()。
A. main() → S(1) → S(0)
B. S(0) → S(1) → main()
C. main() → S(0) → S(1)
D. S(1) → S(0) → main()

答案:A
解析: 在递归调用时,栈遵循先进后出的特性。函数调用顺序为 main() 调用 S(1),然后 S(1) 调用 S(0),因此栈从底到顶的顺序是 main() → S(1) → S(0)


一、括号匹配问题

问题描述

给定一个包含圆括号 (、方括号 [ 和花括号 { 的字符串,编写一个算法来判别字符串中的括号是否配对。字符 0 作为字符串的结束符。

解答

以下是使用栈结构实现括号匹配的算法。

算法思路

  1. 初始化栈:使用栈来存储遇到的左括号。
  2. 遍历字符串:逐个检查字符串中的字符。
  3. 左括号入栈:遇到左括号时,将其压入栈中。
  4. 右括号匹配:遇到右括号时,检查栈顶元素是否为相应的左括号:
    • 如果是,弹出栈顶元素。
    • 如果不是,则匹配失败。
  5. 最终检查:遍历完字符串后,如果栈不为空,则表示括号不匹配。

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>

bool BracketsCheck(const char* str) {
std::stack<char> S; // 初始化栈
int i = 0;

while (str[i] != '0') { // 以字符 '0' 作为结束符
switch (str[i]) {
case '(':
S.push('(');
break;
case '[':
S.push('[');
break;
case '{':
S.push('{');
break;
case ')':
if (S.empty() || S.top() != '(') return false;
S.pop();
break;
case ']':
if (S.empty() || S.top() != '[') return false;
S.pop();
break;
case '}':
if (S.empty() || S.top() != '{') return false;
S.pop();
break;
default:
break; // 忽略其他字符
}
i++;
}

if (!S.empty()) {
std::cout << "括号不匹配\n";
return false;
} else {
std::cout << "括号匹配\n";
return true;
}
}

int main() {
const char* expression = "{[()]}0"; // 示例输入
BracketsCheck(expression);
return 0;
}

解释

  • 使用一个栈来保存左括号,当遇到右括号时检查栈顶的元素是否为相应的左括号。
  • 如果所有括号都匹配且栈在结束时为空,则输出“括号匹配”;否则输出“括号不匹配”。

二、火车调度问题

问题描述

在一个火车调度站,车厢有硬座车厢(H)和软座车厢(S)。所有车厢必须通过一个栈道进行调度。请编写一个算法,输出对这 n 节车厢进行调度的操作序列,以确保所有的软座车厢都被调整到硬座车厢之前。

解答

以下是通过栈实现车厢调度的算法。

算法思路

  1. 初始化栈:用于存储硬座车厢(H)。
  2. 遍历车厢:依次检查每节车厢。
    • 如果是硬座车厢(H),则将其压入栈中。
    • 如果是软座车厢(S),则直接排出到结果序列中。
  3. 弹出栈中硬座车厢:在遍历完所有车厢后,将栈中的硬座车厢依次弹出,并排到结果序列的末尾。

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
#include <iostream>
#include <stack>
#include <cstring>

void TrainArrange(const char* train) {
std::stack<char> s; // 用栈存储硬座车厢
char result[100]; // 存放调度后的结果
char* p = result; // 结果指针
const char* q = train; // 输入车厢指针

while (*q) {
if (*q == 'H') {
s.push('H'); // 硬座车厢入栈
} else if (*q == 'S') {
*p++ = 'S'; // 软座车厢直接排出
}
q++;
}

// 弹出栈中的硬座车厢
while (!s.empty()) {
*p++ = s.top();
s.pop();
}

*p = '\0'; // 结束结果字符串
std::cout << "调度结果: " << result << std::endl;
}

int main() {
const char* train = "HSHSH0"; // 示例输入
TrainArrange(train);
return 0;
}

解释

  • 该算法首先将所有硬座车厢(H)压入栈中,软座车厢(S)直接放入结果序列。
  • 在完成遍历后,将栈中的硬座车厢按顺序弹出,形成最终的调度序列。
  • 该结果序列保证了所有软座车厢在硬座车厢之前。

我们来逐步解决这两个问题:第一个是利用栈实现递归函数的非递归计算,第二个是模拟汽车渡船的管理系统。


三. 利用栈实现递归函数的非递归计算

问题描述

给定一个递归函数:

  • $ P(0, x) = 1 $
  • $ P(1, x) = 2x $
  • $ P(n, x) = 2x P(n-1, x) - 2(n-1) P(n-2, x) $ (当 $ n > 1 $)

设计一个算法使用栈来实现这个递归函数的非递归计算。

解答

我们可以利用栈保存当前的 n 值和对应的 P(n, x) 值。

算法思路

  1. 设置栈:用于存储 $ n $ 和对应的 $ P(n, x) $ 值。
  2. 初始条件:当 $ n = 0 $ 和 $ n = 1 $ 时的初始值分别为 1 和 $ 2x $。
  3. 入栈操作:从 $ n $ 开始,依次将 $ n $ 入栈,直到 $ n = 2 $。
  4. 出栈并计算:每次出栈时,根据栈顶元素的 $ n $ 值计算对应的 $ P(n, x) $ 值,并更新上一个结果。

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
#include <iostream>
#include <stack>

struct StackElement {
int n; // 保存 n
double val; // 保存 P(n, x) 的值
};

double P(int n, double x) {
std::stack<StackElement> s; // 创建栈
double fv1 = 1, fv2 = 2 * x; // P(0, x) 和 P(1, x)

for (int i = n; i >= 2; i--) {
StackElement elem;
elem.n = i;
s.push(elem); // 入栈
}

while (!s.empty()) {
StackElement elem = s.top();
s.pop();

// 计算 P(n, x)
elem.val = 2 * x * fv2 - 2 * (elem.n - 1) * fv1;
fv1 = fv2; // 更新 fv1
fv2 = elem.val; // 更新 fv2
}

if (n == 0) {
return fv1; // 返回 P(0, x)
}
return fv2; // 返回 P(n, x)
}

int main() {
int n = 5; // 示例 n
double x = 3.0; // 示例 x
std::cout << "P(" << n << ", " << x << ") = " << P(n, x) << std::endl;
return 0;
}

解释

  • 栈结构:用栈存储 n 值,按从大到小的顺序入栈。
  • 出栈计算:每次出栈时计算对应的 $ P(n, x) $,通过更新 fv1fv2 来得到最终结果。

四. 汽车渡船管理系统

问题描述

在一个汽车渡船口,渡船每次能载10辆车。车辆分为客车和货车,遵循如下规定:

  • 同类车先到先上船。
  • 客车先于货车上船,每上4辆客车后才能放上1辆货车。
  • 若等待客车不足4辆,则用货车补齐。
  • 若无货车等待,允许客车都上船。

解答

设计一个算法模拟渡口管理,确保车辆按照规定上船。

算法思路

  1. 设置队列:使用两个队列分别管理客车(q1)和货车(q2)。
  2. 控制上船数量:每次尝试上船最多10辆车,优先选择客车,按照规定的顺序上船。
  3. 处理逻辑
    • 如果客车队列有车且小于4辆,则优先将客车上船。
    • 当客车数量达到4辆时,可以放货车。
    • 如果客车队列空,直接放货车。

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
#include <iostream>
#include <queue>

void manager(std::queue<int>& q1, std::queue<int>& q2) {
int j = 0; // 总车辆数
int i = 0; // 计数器,记录已上船客车数

while (j < 10) {
// 情况1:有足够的客车上船
if (!q1.empty() && i < 4) {
int car = q1.front(); // 从客车队列出队
q1.pop();
// 上船
std::cout << "客车 " << car << " 上船" << std::endl;
i++;
j++;
}
// 情况2:上完客车后放货车
else if (i >= 4 && !q2.empty()) {
int car = q2.front(); // 从货车队列出队
q2.pop();
// 上船
std::cout << "货车 " << car << " 上船" << std::endl;
j++;
i = 0; // 重置计数器
}
// 情况3:客车不足4辆,用货车补齐
else if (i < 4 && !q2.empty()) {
int car = q2.front(); // 从货车队列出队
q2.pop();
// 上船
std::cout << "货车 " << car << " 上船" << std::endl;
j++;
i = 0; // 重置计数器
}
// 情况4:都没有车
else {
std::cout << "没有车可以上船,渡船空驶。" << std::endl;
break;
}
}
}

int main() {
std::queue<int> q1; // 客车队列
std::queue<int> q2; // 货车队列

// 初始化客车和货车队列
for (int i = 1; i <= 5; i++) {
q1.push(i); // 加入5辆客车
}
for (int i = 1; i <= 5; i++) {
q2.push(i); // 加入5辆货车
}

manager(q1, q2); // 调用管理函数
return 0;
}

解释

  • 队列结构:客车和货车使用两个队列分别管理,确保同类车先到先上。
  • 上船逻辑:根据条件判断,决定哪类车辆上船,遵循规定的优先级。
  • 输出信息:在每次车辆上船时输出车辆信息,以便跟踪渡船的状态。