
栈和队列的数据结构
学习目标
01 栈的工作原理和基本操作
02 栈的顺序存储
03 栈的链式存储
04 栈的应用场景
05 队列的基本概念和类型
06 循环队列和链式队列实操
PART.01 栈的工作原理和基本操作
栈的定义
在数据结构中,栈(Stack)是一种遵循**"后进先出"(Last‑In‑First‑Out, LIFO)**原则的线性数据结构,可类比为"堆叠的盘子"------只能从顶部放新盘子,也只能从顶部取走盘子。
栈(Stack)是只允许在一端进行插入或删除操作的线性表。首先栈是一种线性表,但限定这种线性表只能在某一端进行插入和删除操作。
栈的基本概念
- 栈顶:栈中最活跃的一端,所有插入(进栈) 和删除(出栈)操作均在此进行。
- 栈底:栈中固定的一端,数据一旦压入栈底,只有当上方所有数据出栈后才能被访问。
- 空栈:栈中没有任何元素时,栈顶与栈底重合。
栈的工作原理
- 栈的工作流程完全围绕LIFO原则
- 进栈(Push):新元素从栈顶加入,栈顶指针向上移动(栈的容量+ 1)。
- 出栈(Pop) :仅栈顶元素可被删除,栈顶指针向下移动(栈的容量‑ 1)。 例:栈内元素a、b、c,执行出栈,先删除C,再删除B,最后删除A,出栈顺序为C→B→A。
- 访问限制:无法直接访问栈中间或栈底的元素,必须通过出栈操作"暴露"目标元素。
栈的存储实现方式
| 实现方式 | 核心结构 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 顺序栈 | 数组 + 栈顶指针 | 操作效率高(数组下标访问,O (1) ) | 容量固定,可能溢出(需动态扩容) | 元素数量已知、操作频繁的场景 |
| 链式栈 | 链表(无头节点) + 栈顶指针 | 容量动态扩展,无溢出风险 | 每个元素需额外存储指针,空间开销略大 | 元素数量不确定、需灵活扩容的场景 |
栈的基本操作
InitStack(&S):初始化一个空栈S。StackEmpty(S):判断一个栈是否为空,若栈S为空则返回true,否则返回false。Push(&S,x):进栈,若栈S未满,则将x加入使之成为新栈顶。Pop(&S,&x):出栈,若栈S非空,则弹出栈顶元素,并用x返回。GetTop(S,&x):读栈顶元素,但不出栈,若栈S非空,则用x返回栈顶元素。DestroyStack(&S):销毁栈,并释放栈S占用的存储空间(&表示引用调用)。
练习题
- 3个不同元素依次进栈,能得到( )种不同的出栈序列。
A. 4 B. 5 C. 6 D. 7
解析
- 第一个出栈元素:A(A 进栈后立即出栈)
剩余元素:B、C(需依次进栈)
- B 进→ B 出→ C 进→ C 出 序列:
ABC - B 进→ C 进→ C 出→ B 出 序列:
ACB
- 第一个出栈元素:B(A 进→B 进,B 先出)
剩余元素:A(栈内)、C(未进栈)
- A 出→ C 进→ C 出 序列:
BAC - C 进→ C 出→ A 出 序列:
BCA
- 第一个出栈元素:C(A 进→B 进→C 进,C 先出)
剩余元素:A、B(均在栈内,需按"后进先出" 出栈)
- B 出→ A 出 序列:
CBA
共5种:ABC、ACB、BAC、BCA、CBA
n 个元素的合法出栈序列数符合卡特兰数公式:
C_{n}=\\frac{1}{n+1}\\binom{2n}{n}
答案:B
- 若栈的输入序列是1,2,3,...,n,输出序列的第一个元素是n,则第i个输出元素是( )。
A. 不确定 B.n‑i C. n‑i‑1 D. n‑i+1
解析
当i=1时,输出元素为n。
当i=2时,输出元素为n‑1。
当i=3时,输出元素为n‑2。
依此类推,第i个输出元素为n‑i+1。
答案:D
【2020统考真题】对空栈S进行Push和Pop操作,入栈序列为a,b,c,d,e,经过Push、Push、Pop、Push、Pop、Push、Push、Pop操作后得到的出栈序列是( )。
A. b,a,c B. b,a,e C. b,c,a D. b,c,e
解析:
Push(a)、Push(b) → Pop输出b;Push© → Pop输出c;Push(d)、Push(e) → Pop输出e;出栈序列:
b c e。答案:D
PART.02 栈的顺序存储
如何实现栈的顺序存储
进栈:top加1 栈空:top=‑1 出栈:top减1 栈满:top= MAX_SIZE‑1
c
#define MaxSize 50 //定义栈中元素的最大个数
typedef struct{
Elemtype data [MaxSize]; //存放栈中元素
int top; //栈顶指针
}Stack;
- 栈顶指针:
S.top - 栈顶元素:
S.data[S.top] - 栈空:
S.top=-1; - 栈满:
S.top=MaxSize‑1; - 栈内元素个数:
S.top+1;
栈的基本操作1
c
// 1. 初始化栈
void initStack(Stack *stack) {
stack->top = -1; // 初始化栈顶指针为‑1,表示空栈
}
// 2. 判断栈是否为空
bool isEmpty(Stack *stack) {
return stack->top == -1; // 栈顶指针为‑1则为空
}
// 3. 判断栈是否已满
bool isFull(Stack *stack) {
return stack->top == MAX_SIZE - 1; // 栈顶指针达到最大索引则栈满
}
栈的基本操作2
c
// 4. 入栈操作
bool push(Stack *stack, ElementType element) {
if (isFull(stack)) {
printf("栈已满,无法入栈!\n");
return false; // 入栈失败
}
stack->top++; // 栈顶指针上移
stack->data[stack->top] = element; // 将元素放入栈顶
return true; // 入栈成功
}
// 5. 出栈操作
bool pop(Stack *stack, ElementType *element) {
if (isEmpty(stack)) {
printf("栈为空,无法出栈!\n");
return false; // 出栈失败
}
*element = stack->data[stack->top]; // 将栈顶元素赋值给输出参数
stack->top--; // 栈顶指针下移
return true; // 出栈成功
}
栈的基本操作3
c
// 6. 获取栈顶元素(不删除)
bool getTop(Stack *stack, ElementType *element) {
if (isEmpty(stack)) {
printf("栈为空,无栈顶元素!\n");
return false; // 获取失败
}
*element = stack->data[stack->top]; // 将栈顶元素赋值给输出参数
return true; // 获取成功
}
// 7. 打印栈中所有元素
void printStack(Stack *stack) {
if (isEmpty(stack)) {
printf("栈为空!\n");
return;
}
printf("栈中元素(从栈底到栈顶):");
for (int i = 0; i <= stack->top; i++) {
printf("%d ", stack->data[i]);
}
printf("\n");
}
PART.03 栈的链式存储
采用链式存储的栈称为链栈 ,链栈的优点是便于多个栈共享存储空间和提高其效率,且不存在栈满上溢的情况。通常采用单链表实现,并规定所有操作都是在单链表的表头进行。规定链栈没有头结点 ,top指向栈顶元素。
c
// 定义栈节点结构体
typedef int ElementType; // 栈中元素的类型
typedef struct Node {
int data; // 节点数据
struct Node* next; // 指向下一个节点的指针
} Node;
// 定义链式栈结构体
typedef struct {
Node* top; // 栈顶指针,指向栈顶节点
int size; // 栈的大小(元素个数)
} LinkedStack;
栈的链式存储初始化
c
// 1. 初始化栈
void initStack(LinkedStack* stack) {
stack->top = NULL; // 栈顶指针初始化为NULL,表示空栈
stack->size = 0; // 栈大小初始化为0
}
栈的链式存储基本操作1
c
// 2. 判断栈是否为空
bool isEmpty(LinkedStack* stack){
return stack->top==NULL;
}
// 3. 获取栈的大小
int getSize(LinkedStack* stack){
return stack->size;
}
// 4. 获取栈顶元素(不删除)
bool getTop(LinkedStack* stack,int *value){
if (isEmpty(stack)) {
printf("栈为空,无栈顶元素!\n");
return false;
}
*value = stack->top->data; // 获取栈顶元素的值
return true;
}
栈的链式存储基本操作2
c
// 5. 入栈操作
bool push(LinkedStack* stack, int value){
Node* newNode=(Node* )malloc(sizeof(Node));
if(newNode==NULL){
printf("内存分配失败!");
return false;
}
newNode->data=value;
newNode->next=stack->top;
stack->top=newNode;
stack->size++;
return true;
}
栈的链式存储基本操作3
c
// 6. 出栈操作
bool Pop(LinkedStack* stack, int *value){
Node *p=stack->top;
if(isEmpty(stack)){
printf("空栈\n");
return false;
}
*value=stack->top->data;
stack->top=stack->top->next;
free(p);
stack->size--;
return true;
};
栈的链式存储基本操作4
c
// 7. 打印栈中所有元素
void printStack(LinkedStack* stack){
Node* p=stack->top;
if(isEmpty(stack)){
printf("空栈");
return;
}
while(p!=NULL){
printf("%d ",p->data);
p=p->next;
}
}
// 8. 销毁栈,释放所有节点内存
void destroyStack(LinkedStack* stack){
Node* temp;
while (stack->top != NULL) {
temp = stack->top;
stack->top = stack->top->next;
free(temp);
}
stack->size = 0;
printf("栈已销毁!\n");
}
链式栈的实现与顺序栈的区别
- 存储结构:链式栈使用链表节点存储元素,每个节点包含数据域和指针域。
- 栈顶指针:链式栈的栈顶指针指向链表的首节点(第一个节点)。
- 入栈操作:新元素总是插入在链表的头部(栈顶),时间复杂度为 O(1)。
- 出栈操作:总是删除链表的头部节点(栈顶),时间复杂度为 O(1)。
- 栈满判断:链式栈理论上没有容量限制(除非内存耗尽),所以不需要判断栈满。
- 内存管理:需要手动管理每个节点的内存分配与释放,避免内存泄漏。
PART.04 栈的应用场景
栈的"LIFO" 特性使其在诸多场景中发挥关键作用:
- 函数调用栈 :程序执行时,函数的调用顺序(如
main()→A()→B())通过栈存储,函数返回时按B()→A()→main()出栈,恢复上下文。 - 表达式求值 :计算算术表达式(如
3+(4*2)-5)时,用栈存储运算符和操作数,处理优先级(括号、乘除先于加减)。 - 括号匹配 :判断代码中括号是否成对(如
{[()]}合法,{[)}非法),遇到左括号进栈,遇到右括号则与栈顶左括号匹配并出栈。 - 撤销操作(Undo):文本编辑器、绘图软件中,将每一步操作压入栈,撤销时出栈恢复上一步状态。
栈与递归调用
递归 :子程序(或函数)直接调用自己或通过一系列调用语句间接调用自己。
递归基本思想:把一个不能或不好求解的大问题转化为一个或几个小问题,再把这些小问题进一步分解成更小的小问题,直至每个小问题都可以直接求解。
递归的要素
- 递归边界条件(递归出口):确定递归到何时终止。
- 递归模式(递归体):大问题是如何分解为小问题。
示例:求n!
c
long fact ( int n) {
if ( n == 0 ) return 1;
else return n * fact (n-1);
}
递归调用工作栈原理:
- 运行开始时,首先为递归调用建立一个工作栈,其结构包括值参、局部变量和返回地址;
- 每次执行递归调用之前,把递归函数的值参和局部变量的当前值以及调用后的返回地址压栈;
- 每次递归调用结束后,将栈顶元素出栈,使相应的值参和局部变量恢复为调用前的值,然后转向返回地址指定的位置继续执行。
表达式求值
表达式分为:
- 前缀表达式(波兰式)
- 中缀表达式:人日常书写,计算机不好处理
- 后缀表达式(逆波兰式) :计算机容易处理,不需要括号
示例:(a+b)∗(a−b)(a+b)*(a-b)(a+b)∗(a−b)
- 前缀:
* + a b - a b- 中缀:
(a+b)*(a‑b)- 后缀:
a b + a b - *
将中缀表达式转换成后缀表达式
- 从左到右读取中缀表达式中的每一个字符。
- 如果读取的是操作数 ,直接将其添加到输出队列。
如果读取的是左括号'(' ,将其推入操作符栈。
如果读取的是右括号')' ,则从操作符栈中弹出操作符并添加到输出队列,直到遇到左括号'(',弹出并丢弃左括号。
如果读取的是操作符(+、‑、*、/) : 当操作符栈非空且栈顶操作符的优先级高于或等于当前操作符时,从操作符栈中弹出操作符并添加到输出队列。将当前操作符推入操作符栈。 - 清理栈。当表达式处理完毕后,将操作符栈中剩余的所有操作符依次弹出并添加到输出队列。
由后缀表达式计算表达式的值
后缀表达式(逆波兰表达式)是一个没有括号的表达式,计算过程按照操作数和操作符的顺序进行,操作数压栈,操作符将栈顶的两个操作数弹出进行计算并将结果压栈。
- 从左到右读取后缀表达式中的每一个元素。
- 如果读取的是操作数(数字),将其压入栈中。
- 如果读取的是操作符,从栈中弹出两个操作数。根据操作符进行相应的运算(如加、减、乘、除)。将运算结果压入栈中。
- 遍历完整个后缀表达式后,栈中应该只剩下一个元素,即最终的计算结果。
示例 a=5,b=3;后缀
a b + a b - *
5+3=85‑3=28 * 2 =16,最终结果16。
PART.05 队列的基本概念和基本操作
队列的定义
队列(Queue)简称队,也是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一端进行删除。
- 向队列中插入元素称为入队/进队;
- 删除元素称为出队/离队。
队列操作特性:先进先出(First In First Out,FIFO),类比生活排队。
- 队尾:允许插入(入队)的一端。
- 队首:允许删除(出队)的一端。
- 空队列:不含任何数据元素的队列。
队列常见的基本操作
InitQueue(&Q):初始化队列,构造一个空队列Q。QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false。EnQueue(&Q, x):入队,若队列Q未满,将x加入,使之成为新的队尾。DeQueue(&Q,&x):出队,若队列Q非空,删除队头元素,并用x返回。GetHead(Q,&x):读队头元素,若队列Q非空,则将队头元素赋值给x。
注意:栈和队列是操作受限的线性表,不能随便读取栈或队列中间的某个数据。
队列的特点
- 元素按顺序排列
- 只能在队尾添加,队头删除
- 访问操作通常只能查看队头元素
常见实现方式
- 顺序队列 :用数组实现,需要处理队列满和假溢出问题。
- 链式队列:用链表实现,不存在假溢出问题。
- 循环队列:优化的顺序队列,空间利用率更高。
- 双端队列:两端都可以插入和删除,更加灵活。
顺序队列
c
#define MaxSize 50
typedef struct{
ElemType data[MaxSize];//存放队列元素
int front,rear;//队头指针和队尾指针
}SqQueue;
- 初始时:
Q.front=Q.rear=0。 - 进队操作:队不满时,先送值到队尾元素,再将队尾指针加1。
- 出队操作:队不空时,先取队头元素值,再将队头指针加1。
假溢出:当元素被插入到数组中下标最大的位置上之后,队列的空间就用尽了,但此时数组的低端还有空闲空间,这种现象叫做假溢出。
循环队列
把存储队列元素的表从逻辑上视为一个环,称为循环队列。
当队尾指针Q.rear=MaxSize‑1后,再前进一个位置就自动到0,利用取余运算%实现。
- 队首指针进1:
Q.front=(Q.front+1)%MaxSize - 队尾指针进1:
Q.rear=(Q.rear+1)%MaxSize - 队列长度:
(Q.rear+MaxSize‑Q.front)%MaxSize
双端队列
双端队列是指允许两端都可以进行插入和删除操作的线性表。
- 输出受限的双端队列:允许在一端进行插入和删除,但在另一端只允许插入。
- 输入受限的双端队列:允许在一端进行插入和删除,但在另一端只允许删除。
若限定双端队列从某个端点插入的元素只能从该端点删除,则该双端队列就蜕变为两个栈底相邻接的栈。
链式队列
c
typedef struct LinkNode{
ElemType data;
struct LinkNode *next;
}LinkNode; //链式队列结点
typedef struct{
LinkNode *front,*rear; //队列的队头和队尾指针
}*LinkQueue; //链式队列头结点指针
- 带头结点:队首指针即为链表的头结点指针,增加一个指向队尾结点的指针。
- 不带头结点:当
Q.front==NULL && Q.rear==NULL时,链式队列为空。
队列的应用场景
队列遵循**先进先出(FIFO)**原则:
- 任务调度
- 操作系统进程、线程调度;打印任务队列。
- 数据缓冲
- 网络缓冲区;视频播放缓冲。
- 广度优先搜索(BFS)
- 树、图的广度优先遍历。
- 消息队列系统
- Kafka、RabbitMQ,异步通信、模块解耦。
- 现实模拟:银行排队、超市结账、车辆排队模拟。
- 计算机硬件:处理器指令队列、IO设备请求队列。
- Web服务器:客户端请求排队处理。
PART.06 链式队列的基本操作实操
链式队列的结构体设计
c
// 队列节点结构体
typedef struct Node {
int data;
struct Node* next;
} Node;
// 队列结构体
typedef struct Queue {
Node* front; // 队头指针
Node* rear; // 队尾指针
int size; // 队列大小
} Queue;
c
// 初始化队列
Queue* initQueue() {
Queue* queue = (Queue*)malloc(sizeof(Queue));
if (queue == NULL) {
printf("内存分配失败\n");
return NULL;
}
queue->front = NULL;
queue->rear = NULL;
queue->size = 0;
return queue;
}
链式队列的基本操作
c
// 判断队列是否为空
int isEmpty(Queue* queue) {
if (queue == NULL) return 1;
return queue->size == 0;
}
// 获取队列大小
int getSize(Queue* queue) {
if (queue == NULL) return 0;
return queue->size;
}
// 获取队头元素
int getFront(Queue* queue, int* data) {
if (isEmpty(queue)) {
printf("队列为空\n");
return 0;
}
*data = queue->front->data;
return 1;
}
链式队列的入队出队操作
c
// 入队操作
int enqueue(Queue* queue, int data) {
if (queue == NULL) return 0;
// 创建新节点
Node* newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) {
printf("内存分配失败\n");
return 0;
}
newNode->data = data;
newNode->next = NULL;
// 如果队列为空,新节点既是队头也是队尾
if (isEmpty(queue)) {
queue->front = newNode;
} else {
queue->rear->next = newNode;
}
queue->rear = newNode;
queue->size++;
return 1;
}
// 出队操作
int dequeue(Queue* queue, int* data) {
if (queue == NULL || isEmpty(queue)) {
printf("队列为空,无法出队\n");
return 0;
}
// 获取队头节点数据
Node* temp = queue->front;
*data = temp->data;
// 更新队头指针
queue->front = queue->front->next;
free(temp);
queue->size--;
// 如果队列变为空,更新队尾指针
if (isEmpty(queue)) {
queue->rear = NULL;
}
return 1;
}
链式队列的打印和销毁操作
c
// 打印队列元素
void printQueue(Queue* queue) {
if (queue == NULL || isEmpty(queue)) {
printf("队列为空\n");
return;
}
printf("队列元素: ");
Node* current = queue->front;
while (current != NULL) {
printf("%d ", current->data);
current = current->next;
}
printf("\n");
}
// 销毁队列
void destroyQueue(Queue* queue) {
if (queue == NULL) return;
Node* current = queue->front;
Node* next;
while (current != NULL) {
next = current->next;
free(current);
current = next;
}
free(queue);
}
PART.07 循环队列的基本操作实操
循环队列队满判断
为了区分是队空还是队满的情况,有三种处理方式:
-
牺牲一个单元区分(最常用):约定"队头指针在队尾指针的下一位置作为队满的标志"。
- 队满条件:
(Q.rear+1)%MaxSize==Q.front - 队空条件:
Q.front==Q.rear - 队列中元素个数:
(Q.rear‑Q.front+MaxSize) %MaxSize
- 队满条件:
-
结构体增设
size成员,记录实际元素个数。- 队空:
Q.size==0 - 队满:
Q.size==MaxSize
此时队空、队满都满足
Q.front==Q.rear,依靠size区分。 - 队空:
-
增设
tag标记成员。- 插入成功置
tag=1;删除成功置tag=0。 front==rear时:tag=1代表队满;tag=0代表队空。
- 插入成功置
循环队列(动态顺序)结构体与基础函数
c
// 循环队列结构体定义
typedef struct {
int *base; // 动态分配的数组基地址
int front; // 头指针,指向队头元素
int rear; // 尾指针,指向队尾元素的下一个位置
int capacity; // 队列的最大容量
} CircularQueue;
// 初始化队列
bool InitQueue(CircularQueue *q, int capacity) {
// 检查容量是否合法
if (capacity <= 0) {
printf("初始化失败:容量必须为正数\n");
return false;
}
// 分配内存空间
q->base = (int *)malloc(capacity * sizeof(int));
if (q->base == NULL) {
printf("初始化失败:内存分配失败\n");
return false;
}
q->front = 0;
q->rear = 0;
q->capacity = capacity;
return true;
}
// 判断队列是否为空
bool IsEmpty(CircularQueue *q) {
return q->front == q->rear;
}
// 判断队列是否已满,牺牲一个空间区分队满队空
bool IsFull(CircularQueue *q) {
return (q->rear + 1) % q->capacity == q->front;
}
// 获取队列长度
int QueueLength(CircularQueue *q) {
return (q->rear - q->front + q->capacity) % q->capacity;
}
循环队列的入队出队操作(动态顺序)
c
// 入队操作
bool EnQueue(CircularQueue *q, int value) {
if (IsFull(q)) {
printf("入队失败:队列已满\n");
return false;
}
q->base[q->rear] = value;
q->rear = (q->rear + 1) % q->capacity; // 循环移动尾指针
return true;
}
// 出队操作
bool DeQueue(CircularQueue *q, int *value) {
if (IsEmpty(q)) {
printf("出队失败:队列为空\n");
return false;
}
*value = q->base[q->front];
q->front = (q->front + 1) % q->capacity; // 循环移动头指针
return true;
}
// 获取队头元素
bool GetHead(CircularQueue *q, int *value) {
if (IsEmpty(q)) {
printf("获取队头失败:队列为空\n");
return false;
}
*value = q->base[q->front];
return true;
}
// 打印队列元素
void PrintQueue(CircularQueue *q) {
if (IsEmpty(q)) {
printf("队列为空\n");
return;
}
printf("队列元素:");
int i = q->front;
while (i != q->rear) {
printf("%d ", q->base[i]);
i = (i + 1) % q->capacity;
}
printf("\n");
}
// 销毁队列
void DestroyQueue(CircularQueue *q) {
if (q->base != NULL) {
free(q->base);
q->base = NULL;
}
q->front = q->rear = 0;
q->capacity = 0;
}