所有的ADT都必须确定一件事情:如何分配内存来存储值,也就是顺序存储还是链式存储。顺序存储分为静态数组和动态分配的数组。就队列而言,顺序分配的队列需要在队列实现之前确定整个队列的长度,内存分配以数组方式实现;而链式队列无需指定队列长度,只要内存充足可以随时增加队列。为了在连续分配的有限内存中使用队列,需要将队列曾经释放的内存空间循环利用起来,因此产生了循环队列。循环队列避免了常规队列中使用数组的空间浪费。
1. 循环队列如何工作
如上图所示,front指针指向队列的首部,rear指针指向队列的尾部。每新增一个元素,front指针不变,rear指针向后移动一位;每弹出一个元素,front指针向后移动一位,rear指针不变。经过一段时间的排队和出列,队列0,1索引对应的空间已经出现了空闲,队列元素数量已经减小。如果没有循环队列,只有当所有元素都已出队列,重置队列后才能使用索引0和1。
循环队列的工作过程是循环递增的,即当队列末尾没有空间时,将变量添加到队列的开始。假设队列共有8个空间,新增1个元素的过程分为两步:1、rear指向下一个索引,其计算公式为 REAR=(REAR + 1)%8;2、将数值放到索引对应的内存空间。
2. 队列空与满的判断
当front与rear指向同一个空间时,有两种可能:队列空或者队列满,所以无法根据这种情况判断队列的空与满。常见的判断队列的空与满有两种思路:1、引入一个新变量,用于记录队列中的元素数量;2、重新定义队列的 " 空 " 与 " 满 "。
2.1 引入新变量判断队列的空与满
队列主要接口函数只有4个:isFull()用于判断队列是否满;isEmpty()用于判断队列是否空;enQueue()用于将新元素压入队列末尾;deQueue()用于将队列首部的元素弹出。假设循环队列元素数量为8,number变量负责表示队列元素的数量,队列状态变化过程如下表:
操作步骤 | 初始状态 | 插入一个元素 | 删除一个元素 | 连续插入元素 | 倒数第二次插入元素 | 最后一次插入元素 |
---|---|---|---|---|---|---|
front | 0 | 0 | 1 | … | 1 | 1 |
rear | -1 | 0 | 0 | … | 7 | 0 |
number | 0 | 1 | 0 | … | 7 | 8 |
队列状态 | 空 | 不空不满 | 空 | … | 不空不满 | 满 |
将代码保存为queue.addvar.c文件,用于后续验证,具体如下:
// Circular Queue implementation in C
#include <stdio.h>
#define SIZE 8
int items[SIZE];
int front = 0, rear = -1;
int number=0;
// Check if the queue is full
int isFull() {
if ( number == SIZE ) return 1;
return 0;
}
// Check if the queue is empty
int isEmpty() {
if ( number == 0 ) return 1;
return 0;
}
// Adding an element
void enQueue(int element) {
if (isFull())
printf("\n Queue is full!! \n");
else {
rear = (rear + 1) % SIZE;
items[rear] = element;
number++;
printf("\n Inserted -> %d", element);
}
}
// Removing an element
int deQueue() {
int element;
if (isEmpty()) {
printf("\n Queue is empty !! \n");
return (-1);
} else {
element = items[front];
front = (front + 1) % SIZE;
number--;
printf("\n Deleted element -> %d \n", element);
return (element);
}
}
// Display the queue
void display() {
int i;
if (isEmpty())
printf(" \n Empty Queue\n");
else {
printf("\n Front -> %d ", front);
printf("\n Items -> ");
for (i = front; i != rear; i = (i + 1) % SIZE) {
printf("%d ", items[i]);
}
printf("%d ", items[i]);
printf("\n Rear -> %d \n", rear);
}
}
2.2 重新定义队列的空与满
简单画了一下队列的空与满的状态图,队列空时,rear向前移1位就等于front,如下图所示:
队列满时,rear向前移2位等于front,如下图所示:
可以看到,使用该方法会浪费1个单位的空间。talk is cheap,保存代码为queue.define.c,用于后续验证,代码内容如下:
// Circular Queue implementation in C
#include <stdio.h>
#define SIZE 8
int items[SIZE];
int front = 1, rear = 0;
// Check if the queue is full
int isFull() {
if ( front == (rear + 2) % SIZE ) return 1;
return 0;
}
// Check if the queue is empty
int isEmpty() {
if ( front == (rear + 1) % SIZE ) return 1;
return 0;
}
// Adding an element
void enQueue(int element) {
if (isFull())
printf("\n Queue is full!! \n");
else {
rear = (rear + 1) % SIZE;
items[rear] = element;
printf("\n Inserted -> %d", element);
}
}
// Removing an element
int deQueue() {
int element;
if (isEmpty()) {
printf("\n Queue is empty !! \n");
return (-1);
} else {
element = items[front];
front = (front + 1) % SIZE;
printf("\n Deleted element -> %d \n", element);
return (element);
}
}
// Display the queue
void display() {
int i;
if (isEmpty())
printf(" \n Empty Queue\n");
else {
printf("\n Front -> %d ", front);
printf("\n Items -> ");
for (i = front; i != rear; i = (i + 1) % SIZE) {
printf("%d ", items[i]);
}
printf("%d ", items[i]);
printf("\n Rear -> %d \n", rear);
}
}
2.3 比较两种方法的实现结果
编写main.c,内容如下。因为首先验证新增变量的方法,所以注释掉文件头部的queue.define.c文件。
#include "queue.addvar.c"
//#include "queue.define.c"
int main() {
// Fails
deQueue();
enQueue(1);
enQueue(2);
enQueue(3);
enQueue(4);
enQueue(5);
enQueue(6);
enQueue(7);
enQueue(8);
// Fails
enQueue(9);
display();
deQueue();
display();
enQueue(10);
display();
// Fails
enQueue(11);
return 0;
}
编译执行,结果如下:
Queue is empty !!
Inserted -> 1
Inserted -> 2
Inserted -> 3
Inserted -> 4
Inserted -> 5
Inserted -> 6
Inserted -> 7
Inserted -> 8
Queue is full!!
Front -> 0
Items -> 1 2 3 4 5 6 7 8
Rear -> 7
Deleted element -> 1
Front -> 1
Items -> 2 3 4 5 6 7 8
Rear -> 7
Inserted -> 10
Front -> 1
Items -> 2 3 4 5 6 7 8 10
Rear -> 0
Queue is full!!
如法炮制,注释掉main.c中的queue.addvar.c,验证重新定义队列空与满的方法,编译执行,结果如下:
Queue is empty !!
Inserted -> 1
Inserted -> 2
Inserted -> 3
Inserted -> 4
Inserted -> 5
Inserted -> 6
Inserted -> 7
Queue is full!!
Queue is full!!
Front -> 1
Items -> 1 2 3 4 5 6 7
Rear -> 7
Deleted element -> 1
Front -> 2
Items -> 2 3 4 5 6 7
Rear -> 7
Inserted -> 10
Front -> 2
Items -> 2 3 4 5 6 7 10
Rear -> 0
Queue is full!!
很明显,可以看到该方法实际使用的内存空间比分配的内存空间少1。有了接口函数,不用操心具体的实现过程了,真的很爽。
参考文献
[1]Kenneth A.Reek.C和指针[M].人民邮电出版社:北京,2009:364-369.
[2]严蔚敏,吴伟民.数据结构(第二版)[M].清华大学出版社:北京,2010:63-64.
[3]Parewa Labs Pvt. Ltd.Circular Queue[EB/OL].https://www.programiz.com/dsa/circular-queue,2020-01-01.
[4]Arpit Gaurav, AshwinGoel.Circular Queue | Set 1 (Introduction and Array Implementation)[EB/OL].https://www.geeksforgeeks.org/circular-queue-set-1-introduction-array-implementation/,2020-01-01.