1. 栈与队列:程序世界的交通管制员
在计算机的世界里,数据流动就像繁忙的城市交通,而栈和队列就是两位尽职的交通管制员。它们虽然都是线性表,但管理数据的方式截然不同。栈遵循"后进先出"(LIFO)原则,就像餐厅里叠放的餐盘,你总是取走最上面的那个;队列则遵循"先进先出"(FIFO)原则,如同排队买票,先来的人先得到服务。
这两种数据结构在编程中无处不在:函数调用使用栈来管理,打印任务使用队列来调度,甚至浏览器前进后退功能也依赖栈实现。理解它们的特性和实现方式,是每个程序员必须掌握的基本功。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 栈的定义与实现
2.1 栈的基本概念
栈(Stack)是一种操作受限的线性表,只允许在表的一端(称为栈顶)进行插入和删除操作。想象一摞书,你只能从最上面取书或放书,这就是栈的工作方式。
栈有两个基本操作:
- 压栈(Push):将元素放入栈顶
- 弹栈(Pop):移除并返回栈顶元素
此外,通常还会提供:
- 查看栈顶元素(Peek/Top)
- 判断栈是否为空(isEmpty)
- 获取栈的大小(Size)
2.2 栈的数组实现
用数组实现栈是最直观的方式。我们需要维护一个数组和一个指向栈顶的指针(或索引)。
c复制#define MAX_SIZE 100
typedef struct {
int data[MAX_SIZE];
int top;
} ArrayStack;
void initStack(ArrayStack *s) {
s->top = -1; // 初始化栈顶指针为-1表示空栈
}
int isEmpty(ArrayStack *s) {
return s->top == -1;
}
int isFull(ArrayStack *s) {
return s->top == MAX_SIZE - 1;
}
void push(ArrayStack *s, int value) {
if (isFull(s)) {
printf("Stack overflow\n");
return;
}
s->data[++s->top] = value;
}
int pop(ArrayStack *s) {
if (isEmpty(s)) {
printf("Stack underflow\n");
return -1; // 错误码
}
return s->data[s->top--];
}
int peek(ArrayStack *s) {
if (isEmpty(s)) {
printf("Stack is empty\n");
return -1;
}
return s->data[s->top];
}
注意:数组实现的栈有固定大小限制,当栈满时继续压栈会导致"栈溢出"(Stack Overflow),这也是那个著名编程问答网站名字的由来。
2.3 栈的链表实现
链表实现的栈可以动态增长,不受固定大小限制。通常我们使用单链表,在表头进行插入和删除操作,这样时间复杂度都是O(1)。
c复制typedef struct StackNode {
int data;
struct StackNode *next;
} StackNode;
typedef struct {
StackNode *top;
int size;
} LinkedStack;
void initLinkedStack(LinkedStack *s) {
s->top = NULL;
s->size = 0;
}
void pushLinked(LinkedStack *s, int value) {
StackNode *newNode = (StackNode*)malloc(sizeof(StackNode));
if (!newNode) {
printf("Memory allocation failed\n");
return;
}
newNode->data = value;
newNode->next = s->top;
s->top = newNode;
s->size++;
}
int popLinked(LinkedStack *s) {
if (s->top == NULL) {
printf("Stack underflow\n");
return -1;
}
StackNode *temp = s->top;
int value = temp->data;
s->top = temp->next;
free(temp);
s->size--;
return value;
}
2.4 栈的应用场景
-
函数调用栈:每次函数调用时,系统会将返回地址、参数和局部变量压入调用栈;函数返回时再弹出这些信息。
-
表达式求值:编译器使用栈来处理运算符优先级和中缀表达式转后缀表达式。
-
括号匹配:检查代码中的括号是否成对出现。
-
浏览器历史记录:前进后退功能使用两个栈实现。
-
撤销操作(Undo):许多编辑器将操作历史保存在栈中。
实操心得:在解决需要"回退"或"反向处理"的问题时,优先考虑栈结构。例如深度优先搜索(DFS)就天然适合用栈实现。
3. 队列的定义与实现
3.1 队列的基本概念
队列(Queue)是另一种操作受限的线性表,它只允许在表的一端(队尾)进行插入,在另一端(队首)进行删除。这就像现实中的排队,新来的人排在队尾,服务从队首开始。
队列的基本操作包括:
- 入队(Enqueue):在队尾添加元素
- 出队(Dequeue):移除并返回队首元素
- 查看队首元素(Front)
- 判断队列是否为空(isEmpty)
- 获取队列大小(Size)
3.2 队列的数组实现
用数组实现队列比栈复杂一些,因为需要在两端操作。简单的实现会导致"假溢出"问题——数组前面有空位但无法使用。解决方案是使用循环队列。
c复制#define QUEUE_SIZE 100
typedef struct {
int data[QUEUE_SIZE];
int front; // 队首索引
int rear; // 队尾索引
int size; // 当前元素数量
} ArrayQueue;
void initQueue(ArrayQueue *q) {
q->front = 0;
q->rear = -1;
q->size = 0;
}
int isFull(ArrayQueue *q) {
return q->size == QUEUE_SIZE;
}
int isEmpty(ArrayQueue *q) {
return q->size == 0;
}
void enqueue(ArrayQueue *q, int value) {
if (isFull(q)) {
printf("Queue is full\n");
return;
}
q->rear = (q->rear + 1) % QUEUE_SIZE; // 循环利用空间
q->data[q->rear] = value;
q->size++;
}
int dequeue(ArrayQueue *q) {
if (isEmpty(q)) {
printf("Queue is empty\n");
return -1;
}
int value = q->data[q->front];
q->front = (q->front + 1) % QUEUE_SIZE;
q->size--;
return value;
}
3.3 队列的链表实现
链表实现的队列更加灵活,没有固定大小限制。通常我们维护两个指针:一个指向队首,一个指向队尾。
c复制typedef struct QueueNode {
int data;
struct QueueNode *next;
} QueueNode;
typedef struct {
QueueNode *front;
QueueNode *rear;
int size;
} LinkedQueue;
void initLinkedQueue(LinkedQueue *q) {
q->front = q->rear = NULL;
q->size = 0;
}
void enqueueLinked(LinkedQueue *q, int value) {
QueueNode *newNode = (QueueNode*)malloc(sizeof(QueueNode));
if (!newNode) {
printf("Memory allocation failed\n");
return;
}
newNode->data = value;
newNode->next = NULL;
if (q->rear == NULL) { // 空队列
q->front = q->rear = newNode;
} else {
q->rear->next = newNode;
q->rear = newNode;
}
q->size++;
}
int dequeueLinked(LinkedQueue *q) {
if (q->front == NULL) {
printf("Queue is empty\n");
return -1;
}
QueueNode *temp = q->front;
int value = temp->data;
q->front = q->front->next;
if (q->front == NULL) { // 队列变空
q->rear = NULL;
}
free(temp);
q->size--;
return value;
}
3.4 队列的应用场景
-
任务调度:操作系统使用队列管理进程和线程。
-
消息队列:分布式系统中用于解耦生产者和消费者。
-
广度优先搜索(BFS):图的遍历算法天然适合队列结构。
-
打印机任务队列:管理多个打印请求。
-
实时系统:处理按时间顺序到达的事件。
注意事项:在多线程环境下使用队列时,必须考虑线程安全问题。标准库通常提供线程安全的队列实现,如Java的BlockingQueue或C++的std::queue配合互斥锁使用。
4. 循环队列:解决空间浪费问题
4.1 循环队列的概念
普通数组实现的队列在出队操作后,前面的空间无法再利用,造成存储空间浪费。循环队列通过将数组视为环形结构来解决这个问题——当指针到达数组末尾时,自动绕回到开头。
循环队列的关键是:
- 队尾指针rear总是指向下一个要插入的位置
- 队首指针front指向当前第一个元素
- 队列为空:front == rear
- 队列为满:(rear + 1) % size == front
4.2 循环队列的实现
c复制typedef struct {
int *data; // 动态数组
int front; // 队首
int rear; // 队尾
int capacity; // 总容量
} CircularQueue;
CircularQueue* createCircularQueue(int k) {
CircularQueue *q = (CircularQueue*)malloc(sizeof(CircularQueue));
q->data = (int*)malloc(k * sizeof(int));
q->front = q->rear = 0;
q->capacity = k;
return q;
}
int isFull(CircularQueue* q) {
return (q->rear + 1) % q->capacity == q->front;
}
int isEmpty(CircularQueue* q) {
return q->front == q->rear;
}
int enqueueCircular(CircularQueue* q, int value) {
if (isFull(q)) {
printf("Queue is full\n");
return 0;
}
q->data[q->rear] = value;
q->rear = (q->rear + 1) % q->capacity;
return 1;
}
int dequeueCircular(CircularQueue* q) {
if (isEmpty(q)) {
printf("Queue is empty\n");
return -1;
}
int value = q->data[q->front];
q->front = (q->front + 1) % q->capacity;
return value;
}
int front(CircularQueue* q) {
if (isEmpty(q)) {
return -1;
}
return q->data[q->front];
}
int rear(CircularQueue* q) {
if (isEmpty(q)) {
return -1;
}
return q->data[(q->rear - 1 + q->capacity) % q->capacity];
}
4.3 循环队列的变种与优化
-
双端队列(Deque):允许在两端进行插入和删除操作,兼具栈和队列的特性。
-
优先级队列:元素按优先级出队,而非插入顺序,通常用堆实现。
-
阻塞队列:当队列为空时,出队操作阻塞;队列满时,入队操作阻塞。
-
动态扩容循环队列:当队列满时自动扩容,类似于动态数组的实现。
实操技巧:在实现循环队列时,常见的一个坑是区分队列满和队列空的条件。通常我们会牺牲一个存储单元来区分这两种情况(即队列满时数组中有一个空位)。另一种方法是维护一个额外的size变量记录当前元素数量。
5. 栈与队列的算法应用
5.1 使用栈实现队列
用两个栈可以模拟队列的行为:
- 一个栈用于入队(stack1)
- 另一个栈用于出队(stack2)
- 当stack2为空时,将stack1的所有元素弹出并压入stack2
c复制typedef struct {
ArrayStack stack1; // 用于入队
ArrayStack stack2; // 用于出队
} MyQueue;
void myQueuePush(MyQueue* obj, int x) {
push(&obj->stack1, x); // 直接压入stack1
}
int myQueuePop(MyQueue* obj) {
if (isEmpty(&obj->stack2)) {
// 将stack1的元素全部转移到stack2
while (!isEmpty(&obj->stack1)) {
push(&obj->stack2, pop(&obj->stack1));
}
}
return pop(&obj->stack2);
}
int myQueuePeek(MyQueue* obj) {
if (isEmpty(&obj->stack2)) {
while (!isEmpty(&obj->stack1)) {
push(&obj->stack2, pop(&obj->stack1));
}
}
return peek(&obj->stack2);
}
5.2 使用队列实现栈
同样可以用两个队列实现栈的功能:
- 始终保持一个队列为空
- 入栈时将元素加入非空队列
- 出栈时将非空队列的前n-1个元素转移到空队列,然后弹出最后一个元素
c复制typedef struct {
ArrayQueue queue1;
ArrayQueue queue2;
} MyStack;
void myStackPush(MyStack* obj, int x) {
if (!isEmpty(&obj->queue1)) {
enqueue(&obj->queue1, x);
} else {
enqueue(&obj->queue2, x);
}
}
int myStackPop(MyStack* obj) {
ArrayQueue *emptyQ, *nonEmptyQ;
if (isEmpty(&obj->queue1)) {
emptyQ = &obj->queue1;
nonEmptyQ = &obj->queue2;
} else {
emptyQ = &obj->queue2;
nonEmptyQ = &obj->queue1;
}
// 将非空队列的前n-1个元素转移到空队列
while (nonEmptyQ->size > 1) {
enqueue(emptyQ, dequeue(nonEmptyQ));
}
return dequeue(nonEmptyQ); // 返回最后一个元素
}
5.3 经典算法问题
- 有效的括号(栈的应用):
给定一个只包括 '(', ')', '{', '}', '[', ']' 的字符串,判断字符串是否有效。
c复制bool isValid(char * s) {
char stack[10000];
int top = -1;
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == '(' || s[i] == '{' || s[i] == '[') {
stack[++top] = s[i];
} else {
if (top == -1) return false;
char c = stack[top--];
if ((s[i] == ')' && c != '(') ||
(s[i] == '}' && c != '{') ||
(s[i] == ']' && c != '[')) {
return false;
}
}
}
return top == -1;
}
- 滑动窗口最大值(双端队列的应用):
给定一个数组和滑动窗口的大小,找出所有滑动窗口里的最大值。
c复制int* maxSlidingWindow(int* nums, int numsSize, int k, int* returnSize) {
if (numsSize == 0) {
*returnSize = 0;
return NULL;
}
*returnSize = numsSize - k + 1;
int *result = (int*)malloc(*returnSize * sizeof(int));
int deque[numsSize];
int front = 0, rear = -1;
for (int i = 0; i < numsSize; i++) {
// 移除不在窗口内的元素
while (front <= rear && deque[front] < i - k + 1) {
front++;
}
// 移除小于当前元素的队列元素
while (front <= rear && nums[deque[rear]] < nums[i]) {
rear--;
}
// 添加当前元素索引
deque[++rear] = i;
// 记录窗口最大值
if (i >= k - 1) {
result[i - k + 1] = nums[deque[front]];
}
}
return result;
}
- 逆波兰表达式求值(栈的应用):
根据逆波兰表示法(后缀表达式),求表达式的值。
c复制int evalRPN(char ** tokens, int tokensSize) {
int stack[10000];
int top = -1;
for (int i = 0; i < tokensSize; i++) {
if (strcmp(tokens[i], "+") == 0) {
int b = stack[top--];
int a = stack[top--];
stack[++top] = a + b;
} else if (strcmp(tokens[i], "-") == 0) {
int b = stack[top--];
int a = stack[top--];
stack[++top] = a - b;
} else if (strcmp(tokens[i], "*") == 0) {
int b = stack[top--];
int a = stack[top--];
stack[++top] = a * b;
} else if (strcmp(tokens[i], "/") == 0) {
int b = stack[top--];
int a = stack[top--];
stack[++top] = a / b;
} else {
stack[++top] = atoi(tokens[i]);
}
}
return stack[top];
}
6. 性能分析与比较
6.1 时间复杂度比较
| 操作 | 栈(数组/链表) | 队列(数组) | 队列(链表) | 循环队列 |
|---|---|---|---|---|
| 插入 | O(1) | O(1)* | O(1) | O(1) |
| 删除 | O(1) | O(n) | O(1) | O(1) |
| 访问顶部 | O(1) | O(1) | O(1) | O(1) |
| 搜索 | O(n) | O(n) | O(n) | O(n) |
*注:普通数组实现的队列出队操作需要移动所有元素,时间复杂度为O(n);循环队列和链表实现则为O(1)。
6.2 空间复杂度比较
- 数组实现:固定大小,可能浪费空间
- 链表实现:动态增长,每个元素需要额外指针空间
- 循环队列:固定大小,但利用率更高
6.3 选择建议
-
选择栈实现:
- 数组实现:适合大小固定或可预估的场景,性能更好
- 链表实现:适合大小变化大的场景,无最大限制
-
选择队列实现:
- 循环队列:大多数场景下的首选,性能平衡
- 链表队列:无大小限制,但每个元素需要额外空间
- 普通数组队列:不推荐,出队性能差
-
特殊需求:
- 需要双端操作:选择双端队列(Deque)
- 需要优先级:选择优先级队列(通常用堆实现)
- 多线程环境:选择线程安全的阻塞队列
7. 常见问题与调试技巧
7.1 栈相关问题排查
-
栈溢出(Stack Overflow)
- 现象:程序崩溃,错误信息包含"stack overflow"
- 原因:递归太深或局部变量占用空间过大
- 解决:改用迭代算法或动态分配内存
-
内存访问越界
- 现象:随机崩溃或数据损坏
- 原因:未检查栈空就执行pop操作
- 解决:所有pop操作前检查isEmpty()
7.2 队列相关问题排查
-
假溢出问题
- 现象:队列报告已满但数组中有空位
- 原因:使用普通数组实现队列
- 解决:改用循环队列实现
-
队列指针错乱
- 现象:数据丢失或顺序错误
- 原因:front/rear指针更新逻辑错误
- 解决:仔细检查指针更新代码,特别是循环队列的模运算
7.3 调试技巧
-
可视化调试:
- 打印栈/队列的完整内容
- 对于循环队列,打印front、rear指针和数组内容
-
边界测试:
- 测试空栈/队列时的pop/dequeue操作
- 测试满栈/队列时的push/enqueue操作
- 测试单元素时的各种操作
-
压力测试:
- 大量随机操作验证正确性
- 长时间运行测试内存泄漏(链表实现)
避坑指南:在实现循环队列时,最容易犯的错误是判断队列满/空的条件。建议在纸上画出各种情况(空、满、部分满)的front/rear位置关系,确保逻辑正确。另一个常见错误是忘记更新size变量(如果有的话),导致计数不准确。
