栈和队列(C语言)
文章目录
- 前言
- 一、栈的概念、应用与结构
- 1.1 栈的定义与特性
- 1.2 栈的应用场景
- 1.3 栈的逻辑结构示意
- 二、栈的多种实现
- 2.1 顺序栈(基于数组)
- 2.1.1 数据结构
- 2.1.2 主要操作
- 2.1.3 优缺点
- 2.2 链式栈(基于单链表)
- 2.2.1 数据结构
- 2.2.2 主要操作
- 2.2.3 优缺点
- 三、队列的概念、应用与结构
- 3.1 队列的定义与特性
- 3.2 队列的应用场景
- 3.3 队列的逻辑结构示意
- 四、队列的多种实现
- 4.1 顺序队列(基于数组)
- 4.1.1 数据结构
- 4.1.2 主要操作
- 4.1.3 问题
- 4.2 循环队列
- 4.2.1 数据结构
- 4.2.2 主要操作
- 4.2.3 优点
- 4.3 链式队列
- 4.3.1 数据结构
- 4.3.2 主要操作
- 五、复杂度与选型建议
- 总结
前言
栈(Stack)和队列(Queue)是最基础、最常用的两种线性数据结构。
- 栈:遵循“后进先出(LIFO)”原则,常用于函数调用管理、表达式求值、括号匹配等场景;
- 队列:遵循“先进先出(FIFO)”原则,常用于任务调度、缓存管理、广度优先搜索等场景。
本篇博客将从概念、典型应用、实现细节,到性能分析,分步骤带你深入掌握各类实现方法。
一、栈的概念、应用与结构
1.1 栈的定义与特性
- 定义:栈是一种只能在一端进行插入和删除操作的线性结构。
- 操作:入栈(Push)、出栈(Pop)、取栈顶(Top/Peek)、判空(IsEmpty)。
- 特性:
- 后进先出(LIFO)
- 操作时间复杂度均为 O(1)
1.2 栈的应用场景
- 函数调用:程序运行时,操作系统用调用栈保存返回地址与局部变量。
- 表达式求值:中缀表达式转后缀、后缀表达式计算都依赖双栈算法。
- 括号匹配:编译器检查源代码括号是否成对匹配。
- 图的深度优先搜索(DFS):可用显式栈代替递归调用。
1.3 栈的逻辑结构示意
┌───────┐ ← 栈顶 (top)
│ 30 │
│ 20 │
│ 10 │
└───────┘ ← 栈底
二、栈的多种实现
2.1 顺序栈(基于数组)
2.1.1 数据结构
#define MAXSIZE 100typedef struct {int data[MAXSIZE];int top; // 指向当前栈顶元素的下标,-1 表示空栈
} SeqStack;
2.1.2 主要操作
// 初始化
void InitSeqStack(SeqStack *s) {s->top = -1;
}// 入栈
int PushSeq(SeqStack *s, int v) {if (s->top >= MAXSIZE - 1) return 0; // 栈满s->data[++s->top] = v;return 1;
}// 出栈
int PopSeq(SeqStack *s, int *v) {if (s->top == -1) return 0; // 栈空*v = s->data[s->top--];return 1;
}// 读栈顶
int TopSeq(SeqStack *s, int *v) {if (s->top == -1) return 0;*v = s->data[s->top];return 1;
}// 判空
int IsEmptySeq(SeqStack *s) {return s->top == -1;
}
2.1.3 优缺点
- 优点:实现简单、访问速度快。
- 缺点:容量固定,易出现“栈满”问题;插入和删除始终在尾部。
2.2 链式栈(基于单链表)
2.2.1 数据结构
typedef struct StackNode {int data;struct StackNode *next;
} StackNode, *LinkStack;
2.2.2 主要操作
// 初始化
void InitLinkStack(LinkStack *s) {*s = NULL;
}// 入栈:在链表头插入
void PushLink(LinkStack *s, int v) {StackNode *node = (StackNode*)malloc(sizeof(StackNode));node->data = v;node->next = *s;*s = node;
}// 出栈:删除链表头
int PopLink(LinkStack *s, int *v) {if (*s == NULL) return 0;StackNode *tmp = *s;*v = tmp->data;*s = tmp->next;free(tmp);return 1;
}// 取栈顶
int TopLink(LinkStack s, int *v) {if (s == NULL) return 0;*v = s->data;return 1;
}// 判空
int IsEmptyLink(LinkStack s) {return s == NULL;
}
2.2.3 优缺点
- 优点:动态分配,空间利用率高,可存储更多数据;
- 缺点:每次入/出栈都要 malloc/free,额外开销大。
三、队列的概念、应用与结构
3.1 队列的定义与特性
- 定义:队列是一种只能在一端插入(队尾)、另一端删除(队头)的线性结构。
- 操作:入队(Enqueue)、出队(Dequeue)、取队头(Front)、判空(IsEmpty)。
- 特性:
- 先进先出(FIFO)
- 基本操作时间复杂度均为 O(1)
3.2 队列的应用场景
- 操作系统进程调度:就绪队列按照先来先服务。
- 缓存/缓冲区:网络数据包、IO 缓冲区常用循环队列。
- 广度优先搜索(BFS):图和树遍历。
- 异步消息处理:生产者—消费者模型。
3.3 队列的逻辑结构示意
front → [10] [20] [30] ← rear
四、队列的多种实现
4.1 顺序队列(基于数组)
4.1.1 数据结构
#define MAXQ 100typedef struct {int data[MAXQ];int front; // 指向队头元素下标int rear; // 指向队尾下一个位置
} SeqQueue;
4.1.2 主要操作
// 初始化
void InitSeqQueue(SeqQueue *q) {q->front = q->rear = 0;
}// 入队
int EnQueue(SeqQueue *q, int v) {if (q->rear == MAXQ) return 0; // 队满q->data[q->rear++] = v;return 1;
}// 出队
int DeQueue(SeqQueue *q, int *v) {if (q->front == q->rear) return 0; // 队空*v = q->data[q->front++];return 1;
}// 取队头
int Front(SeqQueue *q, int *v) {if (q->front == q->rear) return 0;*v = q->data[q->front];return 1;
}// 判空
int IsEmptyQ(SeqQueue *q) {return q->front == q->rear;
}
4.1.3 问题
- 随着 DeQueue 操作,front 索引不断增大,数组前部空间无法复用。
4.2 循环队列
4.2.1 数据结构
#define MAXQ 100typedef struct {int data[MAXQ];int front; // 指向队头int rear; // 指向队尾的下一个位置
} CircQueue;
4.2.2 主要操作
// 初始化
void InitCircQueue(CircQueue *q) {q->front = q->rear = 0;
}// 判满:下一位置等于 front 则满
int IsFullCirc(CircQueue *q) {return (q->rear + 1) % MAXQ == q->front;
}// 入队
int EnCirc(CircQueue *q, int v) {if (IsFullCirc(q)) return 0;q->data[q->rear] = v;q->rear = (q->rear + 1) % MAXQ;return 1;
}// 出队
int DeCirc(CircQueue *q, int *v) {if (IsEmptyQ((SeqQueue*)q)) return 0; // front==rear*v = q->data[q->front];q->front = (q->front + 1) % MAXQ;return 1;
}
4.2.3 优点
- 空间利用率高,不会“假溢出”;
- 逻辑与顺序队列相同,但索引循环使用。
4.3 链式队列
4.3.1 数据结构
typedef struct QNode {int data;struct QNode *next;
} QNode, *QueuePtr;typedef struct {QueuePtr front, rear;
} LinkQueue;
4.3.2 主要操作
// 初始化
void InitLinkQueue(LinkQueue *q) {q->front = q->rear = (QueuePtr)malloc(sizeof(QNode));q->front->next = NULL;
}// 入队:在链尾插入
void EnLink(LinkQueue *q, int v) {QueuePtr node = (QueuePtr)malloc(sizeof(QNode));node->data = v;node->next = NULL;q->rear->next = node;q->rear = node;
}// 出队:删除链头第一个节点
int DeLink(LinkQueue *q, int *v) {if (q->front == q->rear) return 0; // 空队QueuePtr tmp = q->front->next;*v = tmp->data;q->front->next = tmp->next;if (q->rear == tmp) q->rear = q->front;free(tmp);return 1;
}
五、复杂度与选型建议
实现方式 | 空间复杂度 | 入/出操作 | 优点 | 适用场景 |
---|---|---|---|---|
顺序栈 | O(n) | O(1) | 简单、访问快 | 元素个数上限已知、对性能要求高 |
链式栈 | O(n) | O(1) | 动态、无栈满限制 | 元素个数动态、空间不预先分配 |
顺序队列 | O(n) | O(1) | 简单 | 入/出次数少、不关注循环复用 |
循环队列 | O(n) | O(1) | 空间高效、不需搬移元素 | 缓冲区、IO 队列、实时任务调度 |
链式队列 | O(n) | O(1) | 动态、无队满限制 | 元素动态、空闲链表场景 |
总结
- 理解本质:栈“后进先出”,队列“先进先出”;
- 灵活选型:根据元素规模、空间/性能需求,选用顺序或链式实现;
- 掌握变体:循环队列可解决数组队列的“假溢出”问题;