在前一节的学习中,我们学习了线性表的一种特殊形式:栈 这一节我们会学习线性表的另一种特殊形式 : 队列
什么是队列
队列是一种特殊的线性表,它的特点就和它的名字一致:
排队!
第一个进来的,第一个出去;最后一个进来的,最后一个出去
即FIRST-IN-FIRST-OUT
在栈中,我们需要一个top来指向栈顶,但是在队列中,需要两个指针 front头指针和rear尾指针
入队只能从队列尾部入,出队只能从队列头部出
队列的顺序表示
1.顺序表示会出现的问题
front指针永远指向第一个元素,rear指针永远指向最后一个元素的下一个结点;
最开始队列为空时front = rear = 0;
有元素入队 rear++; 有元素出队head++;
很快你就会发现一个问题 有元素入队出队head 和 rear指针都在向后移动,如果元素空间是一定的,那前面的空间不就被浪费了吗?可以看到最后一副图,rear已经指向空,但实际上数组中有空位置,但只能又申请新的空间来储存新的元素,随着出栈入栈的元素越来越多,造成的空间浪费就会越来越多。
2.解决方案:循环队列
有两种解决方法:
(1)在每一次有元素出列时,前面都会空出一个新的位置,将所有的元素都往前移一个位置;
但是如果队列中元素很多,那移动次数就会很多,算法效率太低;
(2)循环队列(Circular queue)
队列的首元素和末尾的元素连在一起
有人会问:数组的头和尾不能连接在一起呀! 而且如果front一直加,那最终就会超过队列的最大容量呀!
这里的循环队列不是真的让数组的头和尾连在一起,而是通过一种运算,实现空间的循环利用:
而这种运算 就是取余
1.入队时,rear = (rear + 1) % MAXISIZE
2.出队时,front = (front + 1)%MAXISIZE
使用模运算,可以使rear 和 front的值始终保持在数组范围内,实现空间的循环利用
但是还是有一个问题 当rear == front时,我们怎么知道队列到底是空还是满呢?
我们可以少用一个存储空间:
当队列为空时: front == rear;
当队列为满时, (rear+1)%MAXISIZE == front;
队列中的元素有多少个呢?
即问即答: rear - front
但是这个答案是错误的! 再仔细看看上面的那副图 你会发现 rear 有时候会比 front小
正确的答案应该是|rear-front| ,那转换成表达式就是 (rear - front + m) %m
假设
m = 5
,front = 3
,rear = 1
:
- 直接计算:
rear - front = 1 - 3 = -2
- 使用模运算:
(rear - front + m) % m = (-2 + 5) % 5 = 3 % 5 = 3
3.基本操作
队列的定义
typedef struct {
int* base;
int rear;
int front;
}cqueue;
队列的初始化
void initQueue(cqueue* cq) {
//申请INITSIZE个空间
cq->base = (int*)malloc(INITSIZE * sizeof(int));
if (!cq) exit(1);
cq->front = cq->rear = 0;
}
入队
int enQueue(cqueue* cq, int x) {
//队列已经满了,需要扩展队列长度!
if ((cq->rear + 1) % cq->capacity == cq->front) {
cq->base = (int*)realloc(cq->base, (cq->capacity + 1) * sizeof(int));
if (!cq->base) exit(1);
cq->capacity++;
}
cq->base[cq->rear] = x;
//更新rear
cq->rear = (cq->rear + 1) % cq->capacity;
return 1;
}
出队
int outQueue(cqueue* cq, int* x) {
//队列为空
if (cq->front == cq->rear) return 0;
//带回出队的值
*x = cq->base[cq->front];
//更新front
cq->front = (cq->front + 1) % cq->capacity;
return 1;
}
打印队列
void printQueue(cqueue* cq) {
int m; //遍历队列
m = cq->front;
while (m != cq->rear) {
printf("%d ", cq->base[m]);
m = (m + 1) % cq->capacity;
}
}
求队列长度
int getLen(cqueue* cq) {
return ((cq->rear - cq->front + cq->capacity) % cq->capacity);
}
获取队列头部元素
int getFront(cqueue* cq,int*x) {
//检查是否为空
if (cq->front == cq->rear) return 0;
*x = cq->base[cq->front];
return 1;
}
队列的链式表示
链队列只需要用一个单链表和一个结构体去储存rear和front指针
要注意front指针是指向头结点,rear指针是指向尾结点。
这里我会在学习中产生一个疑问 为什么需要单独用一个结构体去储存rear和front指针,不能用两个单独的指针直接指向这个链队列?
在书写了一遍方法后,我内心有了一个答案: 如果没有rear和front指针,在做入队和出队操作时,每次都需要遍历一遍链表找到首元素和尾元素,就会比较麻烦
基本操作
1.定义
需要定义一个结构体储存rear和front指针,链队列的结点
typedef struct node {
struct node* next;
int data;
}qlink;
typedef struct {
qlink* front;
qlink* rear;
}linkQueue;
2.初始化
void initQueue(linkQueue* Q) {
Q->front = Q->rear = (qlink*)malloc(sizeof(qlink));
if (!Q->front ||!Q->rear) exit(1);
Q->front->next = NULL;
}
3.入队
void enQueue(linkQueue* Q,int x) {
//不存在满队列的情况
qlink* m;
m = (qlink*)malloc(sizeof(qlink));
m->data = x;
m->next = NULL;
if (!m) exit(1);
Q->rear->next = m;
//要记得修改尾指针
Q->rear = m;
}
4.出队
int outQueue(linkQueue* Q, int* x) {
if (Q->rear == Q->front) return 0; //队列为空
qlink* p = Q->front->next; //要注意Q的front是指向头结点而不是有效结点
*x = p->data;
//出队修改头指针
Q->front->next = p->next;
if (Q->rear == p) Q->rear = Q->front; //如果队列中只有一个结点
//删除后队列为空,要重置rear
free(p);
return 1;
}
5.获取长度
int getLen(linkQueue* Q) {
int x = 0;
qlink* m = Q->front->next; //从有效结点开始
while (m) {
m = m->next;
x++;
}
return x;
}
还有其他的一些诸如打印队列(和获取长度方法差不多,遍历一遍队列即可),获取首元素都比较简单,这里就不介绍了。