掌握栈的后进先出特性、基本操作及顺序栈、共享栈和链式栈的实现,明确栈空、栈满与各操作的复杂度边界。

# 一、栈的定义与基本操作

栈(Stack)是只允许在一端进行插入和删除的线性表。允许操作的一端称为栈顶,另一端称为栈底。插入称为进栈或压栈,删除称为出栈或弹栈。

栈遵循后进先出(LIFO, Last In First Out)原则:最后进栈的元素最先出栈。若依次将 a,b,ca,b,c 压栈,在此期间不出栈,则出栈次序必为 c,b,ac,b,a。

1
2
3
4
flowchart BT
B[栈底:a] --> M[元素:b] --> T[栈顶:c]
P[push d] --> T
T --> O[pop 返回 c]

栈的抽象操作包括:

操作含义失败条件
InitStack初始化为空栈无
StackEmpty判断是否为空无
Push(S, x)将 xx 压入栈顶顺序栈可能栈满;链栈可能内存分配失败
Pop(S, x)删除栈顶并用 xx 返回其值栈空
GetTop(S, x)读取栈顶但不删除栈空
DestroyStack释放栈占用资源无

只要栈顶位置已知,Push、Pop、GetTop 均为 O(1)O(1);栈不支持按逻辑位置随机访问。

# 二、顺序栈

# 1. 栈顶指针的约定

顺序栈使用连续存储空间。以下约定 top 保存栈顶元素的下标:

  • 空栈:top == -1;
  • 首元素压入后:top == 0;
  • 栈满:top == MAX_SIZE - 1;
  • 栈长:top + 1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdbool.h>

#define MAX_SIZE 100
typedef int ElemType;

typedef struct {
ElemType data[MAX_SIZE];
int top;
} SqStack;

void InitStack(SqStack *S) {
S->top = -1;
}

bool StackEmpty(const SqStack *S) {
return S->top == -1;
}

在这种约定下,进栈应先判断满,再递增 top;出栈应先判断空,再读取并递减 top。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool Push(SqStack *S, ElemType x) {
if (S->top == MAX_SIZE - 1) {
return false;
}
S->data[++S->top] = x;
return true;
}

bool Pop(SqStack *S, ElemType *x) {
if (S->top == -1) {
return false;
}
*x = S->data[S->top--];
return true;
}

bool GetTop(const SqStack *S, ElemType *x) {
if (S->top == -1) {
return false;
}
*x = S->data[S->top];
return true;
}

另一种合法约定是 top 指向 “下一个可写位置”,此时空栈为 top == 0、栈顶为 data [top - 1]。两种定义都可行,但判空、判满和数组下标必须成套使用,不能混搭。

# 2. 共享栈

两个顺序栈若各自预留一半数组空间,可能一边满、另一边空,造成浪费。共享栈让两个栈从数组两端向中间生长:

  • 栈 1 初始 top1 == -1,向下标增大方向增长;
  • 栈 2 初始 top2 == MAX_SIZE,向下标减小方向增长;
  • 当 top1 + 1 == top2 时,整个数组才真正满。

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
typedef struct {
ElemType data[MAX_SIZE];
int top1;
int top2;
} ShareStack;

void InitShareStack(ShareStack *S) {
S->top1 = -1;
S->top2 = MAX_SIZE;
}

bool Push1(ShareStack *S, ElemType x) {
if (S->top1 + 1 == S->top2) {
return false;
}
S->data[++S->top1] = x;
return true;
}

bool Push2(ShareStack *S, ElemType x) {
if (S->top1 + 1 == S->top2) {
return false;
}
S->data[--S->top2] = x;
return true;
}

共享栈适用于两个栈的容量需求难以预估、但总容量受固定数组限制的场景。

# 三、链式栈

链式栈通常以不带头结点的单链表实现,并让链表头部充当栈顶。这样新增或删除首结点均为 O(1)O(1),不必遍历链表。

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
#include <stdbool.h>
#include <stdlib.h>

typedef struct StackNode {
ElemType data;
struct StackNode *next;
} StackNode, *LinkStack;

void InitLinkStack(LinkStack *S) {
*S = NULL;
}

bool LinkStackEmpty(LinkStack S) {
return S == NULL;
}

bool LinkPush(LinkStack *S, ElemType x) {
StackNode *node = malloc(sizeof(StackNode));
if (node == NULL) {
return false;
}

node->data = x;
node->next = *S;
*S = node;
return true;
}

bool LinkPop(LinkStack *S, ElemType *x) {
if (*S == NULL) {
return false;
}

StackNode *node = *S;
*x = node->data;
*S = node->next;
free(node);
return true;
}

链式栈没有固定数组容量造成的 “栈满”,但并非无限大:动态内存耗尽时仍会申请失败;每个结点还额外保存一个指针,空间局部性通常弱于顺序栈。

# 四、顺序栈与链式栈的比较

维度顺序栈链式栈
栈顶操作O(1)O(1)O(1)O(1)
容量静态数组时固定;动态数组可扩容随动态内存变化
栈满条件数组空间用尽内存申请失败
单元素额外开销无指针域需要指针域
缓存局部性通常较好通常较差
实现重点top 的边界与约定分配、断链与释放
例题 3.1:栈顶指针的状态判断

顺序栈容量为 5,采用 top 指向栈顶元素、空栈 top == -1 的约定。依次执行 Push (1)、Push (2)、Pop、Push (3) 后,top 的值是多少?栈中元素从栈底到栈顶依次为何?

【解】 初始 top == -1。Push (1) 后为 0;Push (2) 后为 1;Pop 后为 0;Push (3) 后为 1。因此栈内从底到顶为 1,31,3,top == 1。

# 五、考场检查清单

  1. 栈只有栈顶一端可操作,LIFO 不能写成 FIFO。
  2. 写顺序栈代码前先写清 top 的含义和空、满条件。
  3. 进栈和出栈都应先检查边界,避免数组越界或空栈读取。
  4. 共享栈的满条件是两个栈顶相邻,不是某个栈单独到数组中点。
  5. 链式栈将链表头设为栈顶,才能使压栈和出栈均为 O(1)O(1)。
更新于 阅读次数 次

请我喝[茶]~( ̄▽ ̄)~*

梦前辈 微信支付

微信支付

梦前辈 支付宝

支付宝