栈(Stack)和队列(Queue)是两种经典的数据结构,它们在计算机科学和软件开发中起着重要的作用。本文将介绍栈和队列的概念、实现以及一些常见的应用场景。
目录
一、栈(Stack)的介绍
1.1 栈的概念
栈是一种线性数据结构,它具有“后进先出”(Last In First Out,LIFO)的特性。栈可以看作是一种特殊的线性表,只能在表的一端进行插入和删除操作,这一端被称为栈顶。
栈的插入操作称为入栈(Push),删除操作称为出栈(Pop)。栈不支持随机访问,只能访问栈顶元素。
1.2 栈的实现
栈的实现有多种方式,如使用数组、链表等。这里我们以链表实现栈为例进行介绍。
1.2.1 栈的数据结构
// 支持动态增长的栈
typedef int STDataType;
typedef struct Stack
{
STDataType* _a;
int _top; // 栈顶
int _capacity; // 容量
}Stack;
1.2.2 栈的初始化
使用StackInit函数初始化栈,需要传入一个Stack类型的指针,将栈的成员初始化为初始值。
void StackInit(Stack* ps)
{
assert(ps);
ps->_a = NULL;
ps->_top = 0;
ps->_capacity = 0;
}
1.2.3 入栈
使用StackPush函数将元素入栈,需要传入一个Stack类型的指针和一个STDataType类型的数据。入栈时,将数据放入栈顶,并更新栈顶指针。
void StackPush(Stack* ps, STDataType data)
{
assert(ps);
// 如果栈的容量不够,则进行扩容操作
if (ps->_top == ps->_capacity)
{
size_t newCapacity = ps->_capacity == 0 ? 4 : ps->_capacity * 2;
STDataType* tmp = realloc(ps->_a, newCapacity * sizeof(STDataType));
if (tmp == NULL)
{
// 扩容失败,打印错误信息并退出程序
printf("realloc failed\n");
exit(1);
}
ps->_a = tmp;
ps->_capacity = newCapacity;
}
ps->_a[ps->_top++] = data;
}
1.2.4 出栈
使用StackPop函数将栈顶元素出栈,需要传入一个Stack类型的指针。出栈时,将栈顶指针减一,即可将栈顶元素出栈。
void StackPop(Stack* ps)
{
assert(ps);
if (ps->_top == 0)
{
// 如果栈为空,打印错误信息并退出程序
printf("stack is empty\n");
exit(1);
}
ps->_top--;
}
1.2.5 获取栈顶元素
使用StackTop函数获取栈顶元素,需要传入一个Stack类型的指针。直接返回栈顶元素即可。
STDataType StackTop(Stack* ps)
{
assert(ps);
if (ps->_top == 0)
{
// 如果栈为空,打印错误信息并退出程序
printf("stack is empty\n");
exit(1);
}
return ps->_a[ps->_top - 1];
}
1.2.6 获取栈中有效元素个数
使用StackSize函数获取栈中有效元素个数,需要传入一个Stack类型的指针。直接返回栈中有效元素个数即可。
int StackSize(Stack* ps)
{
assert(ps);
return ps->_top;
}
1.2.7 检测栈是否为空
使用StackEmpty函数检测栈是否为空,需要传入一个Stack类型的指针。如果栈为空,返回非零结果;如果不为空,返回0。
int StackEmpty(Stack* ps)
{
assert(ps);
return ps->_top == 0;
}
1.2.8 销毁栈
使用StackDestroy函数销毁栈,需要传入一个Stack类型的指针。释放栈的内存空间即可。
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->_a);
ps->_a = NULL;
ps->_top = 0;
ps->_capacity = 0;
}
以上就是栈的概念和实现的代码,下面给出一个栈的示例程序:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* _a; // 存储栈中元素的数组
int _top; // 栈顶指针,指向栈顶元素的下一个位置
int _capacity; // 栈的容量
}Stack;
void StackInit(Stack* ps); // 初始化栈
void StackPush(Stack* ps, STDataType data); // 入栈
void StackPop(Stack* ps); // 出栈
STDataType StackTop(Stack* ps); // 获取栈顶元素
int StackSize(Stack* ps); // 获取栈的大小
int StackEmpty(Stack* ps); // 判断栈是否为空
void StackDestroy(Stack* ps); // 销毁栈
int main()
{
Stack s;
StackInit(&s); // 初始化栈
StackPush(&s, 1); // 入栈
StackPush(&s, 2);
StackPush(&s, 3);
StackPush(&s, 4);
printf("Stack Size: %d\n", StackSize(&s)); // 输出栈的大小
printf("Stack Top: %d\n", StackTop(&s)); // 输出栈顶元素
StackPop(&s); // 出栈
printf("Stack Top: %d\n", StackTop(&s)); // 输出栈顶元素
StackDestroy(&s); // 销毁栈
return 0;
}
void StackInit(Stack* ps)
{
assert(ps);
ps->_a = NULL; // 将栈中元素存储的数组置为空
ps->_top = 0; // 将栈顶指针置为0,表示空栈
ps->_capacity = 0; // 将栈的容量置为0
}
void StackPush(Stack* ps, STDataType data)
{
assert(ps);
if (ps->_top == ps->_capacity) // 如果栈已满,则需要扩容
{
size_t newCapacity = ps->_capacity == 0 ? 4 : ps->_capacity * 2; // 新容量为原容量的两倍,如果原容量为0,则设置为4
STDataType* tmp = realloc(ps->_a, newCapacity * sizeof(STDataType)); // 重新分配内存
if (tmp == NULL)
{
printf("realloc failed\n");
exit(1);
}
ps->_a = tmp; // 更新栈中元素存储的数组的地址
ps->_capacity = newCapacity; // 更新栈的容量
}
ps->_a[ps->_top++] = data; // 将元素入栈,并更新栈顶指针的位置
}
void StackPop(Stack* ps)
{
assert(ps);
if (ps->_top == 0) // 如果栈为空,则无法出栈
{
printf("stack is empty\n");
exit(1);
}
ps->_top--; // 更新栈顶指针的位置,表示出栈操作
}
STDataType StackTop(Stack* ps)
{
assert(ps);
if (ps->_top == 0) // 如果栈为空,则无法获取栈顶元素
{
printf("stack is empty\n");
exit(1);
}
return ps->_a[ps->_top - 1]; // 返回栈顶元素
}
int StackSize(Stack* ps)
{
assert(ps);
return ps->_top; // 返回栈的大小,即栈顶指针的位置
}
int StackEmpty(Stack* ps)
{
assert(ps);
return ps->_top == 0; // 如果栈顶指针的位置为0,则栈为空
}
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->_a); // 释放栈中元素存储的数组的内存
ps->_a = NULL; // 将栈中元素存储的数组置为空
ps->_top = 0; // 将栈顶指针置为0
ps->_capacity = 0; // 将栈的容量置为0
}
运行结果如下:
1.3 栈的应用
栈在计算机科学和软件开发中有许多应用场景,以下是一些常见的应用场景:
-
函数调用栈:函数的调用和返回过程使用栈来实现,每当一个函数被调用时,就会将函数的参数和返回地址等信息存入栈中,当函数返回时,再从栈中取出这些信息。
-
表达式求值:在编译器和解释器中,栈常用于将中缀表达式转换为后缀表达式,并进行求值操作。
-
括号匹配:栈可以用于判断括号是否匹配,每当遇到左括号时,就将其入栈,当遇到右括号时,就从栈中取出一个左括号并进行匹配。
-
浏览器的前进和后退功能:浏览器的前进和后退功能可以使用两个栈来实现,一个栈用于存储已访问的网页,另一个栈用于存储已后退的网页。
二、队列(Queue)的介绍
2.1 队列的概念
队列是一种线性数据结构,它具有“先进先出”(First In First Out,FIFO)的特性。队列可以看作是一种特殊的线性表,只能在表的一端进行插入操作,称为队尾,只能在表的另一端进行删除操作,称为队首。
队列的插入操作称为入队(Enqueue),删除操作称为出队(Dequeue)。队列不支持随机访问,只能访问队首和队尾元素。
2.2 队列的实现
队列的实现同样有多种方式,如使用数组、链表等。这里我们以链表实现队列为例进行介绍。
2.2.1 队列的数据结构
// 链式结构:表示队列
typedef struct QListNode
{
struct QListNode* _next;
QDataType _data;
}QNode;
// 队列的结构
typedef struct Queue
{
QNode* _front;
QNode* _rear;
}Queue;
2.2.2 初始化队列
使用QueueInit函数初始化队列,需要传入一个Queue类型的指针,将队列的成员初始化为初始值。
void QueueInit(Queue* q)
{
assert(q);
q->_front = NULL;
q->_rear = NULL;
}
2.2.3 入队列
使用QueuePush函数将元素入队列,需要传入一个Queue类型的指针和一个QDataType类型的数据。入队列时,创建一个新的QNode节点,将数据放入新节点,并更新队列的_rear指针。
void QueuePush(Queue* q, QDataType data)
{
assert(q);
QNode* newNode = (QNode*)malloc(sizeof(QNode));
if (newNode == NULL)
{
printf("malloc failed\n");
exit(1);
}
newNode->_data = data;
newNode->_next = NULL;
if (q->_rear == NULL)
{
q->_front = q->_rear = newNode;
}
else
{
q->_rear->_next = newNode;
q->_rear = newNode;
}
}
2.2.4 出队列
使用QueuePop函数将队头元素出队列,需要传入一个Queue类型的指针。出队列时,更新队列的_front指针,并释放出队列节点的内存。
void QueuePop(Queue* q)
{
assert(q);
if (q->_front == NULL)
{
printf("queue is empty\n");
exit(1);
}
QNode* toDelete = q->_front;
q->_front = q->_front->_next;
if (q->_front == NULL)
{
q->_rear = NULL;
}
free(toDelete);
}
2.2.5 获取队头元素
使用QueueFront函数获取队头元素,需要传入一个Queue类型的指针。直接返回队头元素即可。
QDataType QueueFront(Queue* q)
{
assert(q);
if (q->_front == NULL)
{
printf("queue is empty\n");
exit(1);
}
return q->_front->_data;
}
2.2.6 获取队尾元素
使用QueueBack函数获取队尾元素,需要传入一个Queue类型的指针。直接返回队尾元素即可。
QDataType QueueBack(Queue* q)
{
assert(q);
if (q->_rear == NULL)
{
printf("queue is empty\n");
exit(1);
}
return q->_rear->_data;
}
2.2.7 获取队列中有效元素个数
使用QueueSize函数获取队列中有效元素个数,需要传入一个Queue类型的指针。遍历队列,并计数有效元素的个数,即可得到队列中有效元素个数。
int QueueSize(Queue* q)
{
assert(q);
int size = 0;
QNode* cur = q->_front;
while (cur != NULL)
{
size++;
cur = cur->_next;
}
return size;
}
2.2.8 检测队列是否为空
使用QueueEmpty函数检测队列是否为空,需要传入一个Queue类型的指针。如果队列为空,返回非零结果;如果非空,返回0。
int QueueEmpty(Queue* q)
{
assert(q);
return q->_front == NULL;
}
2.2.9 销毁队列
使用QueueDestroy函数销毁队列,需要传入一个Queue类型的指针。遍历队列,释放每个节点的内存空间,并最后释放队列的内存空间。
void QueueDestroy(Queue* q)
{
assert(q);
QNode* cur = q->_front;
while (cur != NULL)
{
QNode* toDelete = cur;
cur = cur->_next;
free(toDelete);
}
q->_front = NULL;
q->_rear = NULL;
}
以上就是队列的概念和实现的代码,下面给出一个队列的示例程序:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int QDataType;
typedef struct QListNode
{
struct QListNode* _next;
QDataType _data;
}QNode;
typedef struct Queue
{
QNode* _front;
QNode* _rear;
}Queue;
void QueueInit(Queue* q)
{
assert(q);
q->_front = NULL;
q->_rear = NULL;
}
void QueuePush(Queue* q, QDataType data)
{
assert(q);
QNode* newNode = (QNode*)malloc(sizeof(QNode)); // 创建一个新节点
if (newNode == NULL)
{
printf("malloc failed\n");
exit(1);
}
newNode->_data = data;
newNode->_next = NULL;
if (q->_rear == NULL) // 如果队列为空,将新节点设置为队列的首尾节点
{
q->_front = q->_rear = newNode;
}
else // 如果队列不为空,将新节点插入到队列的尾部,并更新队尾指针
{
q->_rear->_next = newNode;
q->_rear = newNode;
}
}
void QueuePop(Queue* q)
{
assert(q);
if (q->_front == NULL)
{
printf("queue is empty\n");
exit(1);
}
QNode* toDelete = q->_front; // 获取队列的首节点
q->_front = q->_front->_next; // 更新队列的首节点指针
if (q->_front == NULL) // 如果队列只有一个节点,删除后会导致队列为空,需要同时更新队尾指针
{
q->_rear = NULL;
}
free(toDelete); // 释放被删除节点的内存
}
QDataType QueueFront(Queue* q)
{
assert(q);
if (q->_front == NULL)
{
printf("queue is empty\n");
exit(1);
}
return q->_front->_data; // 返回队列的首节点的数据
}
QDataType QueueBack(Queue* q)
{
assert(q);
if (q->_rear == NULL)
{
printf("queue is empty\n");
exit(1);
}
return q->_rear->_data; // 返回队列的尾节点的数据
}
int QueueSize(Queue* q)
{
assert(q);
int size = 0;
QNode* cur = q->_front; // 从队列的首节点开始遍历
while (cur != NULL) // 遍历直到队列的末尾
{
size++;
cur = cur->_next; // 移动到下一个节点
}
return size; // 返回队列的大小
}
int QueueEmpty(Queue* q)
{
assert(q);
return q->_front == NULL; // 如果队列的首节点为空,则队列为空
}
void QueueDestroy(Queue* q)
{
assert(q);
QNode* cur = q->_front; // 从队列的首节点开始遍历
while (cur != NULL) // 遍历直到队列的末尾
{
QNode* toDelete = cur; // 记录当前节点
cur = cur->_next; // 移动到下一个节点
free(toDelete); // 释放当前节点的内存
}
q->_front = NULL; // 将队列的首尾指针置空
q->_rear = NULL;
}
int main() {
Queue queue;
QueueInit(&queue); // 初始化队列
printf("Pushing elements into queue..."); // 将元素依次插入队列
for (int i = 0; i < 5; i++) {
QueuePush(&queue, i);
}
printf("Done.\n");
printf("Queue size: %d\n", QueueSize(&queue)); // 输出队列的大小
printf("Front element: %d\n", QueueFront(&queue)); // 输出队列的首节点数据
printf("Back element: %d\n", QueueBack(&queue)); // 输出队列的尾节点数据
printf("Popping elements from queue..."); // 将元素依次从队列中删除并输出
while (!QueueEmpty(&queue)) {
printf("%d ", QueueFront(&queue));
QueuePop(&queue);
}
printf("Done.\n");
QueueDestroy(&queue); // 销毁队列并释放内存
return 0;
}
运行结果如下:
2.3 队列的应用
队列在计算机科学和软件开发中也有许多应用场景,以下是一些常见的应用场景:
-
广度优先搜索(BFS):在图论和树的算法中,广度优先搜索(BFS)通常使用队列来实现,用于按层次遍历图或树的节点。
-
线程池:线程池常常使用队列来存储待执行的任务,每当有新的任务提交时,就将其插入到队列的末尾,然后线程池中的线程按照一定的策略从队列中取出任务进行执行。
-
消息队列:在分布式系统和消息中间件中,消息队列常常用于存储和传递消息,生产者将消息插入到队列的尾部,消费者从队列的头部获取消息进行处理。
-
打印队列:在操作系统中,打印队列用于存储待打印的文件,打印任务按照先进先出的顺序进行执行。
四、总结
本文介绍了栈和队列的概念、实现以及一些常见的应用场景。栈具有“后进先出”(LIFO)的特性,适用于函数调用、表达式求值、括号匹配等场景。队列具有“先进先出”(FIFO)的特性,适用于广度优先搜索、线程池、消息队列等场景。栈和队列的实现可以使用数组、链表等数据结构,本文以数组为例进行了详细的代码实现和结果演示。希望通过阅读本文,读者能够更好地理解和应用栈和队列。