Lecture 7:栈基本概念
Chinese-language Computer Science 408 note: Lecture 7:栈基本概念. 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.
7.1 栈的定义
定义:#栈
描述: 栈是只允许在一端进行插入或删除操作的线性表。
解释
- 栈本质上仍然是线性表。
- 栈的限制在于:
- 只能在一端插入;
- 只能在同一端删除。
- 允许插入和删除的一端称为栈顶: $$ \text{Top} $$
- 另一端称为栈底: $$ \text{Bottom} $$
- 不含任何元素的栈称为空栈。
逻辑特点
- 后进入栈的元素先离开栈。
- 先进入栈的元素后离开栈。
- 因此栈又称为后进先出表: $$ \text{LIFO}=\text{Last In First Out} $$
图示信息结构化
- 栈底是最早进入且最后离开的元素所在端,位置通常固定。
- 栈顶是唯一允许插入、删除的一端,随入栈和出栈移动。
- 空栈没有栈顶元素;顺序栈采用
top=-1时,top表示当前栈顶元素的数组下标。
7.2 栈的基本操作
7.2.1 初始化和销毁
初始化
InitStack(&S);
- 作用:初始化栈。
- 含义:
- 构造一个空栈
S; - 分配必要的存储空间;
- 设置初始状态。
- 构造一个空栈
销毁
DestroyStack(&S);
- 作用:销毁栈。
- 含义:
- 释放栈
S占用的存储空间; - 使栈不再可用。
- 释放栈
7.2.2 入栈和出栈
入栈
Push(&S, x);
- 又称进栈、压栈。
- 若栈
S未满,则将元素x加入栈中。 - 加入后,
x成为新的栈顶元素。
出栈
Pop(&S, &x);
- 又称弹栈。
- 若栈
S非空,则弹出栈顶元素。 - 被弹出的元素用
x返回。
7.2.3 读栈顶和判空
读栈顶元素
GetTop(S, &x);
- 若栈
S非空,则用x返回栈顶元素。 - 该操作只读取,不删除栈顶元素。
判断栈空
StackEmpty(S);
- 若栈
S为空,则返回true。 - 否则返回
false。
7.2.4 操作分类总结
| 分类 | 操作 | 作用 |
|---|---|---|
| 创 | InitStack(&S) |
构造空栈 |
| 消 | DestroyStack(&S) |
销毁栈 |
| 增 | Push(&S, x) |
入栈 |
| 删 | Pop(&S, &x) |
出栈 |
| 查 | GetTop(S, &x) |
读栈顶元素 |
| 判定 | StackEmpty(S) |
判断栈是否为空 |
7.3 卡特兰数
题型:#进栈出栈序列
第一部分:解题方法
有 $n$ 个不同元素按照固定顺序依次进栈,能够得到的合法出栈序列个数为: $$ \frac{1}{n+1}C_{2n}^{n} $$
也可以写成组合数形式: $$ \frac{1}{n+1}\binom{2n}{n} $$
该公式称为卡特兰数: $$ C_n=\frac{1}{n+1}\binom{2n}{n} $$
使用条件
- 入栈顺序已经固定。
- 每个元素都必须入栈一次、出栈一次。
- 出栈过程中不能违反栈的后进先出规则。
第二部分:典型例题
例题:3 个不同元素按 $1,2,3$ 的顺序入栈,合法出栈序列有多少种?
代入公式: $$ \frac{1}{3+1}\binom{2\times 3}{3} =\frac{1}{4}\binom{6}{3} =\frac{1}{4}\times 20 =5 $$
所以共有 $5$ 种合法出栈序列。
第三部分:知识点复盘
- 卡特兰数不是任意排列数。
- 全排列数量是: $$ n! $$
- 合法出栈序列数量通常小于 $n!$。
- 判断某个序列是否合法时,要模拟入栈、出栈过程。
7.4 栈的顺序存储实现
7.4.1 顺序栈定义
定义:#顺序栈
描述: 顺序栈是用顺序存储方式实现的栈,通常用静态数组保存栈中元素,并用栈顶指针记录栈顶位置。
特点
- 和顺序表类似,都用数组保存元素。
- 由于栈只允许在栈顶操作,所以只需要重点维护栈顶指针。
- 常见约定:
top指向当前栈顶元素;- 空栈时
top == -1; - 栈满时
top == MaxSize - 1。
以下 ElemType 是待替换的元素类型;函数形参 SqStack &S、ElemType &x 使用 C++ 引用,并非 C 语言函数形参。原 PDF 的示例整体按 C++ 语法阅读,前面的 InitStack(&S) 等是抽象操作记号,不要与下文具体调用 InitStack(S) 混作同一套函数签名。
代码定义
#define MaxSize 10
typedef struct {
ElemType data[MaxSize]; // 静态数组存放栈中元素
int top; // 栈顶指针
} SqStack; // Sq: sequence,表示顺序存储
7.4.2 初始化
代码
void InitStack(SqStack &S) {
S.top = -1;
}
使用示例
void testStack() {
SqStack S; // 声明一个顺序栈,系统分配空间
InitStack(S); // 初始化栈
// 后续操作
// 函数运行结束后,系统自动回收局部变量空间
}
解释
top = -1表示当前没有栈顶元素。- 因为数组下标从 $0$ 开始,所以 $-1$ 可以表示“无有效元素”。
- 若用静态数组作为局部变量,函数结束后系统会自动回收空间。
7.4.3 判断栈空
逻辑
- 最开始栈中无元素时: $$ S.top=-1 $$
- 因此判断栈空的条件为: $$ S.top==-1 $$
代码
bool StackEmpty(SqStack S) {
if (S.top == -1)
return true;
else
return false;
}
补充:另一种栈顶指针约定
- 如果把初始值设置为: $$ S.top=0 $$
- 通常表示
top指向下一个可插入位置,而不是当前栈顶元素。 - 此时第一个元素入栈后,
top会移动到data[1]的位置。 - 对应的栈满条件变为: $$ S.top==MaxSize $$
- 注意:两种约定不能混用,代码中的入栈、出栈、判空、判满条件必须保持一致。
7.4.4 入栈
逻辑
- 入栈前必须先判断栈是否已满。
- 若未满:
- 栈顶指针先加 $1$;
- 再把新元素写入新的栈顶位置。
代码
bool Push(SqStack &S, ElemType x) {
if (S.top == MaxSize - 1)
return false;
S.top = S.top + 1;
S.data[S.top] = x;
return true;
}
等价写法
S.data[++S.top] = x;
易错点
++S.top表示先让top加 $1$,再访问数组。- 不能写成:
S.data[S.top++] = x;
- 因为在
top == -1的空栈约定下,后者会先访问data[-1],下标非法。
7.4.5 出栈
逻辑
- 出栈前必须先判断栈是否为空。
- 若非空:
- 先取出当前栈顶元素;
- 再让栈顶指针减 $1$。
代码
bool Pop(SqStack &S, ElemType &x) {
if (S.top == -1)
return false;
x = S.data[S.top];
S.top = S.top - 1;
return true;
}
注意
- 出栈以后,原数组位置中的数据仍可能留在内存中。
- 该元素只是逻辑上被删除。
- 顺序栈的出栈操作没有
free。
7.4.6 读取栈顶元素
逻辑
- 读取栈顶前必须先判断栈是否为空。
- 读取栈顶元素不会改变
top。 - 因此
GetTop与Pop的区别是:GetTop只读不删;Pop既读又删。
代码
bool GetTop(SqStack S, ElemType &x) {
if (S.top == -1)
return false;
x = S.data[S.top];
return true;
}
7.4.7 顺序栈操作复杂度
| 操作 | 条件判断 | 是否移动大量元素 | 时间复杂度 |
|---|---|---|---|
| 判空 | S.top == -1 |
否 | $O(1)$ |
| 判满 | S.top == MaxSize - 1 |
否 | $O(1)$ |
| 入栈 | 先判满 | 否 | $O(1)$ |
| 出栈 | 先判空 | 否 | $O(1)$ |
| 读栈顶 | 先判空 | 否 | $O(1)$ |
7.5 共享栈
定义:#共享栈
描述: 共享栈是在同一片数组空间中实现两个栈,使两个栈从数组两端向中间增长。
基本思想
- 0 号栈从数组左端开始增长。
- 1 号栈从数组右端开始增长。
- 两个栈共享同一片数组空间。
- 当两个栈顶相邻时,说明没有剩余空间。
图示解读
- 0 号栈从下标 $0$ 向右增长,1 号栈从下标 $MaxSize-1$ 向左增长。
- 初始时
top0=-1、top1=MaxSize;栈满条件为top0+1==top1。 - 两个栈共用尚未占用的中间区域,能减少两个普通顺序栈一边溢出、另一边仍有空位的浪费。
代码定义
#define MaxSize 10
typedef struct {
ElemType data[MaxSize]; // 静态数组存放两个栈的元素
int top0; // 0 号栈栈顶指针
int top1; // 1 号栈栈顶指针
} ShStack;
初始化
void InitStack(ShStack &S) {
S.top0 = -1;
S.top1 = MaxSize;
}
栈满条件
- 0 号栈栈顶的右边紧挨着 1 号栈栈顶时,两个栈之间没有空闲单元。
- 因此共享栈的栈满条件为: $$ S.top0+1==S.top1 $$
特点
- 实现了两个栈。
- 两个栈共享同一个存储空间。
- 当一个栈元素较多、另一个栈元素较少时,可以更充分地利用数组空间。
- 适合两个栈空间需求此消彼长的场景。
7.6 栈的链式存储实现
定义:#链栈
描述: 链栈是用链式存储方式实现的栈,本质上是操作受限的单链表。
概念
- 链栈和单链表的定义几乎没有区别。
- 区别主要是名词和操作限制:
- 单链表可以在多个位置插入、删除;
- 链栈只在栈顶插入、删除。
图示解读
- 链栈结点在物理内存中不必连续,逻辑次序由
next指针连接。 - 栈顶指针指向链表首元结点;入栈和出栈都在表头完成,时间复杂度均为 $O(1)$。
- 采用带头结点还是不带头结点会影响空栈判定和具体代码,但不会改变后进先出的逻辑。
代码定义
typedef struct Linknode {
ElemType data;
struct Linknode *next;
} *LiStack;
解释
data是数据域。next是指针域。LiStack可以理解为链栈的栈顶指针。- 链栈通常把链表头部作为栈顶,便于在 $O(1)$ 时间内入栈、出栈。
补充写法 在一些教材中,也会把结点类型和指针类型分开写:
typedef struct Linknode {
ElemType data;
struct Linknode *next;
} Linknode, *LiStack;
7.6.1 链栈与顺序栈对比
| 对比维度 | 顺序栈 | 链栈 |
|---|---|---|
| 存储方式 | 顺序存储 | 链式存储 |
| 存储空间 | 需要连续空间 | 不要求连续空间 |
| 容量限制 | 受 MaxSize 限制 |
理论上只受内存限制 |
| 栈满判断 | 需要 | 通常不需要固定容量判满 |
| 指针开销 | 无额外指针域 | 每个结点有指针域 |
| 入栈出栈 | $O(1)$ | $O(1)$ |
7.7 面试题:栈和队列的区别
7.7.1 队列
定义:#队列
描述: 队列是允许在一端进行插入、另一端进行删除的线性表。
特点
- 队列像排队一样。
- 进入队列的元素按先进先出的规则处理: $$ \text{FIFO}=\text{First In First Out} $$
- 通常在表尾插入。
- 通常在表头删除。
- 因为队列经常插入、删除,常用定长数组实现循环队列。
- 操作前需要判断队列是否为空、是否已满。
- 如果想要动态长度,也可以用链表存储队列。
- 链式队列需要记录:
- 队头指针;
- 队尾指针。
7.7.2 栈
特点
- 栈只能在固定的栈顶一端进行插入和删除;顺序栈常把数组有效区末端作为栈顶,而链栈常把链表表头作为栈顶,因此“栈顶”不能无条件等同于某种实现的物理表尾。
- 入栈、出栈都发生在栈顶。
- 栈按后进先出规则处理元素: $$ \text{LIFO}=\text{Last In First Out} $$
- 栈常用定长数组实现。
- 若用数组实现,可以用一个变量记录当前栈大小或栈顶位置。
7.7.3 对比表
| 对比维度 | 栈 | 队列 |
|---|---|---|
| 插入位置 | 栈顶 | 队尾 |
| 删除位置 | 栈顶 | 队头 |
| 操作端数 | 同一端插入、删除 | 一端插入、另一端删除 |
| 逻辑规则 | 后进先出 | 先进先出 |
| 英文缩写 | LIFO | FIFO |
| 常见顺序实现 | 顺序栈 | 循环队列 |
| 常见链式实现 | 链栈 | 链式队列 |
7.8 面试题:如何区分循环队列队空和队满
这一部分出现在原 PDF 的 Lecture 7 末尾,但主要属于后续队列内容。这里先按原 PDF 保留,正式讲队列时再展开。
7.8.1 普通情况下的问题
- 循环队列中,队空和队满有时会出现相同的判断形式: $$ Q.front==Q.rear $$
- 若不额外处理,就无法只靠这个条件区分队空和队满。
指针含义
- 队头指针
front指向第一个队列元素。 - 队尾指针
rear指向最后一个元素的下一个位置。 rear指向的位置也就是即将入队的位置。
7.8.2 方法一:牺牲一个存储单元
- 让循环队列永远空出一个位置。
- 当队尾指针再向后移动一格就会到达队头指针时,认为队列已满。
- 队满条件为: $$ (Q.rear+1)\bmod MaxSize==Q.front $$
- 队空条件仍为: $$ Q.front==Q.rear $$
7.8.3 方法二:增加元素个数变量
- 在队列类型中增设一个表示元素个数的数据成员:
int size;
- 队空条件为: $$ Q.size==0 $$
- 队满条件为: $$ Q.size==MaxSize $$
相关知识
- 科目导航:数据结构导航
- 阅读顺序:上一篇:顺序表和链表对比 · 下一篇:队列基本概念
- 关联专题:栈与队列代码
7.9 本节复盘
必须掌握
- 栈是只允许在一端插入或删除的线性表。
- 栈顶是允许操作的一端,栈底是另一端。
- 栈的逻辑规则是后进先出: $$ \text{LIFO} $$
- 顺序栈通常用数组和栈顶指针实现。
- 常见空栈条件: $$ S.top==-1 $$
- 常见栈满条件: $$ S.top==MaxSize-1 $$
- 共享栈的栈满条件: $$ S.top0+1==S.top1 $$
- $n$ 个不同元素按固定顺序入栈时,合法出栈序列个数为: $$ \frac{1}{n+1}\binom{2n}{n} $$
易错点
- 栈只能在栈顶插入、删除,不是任意位置都能操作。
GetTop只读取栈顶元素,不删除元素。Pop会改变栈顶指针。- 顺序栈出栈后,数组中的旧值可能还在,但逻辑上已经被删除。
S.data[++S.top]=x中的++不能随意写反。- 链栈不是新的逻辑结构,它仍然是栈,只是存储方式变成链式。
- 循环队列中
Q.front==Q.rear可能表示队空,也可能在未经处理时与队满混淆。
一句话总结
- 栈:只在一端操作,后进先出。
- 顺序栈:数组加栈顶指针。
- 共享栈:两个栈从数组两端向中间增长。
- 链栈:用单链表思想实现栈顶插入和删除。

