数据结构——循环队列(C++实现)

数据结构------循环队列

我们今天来接着了解队列的变形------循环队列

什么是循环队列

循环队列主要是解决,顺序结构的队列在进行pop操作时,时间复杂度为O(n)的操作:
这是因为,我们默认0号位置就是队头,所以0号位置元素一旦被弹出,后面的元素必须得补上,不然就会出现没有队头的情况。

其实我们不必规定0号位置就是队头,0号位置被弹出之后,自然1号位置就是队头,我们可以用一个变量front记录我们队头的位置:
但是这样有会有一个问题,如果队列一直弹出,就会出现front越界:
这个时候,明明有很多的空位,但是无法完成push,因为下标越界,这就是我们所说的假溢出

所以,我们可以某种方式,使得front越界之后,重新回到开头:

这就是循环队列循环 的来历,但是这样也只能完成队列的push,无法完成pop,所以我们不能只要一个指针,我们需要两个指针,一个用来指向队头,一个用来指向队尾:

这就是循环队列的基本结构

循环队列是一种特殊类型的队列数据结构,它利用固定大小的数组(通常是连续的内存区域)模拟无限循环的空间,从而有效地解决了普通顺序队列在使用过程中可能出现的"假溢出"问题。循环队列通过将数组的两端连接起来,形成一个逻辑上的环形结构,使得队列的头部和尾部可以循环地在数组范围内移动,而不是受限于数组的固定边界。

以下是循环队列的主要特点和运作机制:

  1. 结构

    • 循环队列使用一个固定大小的数组(通常称为缓冲区)来存储元素。
    • 有两个指针,分别表示队列的头部(front)和尾部(rear)。它们用来追踪队列中元素的插入和移除位置。
  2. 插入(入队)

    • 当新元素被添加到队列尾部时,尾指针(rear)递增。
    • 当尾指针到达数组的末尾时,不是停止插入(导致假溢出),而是将其绕回到数组的起始位置,继续在数组中下一个可用位置存放元素。这种绕回操作是通过取模运算(rear = rear % capacity)实现的,保证了尾指针始终在数组范围内循环移动。
  3. 移除(出队)

    • 从队列头部移除元素时,头指针(front)递增。
    • 同样,当头指针到达数组末尾时,也通过取模运算使其绕回到数组起始位置,这样就可以持续访问到队列中的下一个待出队元素。
  4. 空间利用率

    • 由于循环队列能够在数组范围内循环利用空间,即使队列未填满整个数组,也可以连续地进行入队和出队操作,避免了普通顺序队列因头尾指针接近而导致无法继续插入新元素(尽管数组中仍有空闲位置)的情况,即"假溢出"。
  5. 判断队列状态

    • 判断循环队列是否为空:front == rear
    • 判断循环队列是否已满:通常有两种策略:
    • 如果要求队列元素间至少有一个空位作为分隔(区分满和空),则判断条件为 ((rear + 1) % capacity) == front
    • 如果允许队列满时尾部直接追上头部(即队列满时前后相邻),则判断条件为 (rear == front) && (_size == _capacity)
  6. 实现方式

    • 循环队列既可以基于数组实现,也可以借助单链表等其他线性数据结构实现。但最常见的是使用数组,因其能提供常数时间复杂度的随机访问能力,且空间连续,有利于缓存优化。

队满和队空的问题

现在有一个问题,我们一开始设置的队空的时候,队头指针和队尾指针都是指向的一个位置:

如果我们插入元素插满之后,rear会越界:

这个时候,rear越界,会重新回到0号位置:

这个时候队满和队空都是指向一个位置,无法区别,这时候我们有两种办法,一种是设定标志位flag,另一种是留一个空位,我们着重来介绍一下第二种方法:

留一个空位

为了区分队满和队空的区别,我们留一个空位,这个时候队满的条件就会变成:rear + 1 == front
rear指向8,8 + 1 = 9,越界,此时取模 9 % 9 = 0,和front重合,说明队列已满。

因为这是循环队列,所以也会出现下面的情况:

这是因为rear已经转过了一圈,整合上面的两种情况,我们可以得到队满的判断条件:

(rear + 1)% _capacity == front

上面公式可以包括这两种情况,若rear > front,取模完成循环,否则取模是无效操作。

元素个数

当rear > front时,直接rear - front就是元素个数:
当rear < front时:
此时元素个数分为两部分:一部分是_capacity - front ,另一部分是rear - 0

rear - front + _capacity

归并上面两个式子,可以得到元素个数:

(rear - front + _capacity) % _capacity

了解了难点之后,我们就可以编写代码:

cpp 复制代码
#pragma once
#include<iostream>
#include<cassert>

// 循环队列模板类
template<class T>
class CircQueue
{
public:
    // 默认构造函数,初始化队列容量为10
    CircQueue()
    {
        _data = new T[10];
        _capacity = 10;
    }

    // 带参数构造函数,初始化队列容量为size+1
    CircQueue(const size_t& size)
    {
        _data = new T[size + 1];
        _capacity = size + 1;
    }

    // 判断队列是否为空
    bool empty()
    {
        return front == rear;
    }

    // 判断队列是否已满
    bool fullQueue()
    {
        return ((rear + 1) % _capacity) == front;
    }

    // 入队操作
    void push(const T& data)
    {
        assert(!fullQueue()); // 断言队列不为满

        _data[rear++] = data;

        // rear取模循环
        rear = rear % _capacity;
    }

    // 出队操作
    T pop()
    {
        assert(!empty()); // 断言队列不为空
        T data = _data[front];
        front++;

        // 循环
        front = front % _capacity;

        return data;
    }

    // 返回元素数量
    size_t size()
    {
        return (rear - front + _capacity) % _capacity;
    }

