栈和队列,循环队列的实现

大家好,我是weipyh,链表结束以后,接下来数据结构到了栈以及队列的内容,如果在顺序表和链表中对各种的插入删除操作熟悉,对于栈以及队列的内容,会学的较为容易。

1.栈

1.1概念与结构

栈:一种特殊的线性表,其只允许在固定的⼀端进行插入和删除元素操作。进行数据插⼊和删除操作的一端称为栈顶,另⼀端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
栈的栈顶我们一般定义变量为top
压栈:栈的插入操作叫进栈,压栈,或者入栈,在栈顶处插入数据
出栈:栈的删除操作叫出栈,同样在栈顶进行删除
对于栈的结构:逻辑结构一定是线性的,而物理结构则根据实现栈的底层结构决定,来判断是线性的还是非线性的。栈可以使用数组或者链表来实现,不同的实现方式代码的时间复杂度会有所差异,相对而言,用数组实现会好一些,因为在尾插时相比链表来说所需的时间复杂度更小

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 时,可能表示两种状态:
    1. ​队空​​:队列中无元素。
    2. ​队满​​:队列已填满所有空间(rear 绕一圈后追上 front)。

2. 牺牲空间如何解决问题?​

通过 ​​强制保留一个空位​​,使得:

  • ​队空条件​​:front == rear(指针重合)。
  • ​队满条件​​:(rear + 1) % size == frontrear 的下一个位置是 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
​线程安全​单线程操作简单,多线程需同步生产者-消费者模型需同步机制(如锁或信号量)

关键区别总结​

  1. ​操作顺序​​:栈是“后来居上”,队列是“先到先得”。
  2. ​灵活性​​:队列支持两端操作(队首出、队尾入),栈仅支持一端操作。
  3. ​应用场景​​:
    • 栈适合需要“回退”或“递归”的场景(如函数调用)。
    • 队列适合需要“公平调度”的场景(如打印任务排队)。

这就是本期的内容了,谢谢大家的观看

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值