队列

队列(queue)是只允许在一段进行插入操作,而在另一端进行删除操作的线性表。

队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。 假设队列是q = (a1,a2,……,an),那么a1就是队头元素,an是队尾元素。这样我们删除时,总是从a1开始,而插入时,列在最后。这样也比较符合我们通常生活中的习惯,排在第一个的优先出列,最后的当然排在队伍最后。

一、队列的抽象数据类型

同样是线性表,队列也有类似线性表的各种操作,不同的就是插入数据只能在队尾进行,删除数据只能在队头进行。

ADT 队列(Queue)

Data
    同线性表,元素具有相同的类型,相似元素具有前驱和后继关系
Operation
    InitQueue(*Q):初始化栈操作,建立一个空队列Q
    DestroyQueue(*Q):若队列Q存在,则销毁它
    ClearQueue(*Q):使队列Q清空
    QueueEmpty(Q):若队列Q为空,则返回true,否则返回false
    GetHead(Q,*e):若队列存在且非空,用e返回队列Q的队头元素
    EnQueue(*Q,e):若队列Q存在,插入新元素e到队列Q中并成为队尾元素
    DeQueue(*Q,*e):删除队列Q中队头元素,并用e返回其值
    QueueLength(Q):返回队列Q的元素个数

二、循环队列

线性表有顺序存储和链式存储,栈是线性表,所以有这两种存储方式。同样,队列作为一种特殊的线性表,也同样存在这两种存储方式。我们先来看队列的顺序存储结构。

1.队列顺序存储的不足

我们假设一个队列由n个元素,则顺序存储的队列需建立一个大于n的数组,并把所有元素存储在数组的前n个单元,数组下表为0的一端即是队头。所谓的入队列操作,其实就是在队尾追加一个元素,不需要移动任何元素,因此时间复杂度为O(1)。

与栈不同的是,队列元素的出列是在队头,即下标为0的位置,那就意味着,队列中的所有元素都得向前移动,以保证队列的队头,也就是下标为0的位置不为空,此时事件复杂度为O(n)。

这里的实现和线性表的顺序存储结构完全相同。

但是这样的话出队列的性能很低,如果不去限制队列的元素必须存储在数组的前n个单元这一条件,出队列的性能就会大大增加。也就是说,队头不需要一定在下标为0的位置上。

为了避免当只有一个元素时,队头和队尾重合变得麻烦,所以引入两个指针,front指向队头元素,rear指向队尾元素的下一个位置,这样当front等于rear时,此时队列不是还剩一个元素,而是空队列。

但是在实际中,假设这个队列的总个数不超过5个,但是目前如果接着入队的话,因数组末尾元素已经占有,再向后加,就会产生数组越界的错误,可实际上在下标为0和1的位置上还是空闲的。我们把这种现象叫做“假溢出”。

2.循环队列定义

所以解决假溢出的办法就是后面满了,就从头开始,也就是头尾相接的循环,我们把队列的这种头尾相接的顺序存储结构称为循环队列。

此时又出现一个问题,我们说空队列时时,front = rear,现在当队列满时,也是front = rear,那么如何判断此时的队列究竟是空还是满呢?

办法一就是设置一个标志变量flag,当front == read ,flag = 0时队列为空;front == rear,且flag = 1时队列满。

办法二是当队列空时,条件就是front = rear,当队列满时,我们修改其条件,保留一个元素空间。也就是说,队列满时,数组中还有一个空闲单元。

我们重点讨论第二种方法,由于rear可能比front大,也可能比front小,所以尽管他们只差一个位置时就是满的情况,但也可能差整整一圈。所以最大尺寸为QueueSize,那么队列满的条件是(rear + 1) % QueueSize == front(考虑到rear在最后,front在最前的情况,所以%QueueSize)

另外,计算队列长度:当rear> front时,此时队列的长度为rear - front;但当rear < front时,队列长度是rear - front + QueueSize。

因此同用的计算队列长度的公式为
(rear - front + QueueSize) % QueueSize

有了这些讲解,现在实现循环队列的代码就不难了。
循环队列的顺序存储结构代码如下:

typedef int QElemType;//QElemType根据实际情况而定 这里假设为int
//循环队列的顺序存储结构
typedef struct
{
    QElemType date[MAXSIZE];
    int front;//头指针
    int rear;//尾指针,若队列不空,则指向队列尾元素的下一个位置
}SqQueue;