    // 打印队列中的所有元素
    void PrintCicQueue()
    {
        int index = front;
        // 当rear在front之后(正常情况)
        if (rear > front)
        {
            while (index < _capacity - 1)
            {
                std::cout << _data[index] << " ";
                index++;
            }
        }
        // 当front在rear之后或等于rear(队列元素跨越队尾回绕至队首)
        else if (front >= rear)
        {
            if (index < _capacity - 1)
            {
                while (index != _capacity - 1)
                {
                    std::cout << _data[index] << " ";
                    index++;
                }

                if (index == _capacity - 1)
                {
                    index = index % (_capacity - 1);
                    while (index != rear)
                    {
                        std::cout << _data[index] << " ";
                        index++;
                    }
                }
            }
        }
    }
private:
    T* _data; // 动态数组
    int front = 0; // 头指针
    int rear = 0; // 尾指针
    size_t _capacity; // 队列容量
};

在这个循环队列模板类中,我们实现了以下功能:

  1. 默认构造函数和带参数构造函数,用于初始化队列。
  2. empty() 函数,判断队列是否为空。
  3. fullQueue() 函数,判断队列是否已满。
  4. push() 函数,用于向队列中添加元素。
  5. pop() 函数,用于从队列中移除元素。
  6. size() 函数,返回队列中的元素数量。
  7. PrintCicQueue() 函数,打印队列中的所有元素。

我们可以测试一下:

cpp 复制代码
#include"CircQueue.h"

int main()
{
    CircQueue<int> cdeque(10);

    cdeque.push(23);
    cdeque.push(2);
    cdeque.push(1);
    cdeque.push(231);
    cdeque.push(3);
    cdeque.push(5);
    cdeque.push(0);
    cdeque.push(7);
    cdeque.push(17);
    cdeque.push(0);

    cdeque.pop();
    cdeque.pop();
    cdeque.pop();

    cdeque.push(188);
    cdeque.push(23);
    cdeque.push(6);
    //cdeque.push(6);

    cdeque.PrintCicQueue();
    std::cout << "元素个数:"<< cdeque.size() << std::endl;

    return 0;
}

记录元素个数

上面的代码一般来说是考试喜欢考的,如果是自己实现,完全可以自己记录元素个数:

cpp 复制代码
#pragma once
#include<iostream>
#include<cassert>

// 循环队列模板类
template<class T>
class CircQueue
{
public:
    // 默认构造函数,创建一个初始容量为10的循环队列
    CircQueue()
            : _size(0)
    {
        _data = new T[10];
        _capacity = 10;
    }

    // 构造函数,创建一个指定容量的循环队列
    CircQueue(const size_t& size)
            : _size(0)
    {
        _data = new T[size];
        _capacity = size;
    }

    // 入队操作:将数据添加到队列尾部
    void push(const T& data)
    {
        assert(_size < _capacity); // 确保队列未满

        _data[rear++] = data; // 将数据存入队尾
        _size++; // 队列元素个数加一

        // 假溢出处理:将rear指针回绕到队列开始位置
        rear = rear % _capacity;
    }

    // 出队操作:从队列头部移除并返回数据
    T pop()
    {
        assert(_size != 0); // 确保队列非空

        T data = _data[front]; // 获取队首数据
        front++; // 队首指针后移

        // 假溢出处理:将front指针回绕到队列开始位置
        front = front % _capacity;
        _size--; // 队列元素个数减一

        return data; // 返回移除的数据
    }

    // 返回队列中元素的个数
    size_t size() const
    {
        return _size;
    }

    // 判断队列是否为空
    bool empty() const
    {
        return _size == 0;
    }

    // 打印队列中的所有元素
    void PrintCicQueue()
    {
        int index = front;

        // 当rear在front之后(正常情况)
        if (rear > front)
        {
            while (index < _capacity)
            {
                std::cout << _data[index] << " ";
                index++;
            }
        }
            // 当front在rear之后或等于rear(队列元素跨越队尾回绕至队首)
        else if (front >= rear)
        {
            if (index < _capacity)
            {
                while (index != _capacity)
                {
                    std::cout << _data[index] << " ";
                    index++;
                }

                if (index == _capacity)
                {
                    index = index % _capacity;
                    while (index != rear)
                    {
                        std::cout << _data[index] << " ";
                        index++;
                    }
                }
            }
        }
    }

private:
    // 存储队列元素的动态数组
    T* _data;
    // 当前队列中元素的个数
    size_t _size;
    // 队列的最大容量
    size_t _capacity;
    // 指向队列头部的指针
    int front = 0;
    // 指向队列尾部的指针
    int rear = 0;
};

也是可以达到同样的效果:

相关推荐
I_Am_Me_几秒前
【JavaEE进阶】 JavaScript
开发语言·javascript·ecmascript
暮色_年华3 分钟前
Modern Effective C++item 9:优先考虑别名声明而非typedef
c++
重生之我是数学王子11 分钟前
QT基础 编码问题 定时器 事件 绘图事件 keyPressEvent QT5.12.3环境 C++实现
开发语言·c++·qt
Ai 编码助手13 分钟前
使用php和Xunsearch提升音乐网站的歌曲搜索效果
开发语言·php
学习前端的小z17 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
daiyang123...24 分钟前
测试岗位应该学什么
数据结构
神仙别闹24 分钟前
基于C#和Sql Server 2008实现的(WinForm)订单生成系统
开发语言·c#
XINGTECODE25 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
我们的五年35 分钟前
【Linux课程学习】:进程程序替换,execl,execv,execlp,execvp,execve,execle,execvpe函数
linux·c++·学习
kitesxian37 分钟前
Leetcode448. 找到所有数组中消失的数字(HOT100)+Leetcode139. 单词拆分(HOT100)
数据结构·算法·leetcode