1. 队列概述

1.1 队列定义

队列是只允许在一端进行插入操作,在另一端进行删除操作的线性表。
队列是一种先进先出的线性表,允许插入的一端称为队尾,允许删除的一端称为对头。
同栈一样,队列也是一种限定性线性表,同时队列也具有顺序存储和链式存储两种方式。
如下图所示:
队列示意图1.1.1.png

1.2 队列演示

队列演示1.2.1.png
图1.2.1中,a1、a2分别入队,此时队头a1,队尾a2。
队列演示1.2.2.png
图1.2.2中,a1出队a3、a4分别入队,此时队头a2,队尾a4。

2. 队列顺序存储

2.1 顺序存储定义

通过之前的文章,我们了解过顺序存储,同样队列也是通过开辟一段地址连续的存储单元存储数据。
如下图所示:
顺序存储示意图2.1.1.png
我们在内存中开辟连续的5个空间用来存储数据,同时我们定义指针front指向队列的第一个元素,定义指针rear指向最后一个元素的下一个位置。
当有元素出队和入队时,front和rear分别向后移动,如下图所示:
顺序存储示意图2.1.2.png
当font和rear指向同一个位置时,代表空队列。

2.2 循环队列

上面图2.1.2中,队列的存储最大数为5,因为只开辟了5个空间,如果继续在下标为4的位置入队,那么rear指针需要继续后移,此时就已经超出了我们开辟的空间范围,造成了假溢出现象,但是我们从图中可知,下标为0的位置是空的,可以存放数据的,因此我们可以再将后续入队的数据放到数组的前面,我们把队列的这种头尾相接的顺序存储结构称为循环队列。
如下图所示:
循环队列示意图2.2.1.png
循环队列示意图2.2.2.png
循环队列示意图2.2.3.png
但是问题又来了,由图2.2.3可以看出,font指针和rear指针指向同一个位置,之前说过我们通过front和rear指向相同位置代表空队列,那么图2.2.3中这种满队列如何判断呢?
当然前辈们已经给出了具体的判断方法,牺牲一个存储单元,当rear和front只相差一个位置时就认为满队列,由于rear可能比font大,也可能比front小,有可能相差一圈,所以若队列的最大尺寸为QueueSize,那么满队的条件就是(rear+1)%QueueSize == front(取模的目的是为了解决rear比front大的问题)。

比如QueueSize为5:
当front=0,rear=4,(4+1)%5 = 0(等于front), 此时队列满。
当front=2,rear=1,(1+1)%5 = 2(等于front), 此时队列满。
当front=2,rear=0,(0+1)%5 = 1(不等于front), 此时队列不满。

当rear > front时,如上图2.1.2,此时队列的长度为rear - front,但是当rear < front时,如上图2.2.2,此时队列的长度分为两段,一段是0 + rear,另一段是QueueSize - front,加在一起即得到队列长度公式:
(rear - front + QueueSize) % QueueSize

2.3 顺序存储队列常规操作

2.3.1 初始化与清空

基础代码如下:

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 20 /* 存储空间初始分配量 */

typedef int Status;
typedef int QElemType; /* QElemType类型根据实际情况而定,这里假设为int */

/* 循环队列的顺序存储结构 */
typedef struct
{
    QElemType data[MAXSIZE];
    int front;       /* 头指针 */
    int rear;        /* 尾指针,若队列不空,指向队列尾元素的下一个位置 */
}SqQueue;

初始化:
创建队列对象Q的时候已经开辟了一段地址连续的空间,因此初始化的时候只需要设置初始值即可。

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

清空:

//将队列清空
Status ClearQueue(SqQueue *Q){
    Q->front = Q->rear = 0;
    return OK;
}

2.3.2 空队列与满队列判断

空队列:
上面我们已经知道,判断空队列的条件为front指针和rear指针指向同一个位置。

//若队列Q为空队列,则返回TRUR,否则返回FALSE;
Status QueueEmpty(SqQueue Q){
    return Q.front == Q.rear;
}

满队列:
上面我们知道,判断满队列的条件是:(rear+1)%QueueSize == front.

Status QueueFull(SqQueue Q) {
    return (Q.rear+1)%MAXSIZE == Q.front;
}

2.3.3 入队与出队

入队:
当队列为满时,将元素e插入到队尾。

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

出队:
当队列不为空时,删除对头元素。

//若队列不空,则删除Q中队头的元素,用e返回值
Status DeQueue(SqQueue *Q,QElemType *e){
    //判断队列是否为空
    if (QueueEmpty(*Q))
        return ERROR;
    //将队头元素赋值给e
    *e = Q->data[Q->front];
    //front 指针向后移动一位,若到最后则转到数组头部
    Q->front = (Q->front+1)%MAXSIZE;
    return OK;
}

2.3.4 其他操作

1. 获取队头元素:

//若队列不空,则用e返回Q的队头元素,并返回OK,否则返回ERROR;
Status GetHead(SqQueue Q,QElemType *e){
    //队列已空
    if (QueueEmpty(Q))
        return ERROR;
    *e = Q.data[Q.front];
    return OK;
}

2. 获取队列元素个数:

//返回Q的元素个数,也就是队列的当前长度
int QueueLength(SqQueue Q){
    return (Q.rear - Q.front + MAXSIZE)%MAXSIZE;
}

3. 队列遍历:

//从队头到队尾依次对队列的每个元素数组
Status QueueTraverse(SqQueue Q){
    int i;
    i = Q.front;
    while (i != Q.rear) {
        printf("元素:%d,位置:%d\n",Q.data[i], i);
        i = (i+1)%MAXSIZE;
    }
    printf("\n");
    return OK;
}

3. 队列链式存储

3.1 链式存储定义

队列的链式存储结构,其实就是线性表的单链表,只不过它只能是尾进头出,我们把它简称为链队列。
链式存储示意图3.1.1.png
为了方便操作,我们一般都会加入头结点。
我们将队头指针front指向头结点,将队尾指针rear指向最后一个结点。
当front和rear都指向头结点时,则认为空队列,如下图:
链式存储示意图3.1.2.png

3.2 链式存储队列常规操作

基础代码如下:

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

typedef int Status;
typedef int QElemType; /* QElemType类型根据实际情况而定,这里假设为int */

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

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

3.2.1 初始化与销毁

初始化:
初始化时我们需要开辟空间,创建一个头结点,并将front指针和rear指针都指向头结点。

/*初始化队列*/
Status InitQueue(LinkQueue *Q){
    //1. 头/尾指针都指向新生成的结点
    Q->front = Q->rear = (QueuePtr)malloc(sizeof(QNode));
    //2.判断是否创建新结点成功与否
    if (!Q->front) {
        return ERROR;
    }
    //3.头结点的指针域置空
    Q->front->next = NULL;
    return OK;
}

销毁:
销毁时,我们需要通过循环将所有结点释放掉。

/*销毁队列Q*/
Status DestoryQueue(LinkQueue *Q){
    //遍历整个队列,销毁队列的每个结点
    while (Q->front) {
        Q->rear = Q->front->next;
        free(Q->front);
        Q->front = Q->rear;
    }
    return OK;
}

3.2.2 空队列判断

判断队列是否为空队列,只需判断front指针和rear指针是否指向同一个位置。

/*判断队列Q是否为空*/
Status QueueEmpty(LinkQueue Q){
    return Q.front == Q.rear;
}

3.2.3 入队与出队

入队:
入队时,需要创建一个新结点,然后将新结点插入到链表尾部。

/*插入元素e为队列Q的新元素*/
Status EnQueue(LinkQueue *Q,QElemType e){
    //为入队元素分配结点空间,用指针s指向;
    QueuePtr s = (QueuePtr)malloc(sizeof(QNode));
    //判断是否分配成功
    if (!s) {
         return ERROR;
    }
    //将新结点s指定数据域.
    s->data = e;
    s->next = NULL;
    //将新结点插入到队尾
    Q->rear->next = s;
    //修改队尾指针
    Q->rear = s;
    return OK;
}

出队:
出队时,需要将首元结点(头结点的直接后继结点)释放即可,若队头结点就是队尾结点,则删除后将rear指向头结点。

/*出队列*/
Status DeQueue(LinkQueue *Q,QElemType *e){
    QueuePtr p;
    //判断队列是否为空;
    if (Q->front == Q->rear) {
        return ERROR;
    }
    //将要删除的队头结点暂时存储在p
    p = Q->front->next;
    //将要删除的队头结点的值赋值给e
    *e = p->data;
    //将原队列头结点的后继p->next 赋值给头结点后继
    Q->front->next = p ->next;
    //若队头就是队尾,则删除后将rear指向头结点.
    if(Q->rear == p) Q->rear = Q->front;
    free(p);
    return OK;
}

3.2.4 其他操作

1. 获取队头元素:

/*获取队头元素*/
Status GetHead(LinkQueue Q,QElemType *e){
   
    //队列非空
    if (Q.front != Q.rear) {
        //返回队头元素的值,队头指针不变
        *e =  Q.front->next->data;
        return TRUE;
    }
    return  FALSE;
}

2. 获取队列元素个数:

/*获取队列长度*/
int QueueLength(LinkQueue Q){
    int i= 0;
    QueuePtr p;
    p = Q.front;
    while (Q.rear != p) {
        i++;
        p = p->next;
    }
    return i;
}

3. 队列遍历:

/*遍历队列*/
Status QueueTraverse(LinkQueue Q){
    QueuePtr p;
    p = Q.front->next;
    while (p) {
        printf("%d ",p->data);
        p = p->next;
    }
    printf("\n");
    return OK;
}

4. 总结

对于循环队列和链队列的比较,可以从以下两个方面考虑:
① 时间上,他们的基本操作的时间都为O(1),不过循环队列是事先申请好空间,使用期间不释放,而链队列,每次申请和释放也会存在一些时间开销,如果入队出队频繁,两者还是有细微差异的。
② 空间上,循环队列有一个固定的长度,所以就会存在存储空间浪费的问题。而链队列不存在这个问题,尽管它需要一个指针域,有一定的开销,不过还是可以接受的。所以在空间上,链队列更加的灵活。

总之,在可以确定队列长度最大值的情况下,建议用循环队列,如果无法确定长度,则用链队列。


Daniel_Coder
7 声望1 粉丝