循环队列的初始化代码如下:

//初始化一个队列
Status InitQueue(SqQueue *Q)
{
    Q->front = 0;
    Q->rear = 0;
    return OK;
}

循环队列的长度代码如下:

int QueueLength(SqQueue Q)
{
    return (Q.rear - Q.front + MAXSIZE) % MAXSIZE; 
}

循环队列的入队列操作代码如下:

//若队列未满,则插入元素e为Q新的队尾元素
Status EnQueue(SqQueue *Q,QElemType e)
{
    if((Q->rear + 1) % MAXSIZE == Q->front)//队列满的判断
    {
        return ERROR;
    }
    Q->data[Q->rear] = e;//将元素e赋给队尾
    Q->rear = (Q->rear + 1) % MAXSIZE;//队尾指针后移一位
                                    //若到最后则转移到数组头部
} 

循环队列的出队列代码如下所示

//若队列不空,则删除Q中队头元素,用e返回其值
Status DeQueue(SqQueue *Q , QElemType *e)
{
    if(Q->front == Q->rear)//队列空,ERROR
        return ERROR;
    *e = Q->data[font];//队头元素值赋给e
    Q->front = (Q->front + 1) % MAXSIZE;//头指针后移
                                        //若到最后转移到头部
    return OK;
}

大家应该发现,单是顺序存储,若不是循环队列,算法的时间复杂度是不高的,但是循环队列又面临着可能会溢出的问题,所以我们还需要队列的链式存储结构。

三、队列的链式存储结构及实现

队列的链式存储结构,其实就是线性表的单链表,只不过它只能头进尾出而已,我们把它简称为链队列。为了操作上的简便,我们将队列头指针指向链队列的空的头结点,队尾指向终端结点。

空队列时,front和rear都指向头结点。

链队列的结构为:

typedef int QElemType;//QElemType根据实际情况而定 假设为int

typedef struct QNode//结点结构
{
    ElemType data;
    struct QNode *next;
}QNode,*QueuePtr;

typedef struct //队列的链表结构
{
    QueuePtr front,rear;//队头、队尾指针
}LinkQueue;

1.队列的链式存储结构——入队操作

入队操作时,其实就是在表尾部插入结点。
代码如下:

//插入元素e为Q的新的队尾元素
Status EnQueue(LinkQueue *Q,QElemType e)
{
    QueuePtr s = (QueuePtr)malloc(sizeof(QNode));
    if(!s)//存储分配失败
        exit(OVERFLOW);
    s->data = e;//对新结点赋值
    s->next = NULL;
    Q->rear->next = s;//在原队列尾部插入新结点
    Q-rear = s;//将尾指针指向新结点
}

2.队列的链式存储结构——出队操作

出栈操作时,就是头结点的后继结点出队,将头结点的后继阶段改为它后面的结点。
这里要考虑到一个情况,就是除了头节点外,只有一个元素时,则需要将rear指向头结点。

代码如下:

//若队列不空,删除Q的队头元素,用e返回其值,并返回OK,失败返回ERROR
Status DeQueue(LinkQueue *Q,ElemType *e)
{
    QueuePtr p;
    if(Q->rear == Q->front)//若队列为空
        return ERROR;
    p = Q->front->next;//将欲删除的结点暂时赋给p
    *e = p->data;//将与删除的头结点的值赋给e
    Q->front->next = p->next;//将原头指针的后继p->next
                             //赋给头结点的后继
    if(Q->rear == p)//如果队列中除了头指针结点,只有一个元素
        Q->rear = Q->front;//将尾指针指向头结点
    free(p);
    return OK;
}

对于循环队列与链队列比较。
可以从两方面来考虑,从时间上,其实它们的基本操作时间都是常数时间,都为O(1)不过循环队列事先申请好空间,使用期间不释放,而对于链队列来说,每次申请和释放结点也存在一些事件开销,如果入队出队频繁,则两者还是有细微差异

对于空间上来说,循环队列必须有一个固定长度,所以就有存储元素个数和空间浪费的问题。而链队列不存在这个问题,尽管它需要一个指针域,会产生一些空间上的开销,但也可以接受。所以在空间上,链队列更灵活。

总的来说,可以在确定队列长度最大值的情况下,建议使用循环队列;如果你无法预估队列的长度时,则使用链队列

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值