一. 栈(stack)
栈(Stack)是一种线性数据结构,它遵循**后进先出(LIFO, Last In First Out)**的原则。换句话说,最新添加到栈中的元素将最先被移除,而最早添加的元素最后被移除。这种特性使得栈在许多算法和系统操作中具有重要作用。
1.栈的基本操作
栈通常支持以下几种基本操作:
-
入栈(Push):
- 将一个元素添加到栈的顶端。
- 如果栈有大小限制,则在栈满时可能无法进行入栈操作。
-
出栈(Pop):
- 移除并返回栈顶元素。
- 如果栈为空,出栈操作通常会返回一个错误或特殊的值(例如,
null
或-1
),表示栈为空。
-
查看栈顶元素(Peek/Top):
- 返回栈顶元素但不移除它。
- 这个操作通常用于查看栈顶元素而不影响栈的内容。
-
判空(IsEmpty):
- 检查栈是否为空。如果为空,返回
true
,否则返回false
。
- 检查栈是否为空。如果为空,返回
2.栈的应用场景
由于栈的 LIFO 特性,它在许多场景中被广泛应用:
-
函数调用堆栈:
- 在程序执行过程中,每次函数调用都会在栈中创建一个新的栈帧(包含函数的局部变量、参数等),当函数返回时,该栈帧会被弹出。
-
表达式求值与语法解析:
- 在编译器中,栈用于中缀表达式转后缀表达式(逆波兰表示法)以及表达式的求值。
- 例如,处理带括号的数学表达式时,栈可以帮助解析表达式的优先级。
-
撤销操作:
- 许多软件(例如文本编辑器)通过栈来实现“撤销”功能。每次操作都会被记录在栈中,当用户选择撤销时,栈顶的操作会被反向执行并移除。
-
深度优先搜索(DFS):
- 栈可以用来实现图的深度优先搜索算法。
3.栈的实现
栈可以通过两种主要方式实现:
-
数组实现(静态栈):
- 使用固定大小的数组实现栈结构。栈顶的索引随着入栈和出栈操作而变化。
- 优点:实现简单,操作速度快。
- 缺点:栈的大小是固定的,可能会出现溢出问题。
-
链表实现(动态栈):
- 使用链表节点来实现栈,每个节点包含一个数据元素和指向下一个节点的指针。
- 优点:栈的大小不受固定限制,只要内存允许,可以动态增长。
- 缺点:需要额外的内存来存储指针,操作速度相对数组实现稍慢。
4.代码实现
#include <stdio.h>
#include <stdlib.h>
typedef struct stack
{
int *data; //数组
int capacity; //容量
int top; //栈顶索引
}Stack;
void init(Stack *s);
void push(Stack *s, int value);
//出栈,删除栈顶的值
int pop(Stack *s);
//读取,不删除
int peek(Stack *s);
int size(Stack *s);
int main(int argc, char const *argv)
{
Stack *s = malloc(sizeof(Stack));
init(s);
push(s, 100);
push(s, 200);
push(s, 300);
printf("%d\n",size(s));
printf("%d\n", pop(s));
printf("%d\n", pop(s));
printf("%d\n", pop(s));
printf("%d\n",size(s));
return 0;
}
void init(Stack *s)
{
s->capacity = 8;
s->data = malloc(sizeof(int) * s->capacity);
s->top = -1;
}
void push(Stack *s, int value)
{
//是否满
s->data[++s->top] = value;
}
int pop(Stack *s)
{
if (s->top == -1)
{
printf("栈为空\n");
}
else
return s->data[s->top--];
}
int size(Stack *s)
{
return s->top + 1;
}
int peek(Stack *s)
{
if (s->top == -1)
{
printf("栈为空\n");
}
else
return s->data[s->top];
}
二. 队列(queue)
队列是一种先进先出(FIFO, First-In-First-Out)的数据结构。它类似于现实生活中的排队规则,即最先排队的人最先被服务。在计算机科学中,队列被广泛用于各种应用场景,如任务调度、数据缓冲等。
1.队列的基本操作
-
入队(Enqueue):
将一个元素添加到队列的尾部。 -
出队(Dequeue):
移除并返回队列的头部元素。因为是 FIFO 结构,最早进入队列的元素最先被移除。 -
查看队头元素(Peek or Front):
返回队列的头部元素,但不移除它。 -
检查队列是否为空:
判断队列中是否有元素。 -
检查队列是否已满(对于固定大小的队列):
判断队列是否已达到其容量限制。
2.队列的应用场景
-
任务调度:
在操作系统中,任务调度器通常使用队列来管理等待执行的任务。 -
打印队列:
打印机通常会将待打印的文件放入队列中,按照顺序逐一处理。 -
广度优先搜索(BFS):
在图或树的广度优先搜索算法中,队列用于按层次顺序存储节点。 -
数据流处理:
在流式数据处理中,队列用于缓存数据以便按顺序处理。
3.队列的实现
队列可以通过多种方式实现,最常见的有以下几种:
-
数组实现:
- 使用数组来保存队列中的元素。通过两个指针(或索引)来指示队列的头部和尾部。当元素入队时,尾部指针向后移动;当元素出队时,头部指针向后移动。
- 如果是循环队列,则队列满时,尾部指针回到数组的起始位置。
-
链表实现:
使用链表来实现队列,链表的头部作为队列的前端,尾部作为队列的后端。链表的实现方式灵活,不需要担心队列的容量问题。
4.代码实现
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 5 // 队列的最大容量
typedef struct Queue {
int data[MAX_SIZE]; // 存储队列元素的数组
int front; // 指向队列头部的索引
int rear; // 指向队列尾部的索引
int size; // 当前队列中的元素数量
} Queue;
// 初始化队列
void initQueue(Queue *q) {
q->front = 0;
q->rear = -1;
q->size = 0;
}
// 检查队列是否为空
int isEmpty(Queue *q) {
return q->size == 0;
}
// 检查队列是否已满
int isFull(Queue *q) {
return q->size == MAX_SIZE;
}
// 入队操作
void enqueue(Queue *q, int value) {
if (isFull(q)) {
printf("队列已满,无法入队\n");
return;
}
q->rear = (q->rear + 1) % MAX_SIZE; // 循环队列
q->data[q->rear] = value;
q->size++;
}
// 出队操作
int dequeue(Queue *q) {
if (isEmpty(q)) {
printf("队列为空,无法出队\n");
exit(EXIT_FAILURE);
}
int value = q->data[q->front];
q->front = (q->front + 1) % MAX_SIZE; // 循环队列
q->size--;
return value;
}
// 查看队头元素
int peek(Queue *q) {
if (isEmpty(q)) {
printf("队列为空,无法查看队头元素\n");
exit(EXIT_FAILURE);
}
return q->data[q->front];
}
// 获取队列的大小
int size(Queue *q) {
return q->size;
}
int main() {
Queue q;
initQueue(&q);
enqueue(&q, 10);
enqueue(&q, 20);
enqueue(&q, 30);
enqueue(&q, 40);
enqueue(&q, 50);
printf("队列大小: %d\n", size(&q));
printf("队头元素: %d\n", peek(&q));
while (!isEmpty(&q)) {
printf("出队元素: %d\n", dequeue(&q));
}
return 0;
}
三.总结
-
数据结构特性:
- 队列: FIFO 结构,适用于按顺序处理的任务。
- 栈: LIFO 结构,适用于需要倒序处理的任务。
-
实现方式:
- 两者都可以通过数组或链表来实现。队列需要两个指针(队头和队尾),而栈只需一个指针(栈顶)。
-
应用场景:
- 队列通常用于调度、广度优先搜索等需要按顺序处理的任务。
- 栈则常用于递归、表达式求值等需要倒序或回溯处理的任务。
两者都是非常重要的基础数据结构,在计算机科学和编程中有广泛的应用。
如有错误,欢迎批评指正。