循环队列作用

为充分利用向量空间,克服"假溢出"现象的方法。

循环队列的原理

Java循环队列出队 循环队列出队列_Java循环队列出队

在环状顺序表中,最后一个位置(a[6])和第一个位置(a[0])紧挨着,这样做的好处是:

  1. 随着元素做入队和出队操作,整个队列也只会在环状表中 “绕圈圈”,不存在队列整体向顺序表尾部移动的问题,顺序表的空间可以充分利用;
  2. 不存在“假溢出”的问题,当队列中的元素占满整个顺序表时,新元素入队会发生“真溢出”错误。

将向量空间想象为一个首尾相接的圆环,并称这种向量为循环向量。存储在其中的队列称为循环队列(Circular Queue)。循环队列是把顺序队列首尾相连,把存储队列元素的表从逻辑上看成一个环,成为循环队列。

循环队列就是将队列存储空间的最后一个位置绕到第一个位置,形成逻辑上的环状空间,供队列循环使用。在循环队列结构中,当存储空间的最后一个位置已被使用而再要进入队运算时,只需要存储空间的第一个位置空闲,便可将元素加入到第一个位置,即将存储空间的第一个位置作为队尾。 循环队列可以更简单防止伪溢出的发生,但队列大小是固定的。

在循环队列中,当队列为空时,有front=rear,而当所有队列空间全占满时,也有front=rear。为了区别这两种情况,规定循环队列最多只能有MaxSize-1个队列元素,当循环队列中只剩下一个空存储单元时,队列就已经满了。因此,队列判空的条件是front=rear,而队列判满的条件是front=(rear+1)%MaxSize。

循环队列实现入队操作

循环队列实现入队操作的过程和顺序队列类似,完成以下两步操作即可:

  1. 将新元素添加到 rear 指向的空闲空间;
  2. rear 向后移动一位,指向下一个空闲空间,为下次入队新元素做准备。
    例如,在图 1 的基础上,向队列中添加一个新元素 5,实现过程如下图所示:

    可以看到,当顺序表还有空闲空间时,由于我们将它想象成“首尾相连”的状态, a[6] 和 a[0] 紧挨着,rear 变量向后移动一位,会指向 a[0] 的位置。这意味着,队列左侧的空闲空间可以再次利用起来。

需要注意的是,循环队列判断“已满”的方式比较特殊。当我们根据图 2 的方法尝试将元素 6、7 分别入队时,最终的存储状态会变成下图所示:

Java循环队列出队 循环队列出队列_循环队列_02


在图 1 中,我们可以用toprear作为空队列的判断标志,但图 3 中队列已满的状态也是toprear,明显它们是冲突的。解决冲突常用的方法是:仍用toprear作为队列为空的判断标志,将队列已满的判断方法改为(rear+1)%MAX_LENtop,其中 MAX_LEN 为顺序表(数组)的长度。

例如,在图 2 的基础尝试将元素 6 入队,此时 rear 的值为 0,(rear+1)%MAX_LEN的值为 1,而 top 的值为 2,所以等式不成立,意味着循环队列未满,元素 6 就成功存储到了 a[0] 处。

Java循环队列出队 循环队列出队列_算法_03


在图 4 的基础上尝试将元素 7 入队,此时 rear 的值为 1,(rear+1)%MAX_LEN的值为 2,而 top 的值为 2,所以等式成立,意味着循环队列已满,元素 7 就无法入队。

有读者可能会问,图 4 的顺序表中明明还有一块空闲空间没有利用呢?是的,这就是循环队列判断“已满”的方法,浪费一块存储空间,避免和“队列为空”的状态发生冲突。

循环队列实现入队的 C 语言代码为:

int enQueue(int* a, int top, int rear, int data) {
    //添加判断语句,如果rear超过max,则直接将其从a[0]重新开始存储,如果rear+1和top重合,则表示顺序表已满
    if ((rear + 1) % MAX_LEN == top) {
        printf("空间已满\n");
        return rear;
    }
    //将新元素入队
    a[rear % MAX_LEN] = data;
    printf("元素 %d 成功入队\n", data);
    //rear记录下一个空闲空间的位置
    rear = (rear + 1) % MAX_LEN;
    return rear;
}

程序中并没有直接将 data 元素存储到 a[rear] 中,而是将其存储到 a[rear%MAX_LEN] 中,这样就可以将顺序表当做环状表来用。

循环队列实现出队操作

循环队列实现出队的过程也和顺序队列类似,依次执行以下两步操作:

  1. 将 top 记录的队头元素出队;
  2. 将 top 向后移动一位,记录新队头元素的位置。

前面已经讲过,循环队列判断“队列为空”的标志是 top==rear,因此循环队列实现出队操作的 C 语言代码为:

int deQueue(int* a, int top, int rear) {
    //如果top==rear,表示队列为空
    if (top == rear) {
        printf("队列为空\n");
        return top;
    }
    printf("元素 %d 成功出队\n", a[top]);
    //top向后移动一个位置,记录新的队头
    top = (top + 1) % MAX_LEN;
    return top;
}

循环队列的完整实现程序

