06--队列的顺序实现

868 阅读5分钟

一、队列的特征

队列是线性结构中的一种特殊限定性的数据结构,它有以下特征:

  • 1.线性结构的特征队列都有,关于线性结构的特征可以看文章# 01--线性表的顺序存储;
  • 2.队列的特殊限定性特征是:先进先出
  • 3.队列由一个方向入队一个方向出队。我们称入队的方向为队尾,出队的方向队头

二、队列顺序存储分析

下面来介绍队列在顺序存储中的设计与实现,顺序存储的特点是内存连续且有限。结合队列的特征顺序存储的特点,我们来分析一下队列如何实现顺序存储。

假设我们设计一个顺序存储的数组来保存队列,如下图所示:

image.png

  • 1.当队列为空时,队尾rear等于队头front为0;
  • 2.将c1、c2、c3依次入队。此时队头front为0,队尾rear为3;
  • 3.将c1 、c2出队。此时队头front移动到2的位置,队尾rear为3;
  • 4.将c4 c5 c6 入队,随后将c3 c4出队,此时队头front为4,队尾rear为5。

通过上面的一系列操作,最终最大长度为6的队列的4、5位置保存了有效数据,队尾rear = 6-1为5,即数组存满了,但0~3位置其实是没有存储有效数据的。这种情况我们称之为假溢出问题。

三、循环队列

可以使用循环队列来解决假溢出问题,如下图:

image.png

  • 1.初始化一个队列,队尾rear等于队头front等于0;
  • 2.a b c入队,队尾rear指向3;
  • 3.a出队,队头front指向1;
  • 4.d e f g 入队,此时队头front和队尾rear又指向了同一位置1;

通过上面的步骤得出,当队空队满时,队头front和队尾rear都相等,这样就无法判断是队空还是队满了。解决这个问题的办法是在队头和队尾之间永远空出一个位置,牺牲一个存储单元来,这并不会影响对队列的存储空间有多大影响。

通过上面的分析,我们总结如下:

  • 1.设计一个循环队列来实现队列的顺序存储
  • 2.在队尾和队头之间永远空出一个位置,方便判断队满的情况;
  • 3.队空的判断条件是队头front等于队尾rear
  • 4.队满的判断条件是队头rear+1 对数组长度MAXSIZE等于队头front;
  • 5.循环队列的实现在出队、入队修改位置时,对位置模以数组长度MAXSIZE,即可保证存储空间的有效利用,不会出现假溢出的问题了。

四、循环队列的设计

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;
  • 1.设计一个结构体,data数组为队列的顺序存储空间;
  • 2.front指向队头,rear指向队尾

1、初始化

Status InitQueue(SqQueue *Q){
    Q->front = 0;
    Q->rear = 0;

    return OK;
}

队列的初始状态时队列为空,队头front和队尾rear指向同一位置0。

2、清空

Status ClearQueue(SqQueue *Q){
    Q->front = Q->rear = 0;

    return OK;
}

队列的为空时:队头front等于队尾rear等于0。

3、队列判空

Status QueueEmpty(SqQueue Q){
    //队空标记
    if (Q.front == Q.rear)
        return TRUE;
    else
        return FALSE;
}

队列的判空的条件:队头front等于队尾rear等于0。

4、队列的长度

int QueueLength(SqQueue Q){

    return (Q.rear - Q.front + MAXSIZE)%MAXSIZE;

}

因为是循环阶列,队尾rear可能大于也可能小于队头front,所以要将rear-front+MAXSIZE

5.获取队头数据

Status GetHead(SqQueue Q,QElemType *e){

    //队列已空
    if (Q.front == Q.rear)
        return ERROR;

    *e = Q.data[Q.front];

    return OK;
}

front指向队头,直接从data的front位置取出数据即可。

6.入队

Status EnQueue(SqQueue *Q,QElemType e){

    //队列已满
    if((Q->rear+1)%MAXSIZE == Q->front)
        return ERROR;

    //将元素e赋值给队尾
    Q->data[Q->rear] = e;

    //rear指针向后移动一位,若到最后则转到数组头部;
    Q->rear = (Q->rear+1)%MAXSIZE;
    
    return OK;
}
  • 1.队满的判断条件:(rear+1)%MAXSIZE == front;
  • 2.队尾指向的位置是没有有效数据的,所以入队时,可以先在rear位置赋值,再将rear+1;
  • 3.因为是循环阶队,所以队尾rear+1后要模以MAXSIZE。

7.出队

Status DeQueue(SqQueue *Q,QElemType *e){
   
    //判断队列是否为空
    if (Q->front == Q->rear) {
        return ERROR;
    }

    //将队头元素赋值给e
    *e = Q->data[Q->front];

    //front 指针向后移动一位,若到最后则转到数组头部
    Q->front = (Q->front+1)%MAXSIZE;

    return OK;
}
  • 1.队列判空的条件: front == rear
  • 2.队头的位置是有有效数据的,所以可以可以直接取值;
  • 3.出队时,队头要向队尾靠近,所以队头和队尾的运动方向要一致;
  • 4.因为是循环队列,所以队头front+1后要模以MAXSIZE。

8.遍历队列数据

Status QueueTraverse(SqQueue Q){
    int i;
    i = Q.front;
    
    while ((i+Q.front) != Q.rear) {
        printf("%d   ",Q.data[i]);
        i = (i+1)%MAXSIZE;
    }
    printf("\n");

    return OK;
}
  • 1.遍历时,从队头到队尾;
  • 2.在因为是遍历,所以不能改变队列的原有结构,所以要使用一个临时变量i从队头累加到队尾,结束条件是i+front == rear;
  • 3.注意循环队列的位置改变后要模以MAXSIZE,防止数组越界。

五、总结

队列的顺序存储实现要注意假溢出问题和队满判断问题,解决方案是使用循环队列,在队头和队尾之间空出一个位置方便队满的判断。队满和队空的判断如下:

  • 1.判断队空: rear == front;
  • 2.判断队满:(rear + 1)%MAXSIZE = front。

因为是循环队列,所以当rear和front变化时,要模以MAXSIZE得到最终的正确指向。