数据结构之栈和队列
数据结构之栈和队列
数据结构之栈(Stack)
1. 栈的定义
栈(Stack)是一种特殊的线性数据结构,遵循“后进先出”(LIFO, Last In First Out)的原则。也就是说,最后压入栈中的元素最先被弹出。栈可以被看作是一个有序集合,其操作主要包括添加(压栈)和移除(弹栈)元素。
栈的基本操作:
- 压栈(Push):将一个元素添加到栈顶。
- 弹栈(Pop):从栈顶移除并返回一个元素。
- 查看栈顶元素(Peek/Top):返回栈顶的元素,但不移除它。
- 判断栈是否为空(IsEmpty):检查栈中是否有元素。
- 获取栈的大小(Size):返回栈中元素的数量。
2. 栈的存储结构
栈可以通过顺序存储(数组)或链式存储(链表)两种方式实现。
2.1 顺序存储实现
使用数组实现栈的顺序存储。
#include <stdio.h>
#include <stdlib.h>#define MAX_SIZE 100 // 定义栈的最大容量typedef struct {int data[MAX_SIZE]; // 存储栈元素int top; // 栈顶指针
} Stack;// 初始化栈
void initStack(Stack *s) {s->top = -1; // 栈为空时,栈顶指针为 -1
}// 判断栈是否为空
int isEmpty(Stack *s) {return s->top == -1;
}// 判断栈是否满
int isFull(Stack *s) {return s->top == MAX_SIZE - 1;
}// 压栈
void push(Stack *s, int value) {if (isFull(s)) {printf("栈满,无法压栈!\n");return;}s->data[++s->top] = value; // 先增加栈顶指针,然后赋值
}// 弹栈
int pop(Stack *s) {if (isEmpty(s)) {printf("栈空,无法弹栈!\n");return -1; // 返回一个错误值}return s->data[s->top--]; // 返回栈顶元素,然后减少栈顶指针
}// 查看栈顶元素
int peek(Stack *s) {if (isEmpty(s)) {printf("栈空,无法查看栈顶元素!\n");return -1; // 返回一个错误值}return s->data[s->top];
}// 测试栈的功能
int main() {Stack s;initStack(&s);push(&s, 10);push(&s, 20);push(&s, 30);printf("栈顶元素: %d\n", peek(&s)); // 输出: 30while (!isEmpty(&s)) {printf("弹栈元素: %d\n", pop(&s));}pop(&s); // 尝试从空栈弹栈return 0;
}
2.2 链式存储实现
使用链表来实现栈的链式存储。
#include <stdio.h>
#include <stdlib.h>// 链表节点结构
typedef struct Node {int data;struct Node *next;
} Node;// 栈结构
typedef struct {Node *top; // 栈顶指针
} Stack;// 初始化栈
void initStack(Stack *s) {s->top = NULL;
}// 判断栈是否为空
int isEmpty(Stack *s) {return s->top == NULL;
}// 压栈
void push(Stack *s, int value) {Node *newNode = (Node *)malloc(sizeof(Node));newNode->data = value;newNode->next = s->top; // 新节点指向原栈顶s->top = newNode; // 更新栈顶
}// 弹栈
int pop(Stack *s) {if (isEmpty(s)) {printf("栈空,无法弹栈!\n");return -1; // 返回一个错误值}Node *temp = s->top; // 临时保存当前栈顶int poppedValue = temp->data;s->top = s->top->next; // 更新栈顶free(temp); // 释放旧栈顶的内存return poppedValue;
}// 查看栈顶元素
int peek(Stack *s) {if (isEmpty(s)) {printf("栈空,无法查看栈顶元素!\n");return -1; // 返回一个错误值}return s->top->data;
}// 测试栈的功能
int main() {Stack s;initStack(&s);push(&s, 10);push(&s, 20);push(&s, 30);printf("栈顶元素: %d\n", peek(&s)); // 输出: 30while (!isEmpty(&s)) {printf("弹栈元素: %d\n", pop(&s));}pop(&s); // 尝试从空栈弹栈return 0;
}
3. 时间复杂度
所有基本操作的时间复杂度均为 O(1):
- 插入、删除仅涉及栈顶的操作,无需遍历。
4. 栈的应用
栈在计算机科学中有许多重要的应用,包括但不限于:
- 函数调用管理:程序执行时的函数调用和返回过程使用栈来存储局部变量和返回地址。
- 表达式求值:在计算器中,栈被用于解析和计算表达式(如中缀表达式转换成后缀表达式)。
- 括号匹配:检查表达式中的括号是否匹配(如
()
,{}
,[]
)。 - 深度优先搜索(DFS):树或图的深度优先遍历可以使用栈来实现。
- 撤销操作:在文本编辑器中,用户的操作可以通过栈来管理,以实现撤销功能。
数据结构之队列(Queue)
1. 队列的定义
队列(Queue)是一种特殊的线性数据结构,遵循“先进先出”(FIFO, First In First Out)的原则。也就是说,最先加入队列的元素最先被移除。队列可以看作是一个有序集合,它的操作主要包括添加(入队)和移除(出队)元素。
队列的基本操作:
- 入队(Enqueue):将一个元素添加到队列的尾部。
- 出队(Dequeue):从队列的头部移除并返回一个元素。
- 查看队头元素(Front/Peek):返回队列头部的元素,但不移除它。
- 判断队列是否为空(IsEmpty):检查队列中是否有元素。
- 获取队列的大小(Size):返回队列中元素的数量。
2. 队列的存储结构
队列可以通过顺序存储(数组)或链式存储(链表)两种方式实现。
2.1 顺序存储实现
使用数组实现队列的顺序存储。
#include <stdio.h>
#include <stdlib.h>#define MAX_SIZE 100 // 定义队列的最大容量typedef struct {int data[MAX_SIZE]; // 存储队列元素int front; // 队头指针int rear; // 队尾指针
} Queue;// 初始化队列
void initQueue(Queue *q) {q->front = 0; // 队头指针初始化为 0q->rear = 0; // 队尾指针初始化为 0
}// 判断队列是否为空
int isEmpty(Queue *q) {return q->front == q->rear;
}// 判断队列是否满
int isFull(Queue *q) {return (q->rear + 1) % MAX_SIZE == q->front;
}// 入队
void enqueue(Queue *q, int value) {if (isFull(q)) {printf("队列满,无法入队!\n");return;}q->data[q->rear] = value; // 将元素添加到队尾q->rear = (q->rear + 1) % MAX_SIZE; // 更新队尾指针
}// 出队
int dequeue(Queue *q) {if (isEmpty(q)) {printf("队列空,无法出队!\n");return -1; // 返回一个错误值}int value = q->data[q->front]; // 获取队头元素q->front = (q->front + 1) % MAX_SIZE; // 更新队头指针return value;
}// 查看队头元素
int front(Queue *q) {if (isEmpty(q)) {printf("队列空,无法查看队头元素!\n");return -1; // 返回一个错误值}return q->data[q->front];
}// 测试队列的功能
int main() {Queue q;initQueue(&q);enqueue(&q, 10);enqueue(&q, 20);enqueue(&q, 30);printf("队头元素: %d\n", front(&q)); // 输出: 10while (!isEmpty(&q)) {printf("出队元素: %d\n", dequeue(&q));}dequeue(&q); // 尝试从空队列出队return 0;
}
2.2 链式存储实现
使用链表实现队列的链式存储。
#include <stdio.h>
#include <stdlib.h>// 链表节点结构
typedef struct Node {int data;struct Node *next;
} Node;// 队列结构
typedef struct {Node *front; // 队头指针Node *rear; // 队尾指针
} Queue;// 初始化队列
void initQueue(Queue *q) {q->front = NULL;q->rear = NULL;
}// 判断队列是否为空
int isEmpty(Queue *q) {return q->front == NULL;
}// 入队
void enqueue(Queue *q, int value) {Node *newNode = (Node *)malloc(sizeof(Node));newNode->data = value;newNode->next = NULL;if (isEmpty(q)) {q->front = newNode; // 如果队列为空,队头和队尾都指向新节点} else {q->rear->next = newNode; // 将新节点链接到队尾}q->rear = newNode; // 更新队尾指针
}// 出队
int dequeue(Queue *q) {if (isEmpty(q)) {printf("队列空,无法出队!\n");return -1; // 返回一个错误值}Node *temp = q->front; // 临时保存当前队头int value = temp->data;q->front = q->front->next; // 更新队头指针if (q->front == NULL) { // 如果队列变为空,更新队尾指针q->rear = NULL;}free(temp); // 释放旧队头的内存return value;
}// 查看队头元素
int front(Queue *q) {if (isEmpty(q)) {printf("队列空,无法查看队头元素!\n");return -1; // 返回一个错误值}return q->front->data;
}// 测试队列的功能
int main() {Queue q;initQueue(&q);enqueue(&q, 10);enqueue(&q, 20);enqueue(&q, 30);printf("队头元素: %d\n", front(&q)); // 输出: 10while (!isEmpty(&q)) {printf("出队元素: %d\n", dequeue(&q));}dequeue(&q); // 尝试从空队列出队return 0;
}
3. 队列的变种
除了基本的队列之外,还有几个常见的队列变种,每个变种具有不同的特性和用途:
3.1 双端队列(Deque)
双端队列(Deque, Double-Ended Queue)是一种允许在两端插入和删除元素的队列。它可以被视为一个可以从前端或后端进行操作的队列。
- 操作:
push_front
: 在队头插入元素。push_back
: 在队尾插入元素。pop_front
: 从队头移除元素。pop_back
: 从队尾移除元素。
使用链表实现双端队列:
#include <stdio.h>
#include <stdlib.h>// 链表节点结构
typedef struct Node {int data;struct Node *next;struct Node *prev; // 指向前一个节点
} Node;// 双端队列结构
typedef struct {Node *front; // 队头指针Node *rear; // 队尾指针
} Deque;// 初始化双端队列
void initDeque(Deque *dq) {dq->front = NULL;dq->rear = NULL;
}// 判断双端队列是否为空
int isEmpty(Deque *dq) {return dq->front == NULL;
}// 从队头插入
void push_front(Deque *dq, int value) {Node *newNode = (Node *)malloc(sizeof(Node));newNode->data = value;newNode->next = dq->front;newNode->prev = NULL;if (isEmpty(dq)) {dq->rear = newNode; // 如果队列为空,队头和队尾都指向新节点} else {dq->front->prev = newNode; // 更新原队头的前向指针}dq->front = newNode; // 更新队头指针
}// 从队尾插入
void push_back(Deque *dq, int value) {Node *newNode = (Node *)malloc(sizeof(Node));newNode->data = value;newNode->next = NULL;if (isEmpty(dq)) {newNode->prev = NULL; // 队列为空时,前向指针为空dq->front = newNode; // 队头指向新节点} else {newNode->prev = dq->rear; // 更新新节点的前向指针dq->rear->next = newNode; // 更新原队尾的后向指针}dq->rear = newNode; // 更新队尾指针
}// 从队头移除
int pop_front(Deque *dq) {if (isEmpty(dq)) {printf("双端队列空,无法出队!\n");return -1;}Node *temp = dq->front;int value = temp->data;dq->front = dq->front->next; // 更新队头if (dq->front != NULL) {dq->front->prev = NULL; // 更新新的队头的前向指针} else { dq->rear = NULL; // 如果队列变为空,更新队尾指针}free(temp); // 释放旧队头的内存return value;
}// 从队尾移除
int pop_back(Deque *dq) {if (isEmpty(dq)) {printf("双端队列空,无法出队!\n");return -1;}Node *temp = dq->rear;int value = temp->data;dq->rear = dq->rear->prev; // 更新队尾if (dq->rear != NULL) {dq->rear->next = NULL; // 更新新的队尾的后向指针} else {dq->front = NULL; // 如果队列变为空,更新队头指针}free(temp); // 释放旧队尾的内存return value;
}// 测试双端队列功能
int main() {Deque dq;initDeque(&dq);push_back(&dq, 10);push_back(&dq, 20);push_front(&dq, 5);printf("队头元素: %d\n", pop_front(&dq)); // 输出: 5printf("队尾元素: %d\n", pop_back(&dq)); // 输出: 20return 0;
}
3.2 循环队列(Circular Queue)
循环队列(Circular Queue)是一种特殊的队列实现方式,它利用数组的循环性质来有效地使用空间,避免因“队列满”而无法插入元素的情况,即使数组中有空闲的位置。
在循环队列中,队头和队尾指针以环形方式移动。
下面是循环队列的实现:
#include <stdio.h>
#include <stdlib.h>#define MAX_SIZE 5 // 定义队列的最大容量typedef struct {int data[MAX_SIZE]; // 存储队列元素int front; // 队头指针int rear; // 队尾指针
} CircularQueue;// 初始化循环队列
void initCircularQueue(CircularQueue *q) {q->front = 0;q->rear = 0;
}// 判断循环队列是否为空
int isEmpty(CircularQueue *q) {return q->front == q->rear;
}// 判断循环队列是否满
int isFull(CircularQueue *q) {return (q->rear + 1) % MAX_SIZE == q->front;
}// 入队
void enqueue(CircularQueue *q, int value) {if (isFull(q)) {printf("循环队列满,无法入队!\n");return;}q->data[q->rear] = value; // 将元素添加到队尾q->rear = (q->rear + 1) % MAX_SIZE; // 更新队尾指针
}// 出队
int dequeue(CircularQueue *q) {if (isEmpty(q)) {printf("循环队列空,无法出队!\n");return -1; // 返回一个错误值}int value = q->data[q->front]; // 获取队头元素q->front = (q->front + 1) % MAX_SIZE; // 更新队头指针return value;
}// 查看队头元素
int front(CircularQueue *q) {if (isEmpty(q)) {printf("循环队列空,无法查看队头元素!\n");return -1; // 返回一个错误值}return q->data[q->front];
}// 测试循环队列的功能
int main() {CircularQueue q;initCircularQueue(&q);enqueue(&q, 10);enqueue(&q, 20);enqueue(&q, 30);enqueue(&q, 40);enqueue(&q, 50); // 这里应该会提示队列满printf("队头元素: %d\n", front(&q)); // 输出: 10while (!isEmpty(&q)) {printf("出队元素: %d\n", dequeue(&q));}dequeue(&q); // 尝试从空队列出队return 0;
}
4. 队列的复杂度分析
队列的基本操作(入队、出队、查看队头元素)的时间复杂度均为 O(1),无论是顺序存储、链式存储还是循环队列。
- 空间复杂度:
- 顺序队列:O(n),其中 n 是队列的最大容量。
- 链式队列:O(n),其中 n 是队列中当前元素的数量。
- 循环队列:O(n),同样取决于最大容量。
5. 队列的应用
队列在计算机科学中有许多重要的应用,包括但不限于:
- 任务调度:操作系统中的进程调度、线程池中的任务管理等。
- 宽度优先搜索(BFS):图的遍历算法使用队列来维护当前节点。
- 缓冲区:在输入输出处理中,使用队列作为缓冲区(如打印队列、网络数据包处理)。
- 消息队列:用于异步消息传递和事件处理的机制。
- 流量控制:在网络通信中,使用队列来管理流量和数据包。