数据结构篇——队列的操作实现(队列、循环队列)!

一:队列的定义

队列(Queue)是一种先进先出(First In First Out)的线性数据结构,也称为FIFO结构。队列其实也是一种操作受限的线性表,其只能在尾部插入,在头部弹出。在队列中,元素的添加(也称为入队)操作被添加到队尾,而元素的移除(也称为出队)操作则从队首进行,即尾入头出。

二:队列(数组实现)

如下图,在没有元素入队的时候,头指针域尾指针均指向索引为0的位置

如下图,在元素入队以后,尾指针会向后移动,记录队尾元素入队情况 

队列先进先出的特性在数组中可以进行很便利的模拟实现。

①由于普通的非循环队列在队列中没有元素时,此时头指针和尾指针是指向同一处位置的,所以我们可以利用头指针是否与尾指针相同来判断队列是否为空。

② 但如果队列为满的情况下,头指针与尾指针也是指向相同的位置,导致队列的判空和判满操作的条件一样,会造成混淆,所以在队列结构中,我增设一个count变量来实时记录队列内元素数量,在后续可以通过count对象大小与队列可容纳个数进行比较来判断队列是否为满。如何判断队列为满有很多种方法,增设count记录状态是比较容易理解的一种。

③普通队列会出现假溢出的情况,即当队列的前端已经有元素出队,但后端继续入队导致空间不足时,即使队列中还有很多空闲空间也无法继续入队,这就是假溢出。这是由数组实现的普通队列的缺点,解决方法在下文循环队列。

完整代码:


#include <iostream>
#define MAXSIZE 5    //队列可容纳的最大元素个数
using namespace std;

typedef struct Queue
{
    int *base;     //动态数组,用于给队列分配空间
    int front;     //利用索引来模拟头指针
    int rear;      //利用索引来模拟尾指针
    int count;     //用来判断队列内当前元素个数
} Queue;

// 判断队列是否为空
bool emptyQueue(Queue q)
{
    if (q.front == q.rear)      //若头指针与尾指针相同,则队列为空
    {
        cout << "队列为空!!!" << endl;
        return true;
    }
    else
    {
        cout << "队列不为空!!!" << endl;
        return false;
    }
}

// 初始化队列
void initialQueue(Queue &q)
{
    q.base = new int[MAXSIZE];  // 为队列分配空间
    q.front = q.rear = 0;       //初始化头指针、尾指针的索引为0
    q.count = 0; 
}

// 入队
void push(Queue &q)
{
    cout << "请输入待入队的数据:";
    int data = 0;
    cin >> data;
    q.base[q.rear++] = data;   //q.rear为索引,如果是第一个元素,将data放到数组索引为0的位置,并让rear向后移一位
    q.count++; //队列内元素数量+1
}

// 出队并返回队头元素
int pop(Queue &q)
{
    int data = 0;
    data = q.base[q.front++]; //q.front为索引,依次从索引0处开始出队,并让front递增
    q.count--;      //出队后元素数量-1;
    return data;
}

// 销毁队列
void destroyQueue(Queue &q)
{
    delete[] q.base;     //由于初始化时分配的为数组大小的空间,这里要删除数组大小的空间
    q.front = q.rear = 0;
    q.count = 0;
}

// 判断对列是否已满
bool fullQueue(Queue q)
{
    if (q.count == MAXSIZE)  //判断队列内当前元素数量是否与队列最大可容纳数量相同
    {
        cout << "队列已满,无法添加元素!!!" << endl;
        return true;
    }
    else
    {
        cout << "队列未满,还可添加 " << MAXSIZE - q.count << "个元素!!!" << endl;
        return false;
    }
}

int main()
{
    Queue q;
    initialQueue(q);
    emptyQueue(q);
    cout << "请输入待入队的元素个数:";
    int numbers = 0;
    cin >> numbers;
    for (int i = 0; i != numbers; ++i)
    {
        push(q);
    }
    emptyQueue(q);
    cout << "请选择出队元素个数:";
    int n = 0;
    cin >> n;
    for (int i = 0; i != n; ++i)
    {
        cout << pop(q) << " ";
    }
    cout << endl;
    fullQueue(q);
    destroyQueue(q);
    return 0;
}

 

三:队列(链表实现)

用链表来实现队列相对会复杂一点,因为我们要构造一个结点去存放数据和下一个结点的指针,和单链表一样。同时,我们还需要一个结构来对队列进行维护与管理,所以我们还需要增设一个结构来储存头指针与尾指针。

如下图,该图为对链队进行初始化时,此时该链队只有一个头结点。

                                             

如下图,在对链队进行入队操作后,尾指针会跟随移动

 

完整代码: 


#include<iostream>
using namespace std;