#include <stdio.h>
#define MAX_LEN 5//表示顺序表申请的空间大小
int enQueue(int* a, int top, int rear, int data) {
    //添加判断语句,如果rear超过max,则直接将其从a[0]重新开始存储,如果rear+1和top重合,则表示顺序表已满
    if ((rear + 1) % MAX_LEN == top) {
        printf("空间已满\n");
        return rear;
    }
    //将新元素入队
    a[rear % MAX_LEN] = data;
    printf("元素 %d 成功入队\n", data);
    //rear记录下一个空闲空间的位置
    rear = (rear + 1) % MAX_LEN;
    return rear;
}
int deQueue(int* a, int top, int rear) {
    //如果top==rear,表示队列为空
    if (top == rear) {
        printf("队列为空\n");
        return top;
    }
    printf("元素 %d 成功出队\n", a[top]);
    //top向后移动一个位置,记录新的队头
    top = (top + 1) % MAX_LEN;
    return top;
}
int main() {
    //定义长度为 5 的顺序表
    int a[MAX_LEN] = {0};
    //当队列中没有元素时,队头和队尾指向同一位置
    int top = 0, rear = 0;
    //元素 1 成功入队
    rear = enQueue(a, top, rear, 1);
    //元素 2 成功入队
    rear = enQueue(a, top, rear, 2);
    //元素 3 成功入队
    rear = enQueue(a, top, rear, 3);
    //元素 4 成功入队
    rear = enQueue(a, top, rear, 4);
    //元素 5 入队会失败
    rear = enQueue(a, top, rear, 5);

    //元素 1 成功出队
    top = deQueue(a, top, rear);
    //元素 5 再入队,会成功
    rear = enQueue(a, top, rear, 5);
    //元素 2 成功出队
    top = deQueue(a, top, rear);
    //元素 3 成功出队
    top = deQueue(a, top, rear);
    //元素 4 成功出队
    top = deQueue(a, top, rear);
    //元素 5 成功出队
    top = deQueue(a, top, rear);
    //队列为空时,出队操作失败
    top = deQueue(a, top, rear);
    return 0;
}

运行结构为:

元素 1 成功入队
元素 2 成功入队
元素 3 成功入队
元素 4 成功入队
空间已满
元素 1 成功出队
元素 5 成功入队
元素 2 成功出队
元素 3 成功出队
元素 4 成功出队
元素 5 成功出队
队列为空

总结

// 队列的顺序存储结构(循环队列)
#define MAX_QSIZE 5 // 最大队列长度+1
typedef struct {
    int *base; // 初始化的动态分配存储空间
    int front; // 头指针,若队列不空,指向队列头元素
    int rear; // 尾指针,若队列不空,指向队列尾元素的下一个位置
} SqQueue;
 
 
// 构造一个空队列Q
SqQueue* Q_Init() {
    SqQueue *Q = (SqQueue*)malloc(sizeof(SqQueue));
    // 存储分配失败
    if (!Q){
        exit(OVERFLOW);
    }
    Q->base = (int *)malloc(MAX_QSIZE * sizeof(int));
    // 存储分配失败
    if (!Q->base){
        exit(OVERFLOW);
    }
    Q->front = Q->rear = 0;
    return Q;
}
 
// 销毁队列Q,Q不再存在
void Q_Destroy(SqQueue *Q) {
    if (Q->base)
        free(Q->base);
    Q->base = NULL;
    Q->front = Q->rear = 0;
    free(Q);
}
 
// 将Q清为空队列
void Q_Clear(SqQueue *Q) {
    Q->front = Q->rear = 0;
}
 
// 若队列Q为空队列,则返回1;否则返回-1
int Q_Empty(SqQueue Q) {
    if (Q.front == Q.rear) // 队列空的标志
        return 1;
    else
        return -1;
}
 
// 返回Q的元素个数,即队列的长度
int Q_Length(SqQueue Q) {
    return (Q.rear - Q.front + MAX_QSIZE) % MAX_QSIZE;
}
 
// 若队列不空,则用e返回Q的队头元素,并返回OK;否则返回ERROR
int Q_GetHead(SqQueue Q, int &e) {
    if (Q.front == Q.rear) // 队列空
        return -1;
    e = Q.base[Q.front];
    return 1;
}
 
// 打印队列中的内容
void Q_Print(SqQueue Q) {
    int p = Q.front;
    while (Q.rear != p) {
        cout << Q.base[p] << endl;
        p = (p + 1) % MAX_QSIZE;
    }
}
 
// 插入元素e为Q的新的队尾元素
int Q_Put(SqQueue *Q, int e) {
    if ((Q->rear + 1) % MAX_QSIZE == Q->front) // 队列满
        return -1;
    Q->base[Q->rear] = e;
    Q->rear = (Q->rear + 1) % MAX_QSIZE;
    return 1;
}
 
// 若队列不空,则删除Q的队头元素,用e返回其值,并返回1;否则返回-1
int Q_Poll(SqQueue *Q, int &e) {
    if (Q->front == Q->rear) // 队列空
        return -1;
    e = Q->base[Q->front];
    Q->front = (Q->front + 1) % MAX_QSIZE;
    return 1;
}