考研数据结构之栈(Stack)详解(包含真题及解析)
一、栈的基本概念
栈(Stack) 是一种后进先出(LIFO, Last In First Out) 的线性数据结构,仅允许在栈顶(Top) 进行插入(入栈,Push)和删除(出栈,Pop)操作。
核心特性:
- 插入和删除操作的时间复杂度为 (O(1))
- 常见应用:函数调用栈、表达式求值、括号匹配、递归实现等
二、栈的顺序存储结构(顺序栈)
顺序栈通过数组实现,需预先分配固定大小的连续内存空间,并用一个指针 top
标记栈顶位置。
1. 结构定义
#define MAX_SIZE 100 // 栈的最大容量
typedef struct {
int data[MAX_SIZE];
int top; // 栈顶指针(通常初始化为-1)
} SeqStack;
2. 基本操作实现
初始化栈
void InitStack(SeqStack *S) {
S->top = -1; // 空栈时top=-1
}
入栈(Push)
int Push(SeqStack *S, int value) {
if (S->top >= MAX_SIZE - 1) { // 栈满
return 0; // 入栈失败
}
S->data[++S->top] = value; // top先+1,再赋值
return 1;
}
出栈(Pop)
int Pop(SeqStack *S, int *value) {
if (S->top == -1) { // 栈空
return 0; // 出栈失败
}
*value = S->data[S->top--]; // 先取数据,再top-1
return 1;
}
获取栈顶元素(Peek)
int GetTop(SeqStack S) {
if (S.top == -1) {
printf("栈空!");
return -1;
}
return S.data[S.top];
}
优缺点:
- ✅ 优点:内存连续,访问速度快
- ❌ 缺点:容量固定,可能溢出
三、栈的链式存储结构(链栈)
链栈通过链表实现,动态分配内存,栈顶为链表的头结点。
1. 结构定义
typedef struct StackNode {
int data;
struct StackNode *next;
} StackNode;
typedef struct {
StackNode *top; // 栈顶指针
} LinkStack;
2. 基本操作实现
初始化栈
void InitLinkStack(LinkStack *S) {
S->top = NULL; // 空栈
}
入栈(Push)
void LinkPush(LinkStack *S, int value) {
StackNode *newNode = (StackNode*)malloc(sizeof(StackNode));
newNode->data = value;
newNode->next = S->top; // 新结点指向原栈顶
S->top = newNode; // 更新栈顶
}
出栈(Pop)
int LinkPop(LinkStack *S, int *value) {
if (S->top == NULL) {
return 0; // 栈空
}
StackNode *temp = S->top;
*value = temp->data;
S->top = S->top->next; // 栈顶下移
free(temp); // 释放原栈顶
return 1;
}
优缺点:
- ✅ 优点:动态扩容,无溢出风险
- ❌ 缺点:内存碎片化,访问速度稍慢
四、顺序栈 vs 链栈
特性 | 顺序栈 | 链栈 |
---|---|---|
内存分配 | 静态(固定大小) | 动态(按需分配) |
插入/删除时间复杂度 | (O(1)) | (O(1)) |
空间利用率 | 可能浪费 | 灵活高效 |
适用场景 | 数据规模已知 | 数据规模动态变化 |
五、考研高频考点
- 栈的应用:实现括号匹配、中缀转后缀表达式
- 算法设计:双栈模拟队列、最小栈问题
- 代码手写:顺序栈的入栈/出栈操作、链栈的创建与销毁
考研数据结构之栈(Stack)详解:顺序存储与链式存储
六、考研真题及解析
真题示例1:括号匹配(应用题)
题目:给定一个只包含括号的字符串(如 “()[]{}”),判断其是否有效。要求使用栈实现,并分析时间复杂度。
答案:
int isValid(char* s) {
LinkStack stack;
InitLinkStack(&stack);
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == '(' || s[i] == '[' || s[i] == '{') {
LinkPush(&stack, s[i]);
} else {
if (stack.top == NULL) return 0;
char top_char;
LinkPop(&stack, &top_char);
if ((s[i] == ')' && top_char != '(') ||
(s[i] == ']' && top_char != '[') ||
(s[i] == '}' && top_char != '{')) {
return 0;
}
}
}
return stack.top == NULL; // 栈空说明全部匹配
}
解析:
- 遍历字符串,遇到左括号则入栈
- 遇到右括号时,若栈空或不匹配则返回错误
- 最终栈空表示完全匹配
时间复杂度:(O(n)),每个元素仅入栈、出栈一次
真题示例2:中缀转后缀表达式(应用题)
题目:将中缀表达式 “a + b * c - (d + e)” 转换为后缀表达式,写出转换过程。
答案:
转换结果:a b c * + d e + -
步骤解析:
当前字符 | 操作 | 栈状态(底→顶) | 输出 |
---|---|---|---|
a | 直接输出 | 空 | a |
+ | 栈空,入栈 | + | a |
b | 直接输出 | + | a b |
* | 优先级高于栈顶’+',入栈 | + * | a b |
c | 直接输出 | + * | a b c |
- | 弹出栈顶’*‘和’+‘,’-'入栈 | - | a b c * + |
( | 直接入栈 | - ( | a b c * + |
d | 直接输出 | - ( | a b c * + d |
+ | 栈顶为’(', 入栈 | - ( + | a b c * + d |
e | 直接输出 | - ( + | a b c * + d e |
) | 弹出栈内运算符直到’(’ | - | a b c * + d e + |
结束 | 弹出栈中剩余运算符 | 空 | a b c * + d e + - |
真题示例3:双栈模拟队列(算法设计)
题目:用两个栈实现队列的入队(enqueue)和出队(dequeue)操作,说明时间复杂度。
解答:
typedef struct {
SeqStack s1; // 主栈(用于入队)
SeqStack s2; // 辅助栈(用于出队)
} MyQueue;
void enqueue(MyQueue *q, int x) {
Push(&q->s1, x); // 直接压入s1
}
int dequeue(MyQueue *q) {
if (q->s2.top == -1) {
// 将s1元素全部倒入s2
while (q->s1.top != -1) {
int val;
Pop(&q->s1, &val);
Push(&q->s2, val);
}
}
int res;
Pop(&q->s2, &res);
return res;
}
时间复杂度分析:
- 入队:(O(1))
- 出队:均摊 (O(1))(每个元素最多经历两次栈操作)
真题示例4:最小栈(编程题)
题目:设计一个支持常数时间获取最小值的栈,要求所有操作时间复杂度为 (O(1))。
解答:
typedef struct {
SeqStack data_stack; // 主栈
SeqStack min_stack; // 辅助栈(存储历史最小值)
} MinStack;
void minStackPush(MinStack *obj, int val) {
Push(&obj->data_stack, val);
if (obj->min_stack.top == -1 || val <= GetTop(obj->min_stack)) {
Push(&obj->min_stack, val);
}
}
void minStackPop(MinStack *obj) {
int val = GetTop(obj->data_stack);
Pop(&obj->data_stack, &val);
if (val == GetTop(obj->min_stack)) {
int tmp;
Pop(&obj->min_stack, &tmp);
}
}
int minStackGetMin(MinStack *obj) {
return GetTop(obj->min_stack);
}
真题示例5:出栈序列判断(选择题)
题目:若入栈序列为1,2,3,4,5,则下列哪个不可能是合法的出栈序列?
A. 3,2,5,4,1
B. 5,4,3,2,1
C. 4,5,3,2,1
D. 2,1,3,5,4
答案:C(4,5,3,2,1)
解析:
- 选项C中,4出栈后栈内剩余元素为1,2,3。此时若要5出栈,需先入栈5,但5尚未入栈,因此矛盾。
七、总结
通过真题训练可以加深对栈的特性、应用场景及代码实现的理解。重点掌握:
- 栈在括号匹配、表达式转换中的核心逻辑
- 双栈配合解决复杂问题的思路(如队列模拟、最小栈)
- 出栈序列合法性的快速判断方法(模拟栈操作)