//与单链表的定义一样
typedef struct QueueNode
{
    int data;    
    QueueNode *next;
} QueueNode, *QueuePtr;

typedef struct ListQueue
{
    QueuePtr front;     //指向QueueNode类型的头指针
    QueuePtr rear;      //指向QueueNode类型的尾指针
} ListQueue;

//初始化队列
void initialQueue(ListQueue &q)
{
    q.front = q.rear = new QueueNode;   //此时为头指针与尾指针分配了一块空间,相当于链表中的头结点,用来简化入队与出队的操作
    q.rear->next = nullptr;  //将尾指针的next域置空
}

//判断队列是否为空
bool emptyQueue(ListQueue q)
{
    if(q.front == q.rear)       //如果头尾指针指向同一块位置,则队列为空
    {
        cout << "队列为空!!!" << endl;
        return true;
    }
    else
    {
        cout << "队列不为空!!!" << endl;
        return false;
    }
}

//入队
void push(ListQueue &q)
{
    cout << "请输入待入队数据:";
    int data = 0;
    cin >> data;
    QueuePtr p = new QueueNode;     //new一块空间用来添加元素
    p->data = data;
    p->next = nullptr;
    q.rear->next = p;      //入队时只需要对尾指针进行操作
    q.rear = p;            //更新尾指针
}

//获取队头元素
int getQueueData(ListQueue q)
{
    return q.front->data;     //获取队头元素时需要跳过头结点,头结点内不存储数据
}

//出队并获取队头元素
int pop(ListQueue &q)
{
    int data = 0;
    QueuePtr temp = q.front->next;    //保存待出队节点的下一节点地址
    data = temp->data;                //获取带出队节点的数据
    q.front->next = temp->next;       //架空待出队结点,因为在出队后,结点要被删除
    if(temp == q.rear)       //重要操作,此操作可用来判断队列在执行相同次数的入队与出队后队列是否为空
        q.front = q.rear;
    delete temp;
    return data;
}

//销毁链队
void destroyQueue(ListQueue &q)
{
    while(q.front)        //当队列为空时,表示队列已经从队头销毁到了队尾
    { 
        QueuePtr temp = q.front->next;   //记录下一节点位置
        delete q.front; //删除当前节点
        q.front = temp; //更新节点
    }
}

int main()
{
    ListQueue q;
    initialQueue(q);
    emptyQueue(q);
    cout << "请输入待入队的元素个数:";
    int numbers = 0;
    cin >> numbers;
    for (int i = 0; i != numbers; ++i)
    {
        push(q);
    }
    emptyQueue(q);
    cout << "请输入待出队的元素个数:";
    int n = 0;
    cin >> n;
    for (int i = 0; i != n; ++i)
    {
        cout << pop(q) << " ";
    }
    cout << endl;
    emptyQueue(q);
    //destroyQueue(q);  //此操作在队列所有元素出队完以后不可在调用
    return 0;
}

四:循环队列(数组实现)

 ①为了更好的解决队列中存在的假溢出问题,循环队列应运而生。在循环队列中,即让队列中少存储一个元素,如图

队列中最大可存储元素个数为5,但实际上我们只存储4个元素,空下一块空间不存储任何数据,通过该方法我们可以解决队列的假溢出问题。

②少存储一个元素的方式使得我们在存储数据时好像不容易找的一个方法去对应。但聪明的人已经帮我们解决了这个问题,下面我来介绍介绍介绍。

队列的最大可存储元素个数为5,我们将他表示为MAXSIZE,虽然最大可存储元素个数为5,但是在队列中我们只存储4个元素。在将data值赋值给以rear为索引的位置后,将rear = (rear + 1) % MAXSIZE;如果rear是0,即第一个入队的元素,存储数据后rear = (0 + 1) % MAXSIZE; 此时rear为1。存储一个数据后rear更新为1,存储两个元素后rear更新为2,当我们存储了四个数据后,此时索引rear为4,存储后我们需要执行rear = (rear + 1) % MAXSIZE;此时rear = ( 4 + 1) % MAXSIZE;此时rear又回到了索引为0的位置重新开始后续的入队操作。详情请浏览完整代码。

完整代码: 

#include<iostream>
#define MAXSIZE 5
using namespace std;

typedef struct Queue
{
    int *base;
    int front;
    int rear;
} Queue;

//判断队列是否为空
bool emptyQueue(Queue &q)
{
    if (q.front == q.rear)    //若尾指针与头指针指向位置相同,链队则为空
    {
        cout << "队列为空!!!" << endl;
        return true;
    }
    else
    {
        cout << "队列不为空!!!" << endl;
        return false;
    }
}

