📌目录
- [📚 一,栈和队列的定义与特点](#📚 一,栈和队列的定义与特点)
-
- [📥 (一)栈的定义与特点](#📥 (一)栈的定义与特点)
- [📤 (二)队列的定义与特点](#📤 (二)队列的定义与特点)
- [🌰 二,案例引入](#🌰 二,案例引入)
- [📚 三,栈的表示与操作的实现](#📚 三,栈的表示与操作的实现)
- [🔄 四,栈与递归](#🔄 四,栈与递归)
- [📋 五,队列的表示和操作的实现](#📋 五,队列的表示和操作的实现)
- [🛠️ 六,案例分析与实现](#🛠️ 六,案例分析与实现)
- [📝 章结](#📝 章结)

📚 一,栈和队列的定义与特点
栈和队列是两种特殊的线性表,它们在操作上具有严格的限制,广泛应用于算法设计、系统开发等领域。
📥 (一)栈的定义与特点
栈(Stack) 是一种限定仅在表尾进行插入和删除操作的线性表,其核心特点可概括为**"后进先出"(Last In First Out,LIFO)**。
-
关键术语:
- 允许操作的一端称为栈顶(Top);
- 另一端称为栈底(Bottom);
- 插入元素的操作称为入栈(Push);
- 删除元素的操作称为出栈(Pop);
- 不含元素的栈称为空栈。
-
示例:叠放的盘子,只能从最上方取放,最后放入的盘子最先被取出。
-
数学描述:设栈S=(a₁,a₂,...,aₙ),则a₁为栈底元素,aₙ为栈顶元素,操作顺序满足:入栈a₁→a₂→...→aₙ,出栈aₙ→...→a₂→a₁。
📤 (二)队列的定义与特点
队列(Queue) 是一种限定仅在表尾插入元素、表头删除元素的线性表,其核心特点为**"先进先出"(First In First Out,FIFO)**。
-
关键术语:
- 表尾(插入端)称为队尾(Rear);
- 表头(删除端)称为队头(Front);
- 插入元素的操作称为入队(Enqueue);
- 删除元素的操作称为出队(Dequeue);
- 不含元素的队列称为空队列。
-
示例:排队购票,先到的人先购票离开,后到的人依次排队等待。
-
数学描述:设队列Q=(a₁,a₂,...,aₙ),则a₁为队头元素,aₙ为队尾元素,操作顺序满足:入队a₁→a₂→...→aₙ,出队a₁→a₂→...→aₙ。
🌰 二,案例引入
场景1:栈的应用------浏览器历史记录
当你浏览网页时:
- 点击"前进"按钮,会跳转到上一次关闭的页面(对应栈的入栈);
- 点击"后退"按钮,会返回之前浏览的页面(对应栈的出栈)。
浏览器通过栈结构记录访问顺序,确保"最后访问的页面最先被回溯"。
场景2:队列的应用------打印机任务调度
多台电脑向同一台打印机发送打印任务时:
- 任务按提交顺序进入队列(入队);
- 打印机按顺序处理任务(出队),先提交的任务先打印。
队列保证了任务处理的公平性,避免"后提交的任务插队"。
这两个案例揭示了栈和队列的核心价值------通过限制操作顺序,简化特定场景下的问题处理逻辑。
📚 三,栈的表示与操作的实现
(一)栈的类型定义
栈的抽象数据类型(ADT)定义如下:
ADT Stack {
数据:
栈中元素具有相同类型,相邻元素具有前驱和后继关系,仅允许在栈顶操作。
操作:
1. InitStack(&S):初始化栈S为空栈。
2. StackEmpty(S):判断栈S是否为空,若空返回TRUE,否则返回FALSE。
3. Push(&S, e):将元素e插入栈S的栈顶。
4. Pop(&S, &e):删除栈S的栈顶元素,并用e返回其值。
5. GetTop(S, &e):获取栈S的栈顶元素,并用e返回其值(不删除)。
6. DestroyStack(&S):销毁栈S,释放占用的内存空间。
}
(二)顺序栈的表示与实现
顺序栈是栈的顺序存储结构,采用数组存储元素,用指针标记栈顶位置。
-
存储表示(C语言):
c#define MAXSIZE 100 // 栈的最大容量 typedef int ElemType; typedef struct { ElemType data[MAXSIZE]; // 存储栈元素的数组 int top; // 栈顶指针(-1表示空栈,0~MAXSIZE-1表示栈顶位置) } SqStack;
-
核心操作实现:
-
初始化:
cvoid InitStack(SqStack *S) { S->top = -1; // 空栈标志 }
时间复杂度:O(1)
-
入栈:
cint Push(SqStack *S, ElemType e) { if (S->top == MAXSIZE - 1) // 栈满 return 0; S->data[++S->top] = e; // 栈顶指针先加1,再存入元素 return 1; }
时间复杂度:O(1)
-
出栈:
cint Pop(SqStack *S, ElemType *e) { if (S->top == -1) // 栈空 return 0; *e = S->data[S->top--]; // 先取出栈顶元素,再将栈顶指针减1 return 1; }
时间复杂度:O(1)
-
取栈顶元素:
cint GetTop(SqStack S, ElemType *e) { if (S.top == -1) // 栈空 return 0; *e = S.data[S.top]; // 仅读取,不修改栈顶指针 return 1; }
时间复杂度:O(1)
-
(三)链栈的表示与实现
链栈是栈的链式存储结构,采用单链表存储元素,头节点作为栈顶(便于插入和删除)。
-
存储表示(C语言):
ctypedef int ElemType; typedef struct StackNode { ElemType data; // 数据域 struct StackNode *next; // 指针域(指向后继节点,即栈底方向) } StackNode, *LinkStack;
注:链栈无栈满限制(除非内存不足),空栈标志为
top == NULL
。 -
核心操作实现:
-
初始化:
cvoid InitStack(LinkStack *top) { *top = NULL; // 空栈,栈顶指针为NULL }
时间复杂度:O(1)
-
入栈:
cint Push(LinkStack *top, ElemType e) { StackNode *s = (StackNode*)malloc(sizeof(StackNode)); // 创建新节点 if (!s) return 0; // 内存分配失败 s->data = e; s->next = *top; // 新节点指向原栈顶 *top = s; // 栈顶指针更新为新节点 return 1; }
时间复杂度:O(1)
-
出栈:
cint Pop(LinkStack *top, ElemType *e) { if (*top == NULL) return 0; // 空栈 StackNode *p = *top; // 暂存栈顶节点 *e = p->data; // 取出数据 *top = p->next; // 栈顶指针下移 free(p); // 释放原栈顶节点 return 1; }
时间复杂度:O(1)
-
-
顺序栈 vs 链栈:
- 顺序栈适合已知最大容量的场景(如固定大小的缓存),实现简单、访问高效;
- 链栈适合容量不确定的场景(如动态表达式求值),内存利用率更高。
🔄 四,栈与递归
(一)采用递归算法解决的问题
递归是指函数直接或间接调用自身的编程技巧,栈是递归实现的底层支撑。适合用递归解决的问题具有**"自相似性"**,即问题可分解为规模更小的同类子问题,例如:
- 数学问题:阶乘计算(n! = n × (n-1)!)、斐波那契数列(F(n) = F(n-1) + F(n-2));
- 数据结构问题:二叉树遍历(先序遍历 = 根节点 + 左子树先序 + 右子树先序)、汉诺塔问题。
(二)递归过程与递归工作栈
递归函数的执行过程依赖递归工作栈存储每次调用的状态,包括:
- 参数:函数的输入值(如阶乘中的n);
- 局部变量:函数内部定义的变量;
- 返回地址:函数执行完毕后需返回的调用处地址。
示例:计算3!的递归过程
- 调用
fact(3)
时,将参数3、返回地址存入栈; fact(3)
调用fact(2)
,将参数2、返回地址存入栈;fact(2)
调用fact(1)
,将参数1、返回地址存入栈;fact(1)
调用fact(0)
,将参数0、返回地址存入栈;fact(0)
返回1,弹出栈顶(0的状态),计算1×1=1
;- 依次弹出栈中状态,最终计算
3×2×1×1=6
。
栈的"后进先出"特性完美匹配递归的"最后调用先返回"逻辑。
(三)递归算法的效率分析
递归的优势是代码简洁,但存在潜在问题:
- 时间效率:多次重复计算(如斐波那契数列的递归实现,时间复杂度O(2ⁿ));
- 空间效率:递归深度过大会导致栈溢出(如计算10000!的递归调用可能超出栈容量)。
优化方向:
- 用记忆化搜索(缓存子问题结果)减少重复计算;
- 用栈模拟递归(非递归实现)避免栈溢出。
(四)利用栈将递归转换为非递归的方法
核心思路:用人工定义的栈模拟递归工作栈,存储调用状态,步骤如下:
- 初始化栈,将初始参数入栈;
- 循环:弹出栈顶状态,处理当前逻辑;
- 若需继续递归,将子问题参数入栈;
- 直至栈空,完成所有计算。
示例:阶乘的非递归实现(栈模拟)
c
int Fact(int n) {
SqStack S;
InitStack(&S);
int result = 1;
// 将所有参数入栈(从n到1)
while (n > 0) {
Push(&S, n);
n--;
}
// 出栈计算乘积
while (!StackEmpty(S)) {
int e;
Pop(&S, &e);
result *= e;
}
return result;
}
📋 五,队列的表示和操作的实现
(一)队列的类型定义
队列的抽象数据类型(ADT)定义如下:
ADT Queue {
数据:
队列中元素具有相同类型,相邻元素具有前驱和后继关系,仅允许在队尾插入、队头删除。
操作:
1. InitQueue(&Q):初始化队列Q为空队列。
2. QueueEmpty(Q):判断队列Q是否为空,若空返回TRUE,否则返回FALSE。
3. EnQueue(&Q, e):将元素e插入队列Q的队尾。
4. DeQueue(&Q, &e):删除队列Q的队头元素,并用e返回其值。
5. GetHead(Q, &e):获取队列Q的队头元素,并用e返回其值(不删除)。
6. DestroyQueue(&Q):销毁队列Q,释放占用的内存空间。
}
(二)循环队列------队列的顺序表示与实现
顺序队列 用数组存储元素,队头指针front
指向队头元素,队尾指针rear
指向队尾元素的下一个位置。但普通顺序队列存在"假溢出"问题(数组未满但rear
已达最大下标),循环队列通过将数组视为环形解决该问题。
-
存储表示(C语言):
c#define MAXSIZE 100 typedef int ElemType; typedef struct { ElemType data[MAXSIZE]; // 存储队列元素 int front; // 队头指针(初始0) int rear; // 队尾指针(初始0) } SqQueue;
- 队空标志:
Q.front == Q.rear
; - 队满标志:
(Q.rear + 1) % MAXSIZE == Q.front
(牺牲一个位置区分空满)。
- 队空标志:
-
核心操作实现:
-
入队:
cint EnQueue(SqQueue *Q, ElemType e) { if ((Q->rear + 1) % MAXSIZE == Q->front) return 0; // 队满 Q->data[Q->rear] = e; Q->rear = (Q->rear + 1) % MAXSIZE; // 队尾指针循环后移 return 1; }
时间复杂度:O(1)
-
出队:
cint DeQueue(SqQueue *Q, ElemType *e) { if (Q->front == Q->rear) return 0; // 队空 *e = Q->data[Q->front]; Q->front = (Q->front + 1) % MAXSIZE; // 队头指针循环后移 return 1; }
时间复杂度:O(1)
-
(三)链队------队列的链式表示与实现
链队用单链表存储元素,队头指针指向头节点,队尾指针指向尾节点(便于入队操作)。
-
存储表示(C语言):
ctypedef int ElemType; typedef struct QNode { ElemType data; struct QNode *next; } QNode, *QueuePtr; typedef struct { QueuePtr front; // 队头指针(指向头节点) QueuePtr rear; // 队尾指针(指向最后一个节点) } LinkQueue;
- 队空标志:
Q.front == Q.rear
(均指向头节点)。
- 队空标志:
-
核心操作实现:
-
初始化:
cvoid InitQueue(LinkQueue *Q) { Q->front = Q->rear = (QueuePtr)malloc(sizeof(QNode)); // 创建头节点 Q->front->next = NULL; }
时间复杂度:O(1)
-
入队:
cint EnQueue(LinkQueue *Q, ElemType e) { QueuePtr s = (QueuePtr)malloc(sizeof(QNode)); if (!s) return 0; // 内存分配失败 s->data = e; s->next = NULL; Q->rear->next = s; // 尾节点后接新节点 Q->rear = s; // 队尾指针更新为新节点 return 1; }
时间复杂度:O(1)
-
出队:
cint DeQueue(LinkQueue *Q, ElemType *e) { if (Q->front == Q->rear) return 0; // 队空 QueuePtr p = Q->front->next; // 暂存队头元素节点 *e = p->data; Q->front->next = p->next; // 头节点指向新队头 if (Q->rear == p) Q->rear = Q->front; // 若删除的是尾节点,更新队尾指针 free(p); return 1; }
时间复杂度:O(1)
-
🛠️ 六,案例分析与实现
案例1:括号匹配问题(栈的应用)
问题描述 :判断一个字符串中的括号(()
、[]
、{}
)是否匹配,例如"([)]"
不匹配,"()[]{}"
匹配。
解题思路:
- 遍历字符串,遇到左括号(
(
、[
、{
)则入栈; - 遇到右括号时,若栈空(无匹配的左括号)则不匹配;否则弹出栈顶元素,检查是否为对应的左括号(如
)
对应(
); - 遍历结束后,若栈为空则完全匹配,否则存在未匹配的左括号。
代码实现(顺序栈):
c
#include <stdio.h>
#include <string.h>
#define MAXSIZE 100
typedef char ElemType;
typedef struct {
ElemType data[MAXSIZE];
int top;
} SqStack;
void InitStack(SqStack *S) { S->top = -1; }
int Push(SqStack *S, ElemType e) {
if (S->top == MAXSIZE - 1) return 0;
S->data[++S->top] = e;
return 1;
}
int Pop(SqStack *S, ElemType *e) {
if (S->top == -1) return 0;
*e = S->data[S->top--];
return 1;
}
int StackEmpty(SqStack S) { return S.top == -1; }
int isMatch(char *str) {
SqStack S;
InitStack(&S);
int len = strlen(str);
for (int i = 0; i < len; i++) {
if (str[i] == '(' || str[i] == '[' || str[i] == '{') {
Push(&S, str[i]); // 左括号入栈
} else {
if (StackEmpty(S)) return 0; // 右括号无匹配的左括号
char top;
Pop(&S, &top);
if ((str[i] == ')' && top != '(') ||
(str[i] == ']' && top != '[') ||
(str[i] == '}' && top != '{')) {
return 0; // 括号类型不匹配
}
}
}
return StackEmpty(S); // 栈空则完全匹配
}
int main() {
char str[] = "({[]})";
printf("%s\n", isMatch(str) ? "匹配" : "不匹配"); // 输出:匹配
return 0;
}
案例2:银行排队模拟(队列的应用)
问题描述:模拟银行窗口服务过程,客户按到达顺序排队,窗口依次为客户服务,计算平均等待时间。
解题思路:
- 用队列存储客户信息(到达时间、服务时长);
- 记录当前时间,依次出队客户:
- 若客户到达时间晚于当前时间,更新当前时间为到达时间;
- 计算等待时间(当前时间 - 到达时间),累加总等待时间;
- 更新当前时间(加上服务时长);
- 平均等待时间 = 总等待时间 / 客户数量。
代码实现(链队):
c
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int arriveTime; // 到达时间(分钟)
int serviceTime; // 服务时长(分钟)
} Customer;
typedef struct QNode {
Customer data;
struct QNode *next;
} QNode, *QueuePtr;
typedef struct {
QueuePtr front;
QueuePtr rear;
} LinkQueue;
void InitQueue(LinkQueue *Q) {
Q->front = Q->rear = (QueuePtr)malloc(sizeof(QNode));
Q->front->next = NULL;
}
void EnQueue(LinkQueue *Q, Customer c) {
QueuePtr s = (QueuePtr)malloc(sizeof(QNode));
s->data = c;
s->next = NULL;
Q->rear->next = s;
Q->rear = s;
}
int DeQueue(LinkQueue *Q, Customer *c) {
if (Q->front == Q->rear) return 0;
QueuePtr p = Q->front->next;
*c = p->data;
Q->front->next = p->next;
if (Q->rear == p) Q->rear = Q->front;
free(p);
return 1;
}
int main() {
LinkQueue Q;
InitQueue(&Q);
// 模拟5个客户:(到达时间, 服务时长)
Customer customers[] = {{10, 5}, {15, 10}, {18, 3}, {20, 7}, {25, 4}};
int n = 5;
for (int i = 0; i < n; i++) {
EnQueue(&Q, customers[i]);
}
int currentTime = 0;
int totalWait = 0;
Customer c;
while (DeQueue(&Q, &c)) {
if (c.arriveTime > currentTime) {
currentTime = c.arriveTime;
}
totalWait += currentTime - c.arriveTime;
currentTime += c.serviceTime;
}
printf("平均等待时间:%.1f分钟\n", (float)totalWait / n); // 输出:2.4分钟
return 0;
}
📝 章结
栈和队列作为两种受限的线性表,通过约束操作顺序(LIFO/FIFO),在解决特定问题时展现出独特优势:
-
栈的核心价值在于"回溯"与"嵌套"场景,如表达式求值、递归实现、浏览器历史记录等,其"后进先出"特性完美匹配"最后处理的内容最先收尾"的逻辑。
-
队列的核心价值在于"公平调度"与"顺序处理"场景,如任务队列、消息缓冲、排队系统等,其"先进先出"特性确保了操作的有序性与公平性。
从实现角度看:
- 顺序存储(顺序栈、循环队列)适合已知规模、追求效率的场景;
- 链式存储(链栈、链队)适合动态规模、避免空间浪费的场景。
理解栈和队列,不仅是掌握两种数据结构,更是学会通过"限制操作"简化问题------在复杂系统中,合理的约束往往比无限制的灵活性更有价值。