大家好,我是weipyh,链表结束以后,接下来数据结构到了栈以及队列的内容,如果在顺序表和链表中对各种的插入删除操作熟悉,对于栈以及队列的内容,会学的较为容易。
1.栈
1.1概念与结构
1.2栈的实现
// 定义栈中数据的类型为整型
typedef int STDataType;
// 定义栈的结构体
typedef struct stack
{
// 指向栈元素数组的指针
STDataType* arr;
// 栈顶指针,用于跟踪栈顶元素的位置
// 当栈为空时,top 通常初始化为 -1(如果从0开始计数)或 0(如果从1开始计数)
int top;
// 栈的容量,即栈中最多能存储的元素数量
int capacity;
} ST;
栈为空的时候,top的值为-1,在栈有数据时,top实际指向的是栈顶元素的下一个元素。
void STInit(ST* ps); //栈的初始化
void STInit(ST* ps)
{
// 为栈的数组指针赋值为nullptr,表示当前没有分配任何内存空间
ps->arr = nullptr;
// 初始化栈顶指针为0,表示栈为空,没有元素
ps->top = 0;
// 初始化栈的容量为0,表示栈的最大容量为0,即栈目前没有分配任何空间
ps->capacity = 0;
}
注意,我们可以规定栈为空的时候top为-1或者0,这两种写法有不同的优点和缺点
①top为0时
优点:由于数组首位的下标为0,top赋值为0会显得更直观,当栈满时,top = capacity - 1
缺点:写代码时需要格外的边界检查,top为0时栈为空,栈满时top = capacity
②top为-1时
优点:在判断栈是否为空时,不需要判断top 是否等于0;栈满时top = capacity - 1;
缺点可能需要对top进行偏移处理,在进行数组索引时,实际上的数组索引为top+1
void STDestroy(ST* ps); //栈的销毁操作
void STDestroy(ST* ps)
{
if (ps->arr)
{
free(ps->arr);
ps->arr = nullptr;
}
ps->capacity = ps->top = 0;
}
void STPush(ST* ps, STDataType x); // 压栈
void STPush(ST* ps, STDataType x)
{
// 断言检查,确保传入的栈指针不为空
assert(ps);
// 如果栈的容量为0,说明栈是空的,需要初始化容量
// 否则,扩展栈的容量为原来的两倍
int Newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
// 重新分配内存以增加栈的容量
STDataType* tmp = (STDataType*)realloc(ps->arr, Newcapacity * sizeof(STDataType));
// 如果内存分配失败
if (tmp == nullptr)
{
// 输出错误信息
perror("realloc fail!");
// 退出程序
exit(1);
}
// 更新栈的数组指针为新分配的内存地址
ps->arr = tmp;
// 更新栈的容量
ps->capacity = Newcapacity;
// 将新元素放入栈顶,并增加栈顶指针
ps->arr[ps->top] = x;
ps->top++;
}
bool STEmpty(ST* ps); //判断栈是否为空
bool STEmpty(ST* ps)
{
// 断言检查,确保传入的栈指针不为空
assert(ps);
// 如果栈顶指针top等于0,说明栈中没有任何元素,即栈为空
// 否则栈中至少有一个元素,栈不为空
return ps->top == 0;
}
void STPop(ST* ps); //出栈
void STPop(ST* ps)
{
// 断言检查,确保传入的栈指针不为空且栈不为空
assert(!STEmpty(ps));
// 栈顶指针top自减1,即移除栈顶元素
--ps->top;
}
STDataType STTop(ST* ps); //获取栈顶元素
STDataType STTop(ST* ps)
{
// 断言检查,确保传入的栈指针不为空且栈不为空
assert(!STEmpty(ps));
// 返回栈顶元素,即数组中索引为top-1的元素
return ps->arr[ps->top - 1];
}
如果规定了top为-1的时候为空,那么最后返回的值需要有所改变
return ps->arr[ps->top];
int STSize(ST* ps); //获取栈中元素的有效个数
int STSize(ST* ps)
{
// 断言检查,确保传入的栈指针不为空
assert(ps);
// 返回栈顶指针top的值,即栈中元素的数量
// 因为top指向栈顶元素的下一个位置,所以top的值即为栈中元素的数量
return ps->top;
}
2.队列
2.1概念与结构
队列是一种 先进先出(First-In-First-Out, FIFO) 的线性数据结构,类似于现实生活中的排队场景(如银行窗口、超市收银台)。
- 核心规则:
- 入队(Enqueue):元素只能从队列的 尾部(Rear) 插入。
- 出队(Dequeue):元素只能从队列的 头部(Front) 删除。
同样,队列也可以使用数组或者链表实现,但链表在实现队列时相比数组更优,当使用数组的结构实现队列时,出队列在数组头上出数据,效率会比较低,时间复杂度会更高
2.2队列的实现
// 定义队列中存储的数据类型为整型
typedef int QDataType;
// 定义队列的节点结构体
typedef struct QueueNode {
// 存储节点的数据
QDataType data;
// 指向下一个节点的指针
struct QueueNode* next;
} QNode;
// 定义队列的结构体
typedef struct Queue {
// 指向队列头部的指针
QNode* phead;
// 指向队列尾部的指针
QNode* ptail;
} Queue;
//初始化队列
void QueueInit(Queue* pq);
void QueueInit(Queue* pq)
{
// 断言检查,确保传入的队列指针不为空
assert(pq);
// 将队列的头指针和尾指针都初始化为nullptr
// 这表示队列是空的,没有任何元素
pq->phead = pq->ptail = nullptr;
}
//销毁队列
void QueueDestroy(Queue* pq);
void QueueDestroy(Queue* pq)
{
// 断言检查,确保传入的队列指针不为空
assert(pq);
// 初始化当前节点指针为队列的头指针
QNode* pcur = pq->phead;
// 遍历队列,释放每个节点的内存
while (pcur)
{
// 保存下一个节点的地址,用于后续遍历
QNode* next = pcur->next;
// 释放当前节点的内存
free(pcur);
// 将当前节点指针移动到下一个节点
pcur = next;
}
// 重置头指针和尾指针为nullptr,表示队列已被销毁
pq->phead = pq->ptail = nullptr;
}
// ⼊队列,队尾
void QueuePush(Queue* pq, QDataType x);
void QueuePush(Queue* pq, QDataType x)
{
// 断言检查,确保传入的队列指针不为空
assert(pq);
// 分配一个新的节点
QNode* newnode = (QNode*)malloc(sizeof(QNode));
// 检查内存分配是否成功
if (newnode == nullptr)
{
// 如果内存分配失败,输出错误信息并退出程序
perror("malloc fail!");
exit(1);
}
// 设置新节点的数据和指针
newnode->data = x;
newnode->next = nullptr;
// 如果队列为空,则新节点既是头节点也是尾节点
if (pq->phead == nullptr)
{
pq->phead = pq->ptail = newnode;
}
else
{
// 如果队列不为空,将新节点添加到队列尾部
pq->ptail->next = newnode;
// 更新尾指针为新节点
pq->ptail = newnode;
}
}
//队列判空
bool QueueEmpty(Queue* pq);
bool QueueEmpty(Queue* pq)
{
// 断言检查,确保传入的队列指针不为空
assert(pq);
// 检查队列的头指针是否为nullptr
// 如果头指针为nullptr,则表示队列中没有任何元素,即队列为空
// 否则,队列中至少有一个元素,队列不为空
return pq->phead == nullptr;
}
// 出队列,队头
void QueuePop(Queue* pq);
void QueuePop(Queue* pq)
{
// 断言检查,确保传入的队列指针不为空
assert(pq);
// 断言检查,确保队列不为空
assert(!QueueEmpty(pq));
// 如果队列中只有一个节点(头指针和尾指针相等),则释放该节点并重置头尾指针
if (pq->phead == pq->ptail)
{
free(pq->phead); // 释放头节点(也是尾节点)
pq->phead = pq->ptail = nullptr; // 重置头尾指针为nullptr
}
else
{
// 如果队列中有多个节点,仅释放头节点
QNode* next = pq->phead->next; // 保存下一个节点的地址
free(pq->phead); // 释放头节点
pq->phead = next; // 更新头指针为下一个节点
}
}
//取队头数据
QDataType QueueFront(Queue* pq);
QDataType QueueFront(Queue* pq)
{
// 断言检查,确保传入的队列指针不为空
assert(pq);
// 断言检查,确保队列不为空
assert(!QueueEmpty(pq));
// 返回队列头部节点的数据
// 由于已经确保队列不为空,所以可以直接访问头节点的数据
return pq->phead->data;
}
//取队尾数据
QDataType QueueBack(Queue* pq);
QDataType QueueBack(Queue* pq)
{
// 断言检查,确保传入的队列指针不为空
assert(pq);
// 断言检查,确保队列不为空
assert(!QueueEmpty(pq));
// 返回队列尾部节点的数据
// 由于已经确保队列不为空,所以可以直接访问尾节点的数据
return pq->ptail->data;
}
//队列有效元素个数
int QueueSize(Queue* pq);
int QueueSize(Queue* pq)
{
// 初始化计数器为0
int count = 0;
// 初始化当前节点指针为队列的头指针
QNode* pcur = pq->phead;
// 遍历队列,对每个节点进行计数
while (pcur)
{
// 每遍历一个节点,计数器加1
count++;
// 移动当前节点指针到下一个节点
pcur = pcur->next;
}
// 返回计数结果,即队列中元素的数量
return count;
}
2.3循环队列的概念与结构
循环队列是 顺序队列的优化版本,通过将队列的存储空间视为一个逻辑上的环形结构,解决普通顺序队列的 “假溢出” 问题。
- 核心思想:当队列的尾部指针
rear到达数组末尾时,通过 模运算(%) 将其绕回到数组开头,实现空间的循环利用。 - 特点:
- 物理存储:基于数组。
- 逻辑行为:队列的首尾相连成环。
循环队列给定固定的空间,使用过程中不能扩容,当环形队列满时,不能继续插入数据
循环队列可以使用循环链表实现,也可以使用数组实现,在初始化方面上,用数组更为简单
typedef struct Queue {
int* arr; // 动态数组,用于存储队列中的元素
int front; // 队首指针,指向队列的第一个元素
int rear; // 队尾指针,指向队列的最后一个元素的下一个位置
int capacity; // 队列的容量,即队列中最多可以存储的元素数量
} Queue;
在循环队列中,牺牲一个存储空间是循环队列设计的核心技巧,是为了 明确区分队空和队满的状态。
为什么循环队列要牺牲一个存储空间?
在循环队列的实现中,牺牲一个存储空间(即实际可用容量比数组大小少1)是为了 明确区分队空和队满的状态。这是循环队列设计的核心技巧,以下是详细解释:
1. 问题的本质:指针歧义
循环队列通过 front 和 rear 两个指针管理数据:
-
front:指向队首元素。 -
rear:指向下一个插入位置(队尾的后一位)。
当front == rear时,可能表示两种状态:- 队空:队列中无元素。
- 队满:队列已填满所有空间(
rear绕一圈后追上front)。
2. 牺牲空间如何解决问题?
通过 强制保留一个空位,使得:
- 队空条件:
front == rear(指针重合)。 - 队满条件:
(rear + 1) % size == front(rear的下一个位置是front)。
所以结构体中的capacity并不是队列的真正空间大小,而是capacity + 1
2.4循环队列的实现
Queue* QueueInit(int k) //循环队列的初始化
Queue* QueueInit(int k) {
Queue* pq = (Queue*)malloc(sizeof(Queue)); // 分配内存给队列结构体
pq->arr = (int*)malloc((k + 1) * sizeof(int)); // 分配内存给队列的数组,多一个空间用于简化满队列和空队列的判断
pq->front = pq->rear = 0; // 初始化队首和队尾指针
pq->capacity = k; // 设置队列的容量
return pq; // 返回初始化后的队列指针
}
bool QueueIsEmpty(Queue* obj) //判断循环队列是否为空
// 判断队列是否为空的函数
bool QueueIsEmpty(Queue* obj) {
return obj->front == obj->rear; // 如果队首和队尾指针相等,队列为空
}
bool QueueIsFull(Queue* obj) //判断循环队列是否为满
// 判断队列是否为满的函数
bool QueueIsFull(Queue* obj) {
return (obj->rear + 1) % (obj->capacity + 1) == obj->front; // 如果队尾指针加一后的位置与队首指针相等,队列满
}
bool enQueue(Queue* obj, int value) //向循环队列插入一个元素
// 向队列中添加一个元素
bool enQueue(Queue* obj, int value) {
// 检查队列是否已满
if (QueueIsFull(obj)) {
// 如果队列已满,则返回false,表示添加失败
return false;
}
// 将传入的值value存储到队尾指针rear当前指向的位置
obj->arr[obj->rear] = value;
// 队尾指针rear自增,指向下一个可插入位置
obj->rear++;
// 为了实现循环队列,让rear指针在达到数组末尾后回到数组开头
obj->rear %= obj->capacity + 1;
// 返回true,表示元素成功添加到队列中
return true;
}
bool DeQueue(Queue* obj) //从循环队列删除一个元素
// 从队列中移除一个元素
bool DeQueue(Queue* obj) {
// 检查队列是否为空
if (QueueIsEmpty(obj)) {
// 如果队列为空,则返回false,表示移除失败
return false;
}
// 队首指针front自增,指向下一个元素
++obj->front;
// 为了实现循环队列,让front指针在达到数组末尾后回到数组开头
obj->front %= obj->capacity + 1;
// 返回true,表示元素成功从队列中移除
return true;
}
int QueueFront(Queue* obj) //获取队首元素
// 获取队首元素的函数
int QueueFront(Queue* obj) {
if (QueueIsEmpty(obj)) { // 检查队列是否为空
return -1; // 如果队列空,返回-1
}
return obj->arr[obj->front]; // 返回队首元素
}
int QueueRear(Queue* obj) //获取队尾元素
int QueueRear(Queue* obj) {
if (QueueIsEmpty(obj)) { // 检查队列是否为空
return -1; // 如果队列空,返回-1
}
int prev = obj->rear - 1; // 计算队尾元素的索引
if (obj->rear == 0) { // 如果队尾指针为0,说明它已经绕回到数组开头
prev = obj->capacity; // 调整索引到数组的最后一个位置
}
return obj->arr[prev]; // 返回队尾元素
}
void QueueFree(Queue* obj) //释放队列内存
// 释放队列占用的内存
void QueueFree(Queue* obj) {
if (obj->arr) { // 检查数组指针是否非空
free(obj->arr); // 释放数组内存
free(obj); // 释放队列结构体内存
obj = NULL; // 将指针设置为NULL,避免悬空指针
}
}
3.栈和队列的区别
栈(Stack)与队列(Queue)的区别对比表
| 特性 | 栈(Stack) | 队列(Queue) |
|---|---|---|
| 操作原则 | 后进先出(LIFO, Last In First Out) | 先进先出(FIFO, First In First Out) |
| 插入操作 | 压栈(Push)——只能在栈顶插入 | 入队(Enqueue)——只能在队尾插入 |
| 删除操作 | 弹栈(Pop)——只能在栈顶删除 | 出队(Dequeue)——只能在队首删除 |
| 访问权限 | 仅能访问栈顶元素(Top) | 可访问队首(Front)和队尾(Rear)元素 |
| 典型应用 | 函数调用栈、表达式求值、括号匹配、撤销操作(Undo) | 任务调度、消息队列、缓冲区管理、BFS算法 |
| 实现方式 | 顺序栈(数组)或链栈(链表) | 顺序队列(数组)或链队列(链表),含循环队列优化 |
| 核心操作时间复杂度 | Push/Pop: O(1) | Enqueue/Dequeue: O(1)(循环队列优化后) |
| 空间复杂度 | O(n)(固定或动态扩容) | O(n)(可能需牺牲一个空间区分队空/队满) |
| 指针/标记 | 仅需一个栈顶指针(top) | 需要队首(front)和队尾(rear)两个指针 |
| 边界条件 | 栈空:top == -1(数组)或 top == NULL(链表) | 队空:front == rear;队满:(rear+1)%size == front |
| 线程安全 | 单线程操作简单,多线程需同步 | 生产者-消费者模型需同步机制(如锁或信号量) |
关键区别总结
- 操作顺序:栈是“后来居上”,队列是“先到先得”。
- 灵活性:队列支持两端操作(队首出、队尾入),栈仅支持一端操作。
- 应用场景:
- 栈适合需要“回退”或“递归”的场景(如函数调用)。
- 队列适合需要“公平调度”的场景(如打印任务排队)。
这就是本期的内容了,谢谢大家的观看

1560

被折叠的 条评论
为什么被折叠?