// 判断队列是否满队
bool fullQueue(Queue &q)
{
    if ((q.rear + 1) % MAXSIZE == q.front)    //如果尾指针的下一位置是头指针,则链队为满
    {
        cout << "队列已满!!!" << endl;
        return true;
    }
    else
    {
        cout << "队列未满 !!!" << endl;
        return false;
    }
}

// 初始化队列
void initialQueue(Queue &q)
{
    q.base = new int[MAXSIZE];
    q.front = q.rear = 0;
}

// 入队
void push(Queue &q)
{
    cout << "请输入带入队元素个数: ";
    int data = 0;
    cin >> data;
    q.base[q.rear] = data;
    q.rear = (q.rear + 1) % MAXSIZE;    //在入队后,尾指针再向后移位的时候会自动跳过最后一个队列空间,因为要实现循环队列,队列最后一个空间不存储数据
}

// 出队并获取队头顶元素
int pop(Queue &q)
{
    int data = 0;
    data = q.base[q.front];
    q.front = (q.front + 1) % MAXSIZE;   //出队时,跳过不存储数据的空间
    return data;
}

// 销毁队列
void destroyQueue(Queue &q)
{
    delete[] q.base;      //销毁已开辟的数组空间
    q.front = q.rear = 0;
}

// 获取队列长度
int queueLength(Queue &q)
{
    return (q.rear - q.front + MAXSIZE) % MAXSIZE;  //这一操作避免了队尾指针在队头指针前而出现的负数的情况
}


int main()
{
    Queue q;
    initialQueue(q);
    emptyQueue(q);
    cout << "请输入待入队的元素个数:";
    int numbers = 0;
    cin >> numbers;
    for (int i = 0; i != numbers; ++i)
    {
        push(q);
    }
    emptyQueue(q);
    fullQueue(q);
    cout << "请选择出队元素个数:";
    int n = 0;
    cin >> n;
    for (int i = 0; i != n; ++i)
    {
        cout << pop(q) << " ";
    }
    cout << endl;
    fullQueue(q);
    destroyQueue(q);

    return 0;
}

在这里由于队列可承载的最大元素数量为5,在入队7个元素后会覆盖掉第一次和第二次入队的元素,需及时出队,以防覆盖。 

 

五:不同实现方法中的优缺点及使用场景

1. 数组实现的普通队列

优点

  • 空间利用率高:由于数组在内存中连续存储,所以空间利用率相对较高,特别是在队列长度接近数组容量时。
  • 随机访问速度快:可以直接通过索引访问队列中的任意元素,访问效率高。

缺点

  • 动态扩容成本高:如果队列在使用过程中达到数组容量,需要重新分配更大的内存空间,并复制原有数据到新数组,这个操作的时间复杂度为O(n)。
  • 可能出现假溢出:当队列的前端已经有元素出队,但后端继续入队导致空间不足时,即使队列中还有很多空闲空间也无法继续入队,这就是假溢出。

2. 链表实现的普通队列

优点

  • 动态扩容方便:链表不需要在内存中连续存储,所以当队列长度增加时,只需要在链表的尾部添加新的节点即可,不需要重新分配内存和复制数据,时间复杂度为O(1)。
  • 不会出现假溢出:由于链表是动态分配的,理论上可以无限扩展,因此不会出现假溢出的情况。

缺点

  • 空间利用率低:每个节点除了存储数据外,还需要存储指向下一个节点的指针,这增加了额外的空间开销。
  • 随机访问效率低:链表不支持通过索引直接访问元素,需要从头节点开始遍历,访问效率较低。

3. 数组实现的循环队列

优点

  • 解决假溢出问题:通过头尾指针的循环移动,循环队列可以有效地利用数组空间,避免了普通数组队列的假溢出问题。
  • 空间利用率高:与普通数组队列相比,循环队列在达到数组容量之前,可以更有效地利用空间。

缺点

  • 数组容量固定:虽然解决了假溢出,但循环队列的容量仍然受限于数组的初始大小,如果需要存储更多元素,仍需要重新分配更大的数组。
  • 需要维护头尾指针:与普通数组队列相比,循环队列需要额外维护头尾指针的位置,增加了实现的复杂度。

使用场景

  • 如果你需要高效的随机访问能力且队列长度相对固定,可以选择数组实现的普通队列。
  • 如果你需要动态扩容且不关心随机访问效率,链表实现的普通队列可能是更好的选择。
  • 如果你需要解决假溢出问题且队列长度变化不大,数组实现的循环队列是一个很好的折中选择。

六:总结

①有的队列图我横着放,有的我竖着放,其实都是一样的道理,怎样放都是可以的,取决于个人喜好,不影响队列本身的含义。

②取模操作在编程操作中是一个很有用的方法,大家最好记住这种方法。

③还是那句话,画图能解决你脑子想不出来的情况,不熟练时一定要画图。

④大家加油!

  • 12
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值