队列
1、队列的基本概念
队列是一种在计算机科学中常见的抽象数据类型,是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾(Rear),允许删除的一端称为队头(Front)。
1.1、队列的特点
- 元素按照加入队列的顺序排列,最先加入的元素最先被访问和移除。
- 只能在队列的一端(称为队尾)添加新元素,而只能在另一端(称为队头)访问和移除元素。
- 队列的长度可以动态改变,可以动态添加和删除元素。
- 队列中可以存储不同类型的元素。
- 队列的操作包括入队(enqueue)和出队(dequeue),分别表示在队尾添加元素和在队头移除元素。
- 队列可以用数组或链表实现,具体实现方式有数组队列、链表队列等。
1.2、队列的常用操作
- 入队(enqueue):将元素插入到队列的尾部。
- 出队(dequeue):从队列的头部删除一个元素。
- 队列是否为空(isEmpty):判断队列是否为空。
- 队列大小(size):返回队列中元素的个数。
- 获取队头元素(front):返回队列的头部元素,但不删除。
- 清空队列(clear):清空队列中的所有元素。
1.3、队列的应用场景
- 任务调度:在操作系统中,队列常用于任务调度,确保任务按照顺序执行。
- 消息传递:在消息系统中,队列用于存储和传递消息,保证消息的顺序性。
- 网络通信:在网络通信中,队列常用于处理网络请求,确保请求按照顺序处理。
- 打印机管理:打印机管理系统中,队列用于管理打印任务,确保先进来的任务先打印。
- 缓冲区:在计算机系统中,队列可以用于存储和管理缓冲区数据,例如网络缓冲区、磁盘缓冲区等。
- 排队系统:在银行、超市等场所,队列用于管理客户的排队顺序,保证公平性和效率。
2、队列的顺序存储结构
2.1、顺序队列
2.1.1、顺序队列
顺序队列是一种基于数组实现的队列数据结构。
顺序队列的优点是实现简单,插入和删除元素的时间复杂度为O(1)。缺点是在插入和删除元素时可能需要移动大量的元素,当队列的容量较大时效率较低,且浪费空间。
2.1.2、顺序队列的特点
- 使用数组存储元素,通过数组的下标来表示队列中元素的位置。
- 队列有一个头指针和一个尾指针,分别指向队列的头部和尾部元素。
- 当队列为空时,头指针和尾指针指向同一个位置;当队列满时,头指针和尾指针指向数组的结尾位置。
2.1.3、顺序队列中的溢出现象
- "下溢"现象:当队列为空时,做出队运算产生的溢出现象。“下溢”是正常现象,常用作程序控制转移的条件。
- "真上溢"现象:当队列满时,做进对运算产生空间溢出的现象。“真上溢”是一种出错状态,应设法避免。
- "假上溢"现象:由于入队和出队操作中,头尾指针只增加不减小,致使被删元素的空间永远无法重新利用。当队列中实际的元素个数远远小于向量空间的规模时,也可能由于尾指针已超越向量空间的上界而不能做入队操作。该现象称为"假上溢"现象。
如图(d),队列出现“上溢出”,然而却又不是真正的溢出,所以是一种“假溢出”。
2.1.4、顺序队列中的操作
初始状态(队空条件):q->front = q->rear = 0
。
进队操作:队不满时,先送值到队尾元素,再将队尾指针加1。
出队操作:队不空时,先取队头元素值,再将队头指针加1。
#include <stdio.h>
#define MAXSIZE 100
// 定义队列结构
typedef struct
{
int data[MAXSIZE]; // 队列的数据存储数组
int front; // 队头指针
int rear; // 队尾指针
} SeqQueue;
// 初始化队列
void initQueue(SeqQueue *q)
{
q->front=q->rear=0;
}
// 判断队列是否为空
int isEmpty(SeqQueue *q)
{
return q->front==q->rear;
}
// 入队操作
void enqueue(SeqQueue *q, int x)
{
if (q->rear==MAXSIZE)
{
printf("队列已满,无法插入!\n");
return;
}
q->data[q->rear]=x;
q->rear++;
}
// 出队操作
int dequeue(SeqQueue *q)
{
if (isEmpty(q))
{
printf("队列为空,无法删除!\n");
return -1;
}
int x=q->data[q->front];
q->front++;
return x;
}
// 打印队列中的元素
void printQueue(SeqQueue *q)
{
printf("队列中的元素为:\n");
for (int i=q->front;i<q->rear;i++)
{
printf("%d ", q->data[i]);
}
printf("\n");
}
int main() {
SeqQueue q;
initQueue(&q);
enqueue(&q, 1);
enqueue(&q, 2);
enqueue(&q, 3);
printQueue(&q);
int x=dequeue(&q);
printf("出队的元素为:%d\n", x);
printQueue(&q);
return 0;
}
2.2、循环队列
2.2.1、循环队列
在顺序队列中说到,顺序队列中存在假溢出现象,那解决假溢出的办法就是后面满了,就再从头开始喽^~^,也就是头尾相接的循环。我们把队列的这种头尾相接的循环存储结构称为循环队列。
特点:
可以重复利用数组空间,避免了普通队列因为队列头部空出的位置无法再利用而浪费空间的问题。使用循环队列可以提高队列的效率,尤其在需要频繁进行入队和出队操作的情况下。
2.2.2、循环队列的实现方式
初始时,front和rear都指向数组的第一个位置。
入队操作时,先将元素插入rear指向的位置,然后将rear指针向后移动一位。如果rear指针超过了数组的最大索引值,则将其设置为0,从数组的第一个位置开始继续插入。
出队操作时,先将front指向的元素取出,然后将front指针向后移动一位。如果front指针超过了数组的最大索引值,则将其设置为0,从数组的第一个位置开始继续取出。
空间浪费问题解决了,队列满和队列空均存在Rear==Front,该如何区分呢?
为了区分队空还是队满的情况
牺牲一个单元来区分队空和队满,入队时少用一个队列单元,这是种较为普遍的做法,约定以“队头指针在队尾指针的下一位置作为队满的标志”,如图
- 初始时:q->front=q->rear=0
- 队首指针进1:q->front=(q->front+1)%MAXSIZE
- 队尾指针进1:q->rear=(q->rear+1)%MAXSIZE
- 队满条件: (q->rear+1)%MAISIZE==q->front
- 队空条件: q->front==q->rear
- 队列中元素的个数: (q->rear-q->front+MAISIZE)%MAISIZE
(当rear>pront时,元素的个数为rear-pront,如图1;当rear<pront时,元素的个数为 MAISIZE-pront+rear,如图2)
图1 图2
2.2.3、循环队列中的操作
#include <stdio.h>
#define MIXSIZE 50
//队列的顺序存储类
typedef struct
{
int data[MIXSIZE]; // 队列的数据存储数组
int front; // 队列的头部指针
int rear; // 队列的尾部指针
} Queue;
//初始化队列的指针
void initQueue(Queue *q)
{
q->front=q->rear=0;
}
// 判断队列是否已满
int isFull(Queue *q)
{
if((q->rear+1)%MIXSIZE==q->front)
return 1;//满
else return 0;
}
// 判断队列是否为空
int isEmpty(Queue *q)
{
if(return q->front==q->rear)
return 1;//空
else return 0;
}
//求队列当前长度
int QueueLength(Queue *q)
{
return (q.rear-q.front+MAXSIZE)%MAXSIZE;
}
// 入队操作
void enqueue(Queue *q, int value)
{
if(isFull(q))
{
printf("Queue is full.\n");
return;
}
q->data[q->rear]=value;
q->rear=(q->rear+1)%MIXSIZE;
}
// 出队操作
int dequeue(Queue *q)
{
if(isEmpty(q))
{
printf("Queue is empty.\n");
return -1;
}
int value=q->data[q->front];
q->front=(q->front+1)%MIXSIZE;
return value;
}
int main() {
Queue q;
initQueue(&q);
enqueue(&q, 10);
enqueue(&q, 20);
enqueue(&q, 30);
printf("%d\n", dequeue(&q));
printf("%d\n", dequeue(&q));
printf("%d\n", dequeue(&q));
printf("%d\n", dequeue(&q)); // 队列已空,将返回-1
return 0;
}
3、队列的链式存储结构
3.1、链队列
队列的链式存储结构即为链队列,是一个同时带有队头指针和队尾指针的单链表,只能尾进头出。
1)链队列由结点构成,每个结点包含一个元素和一个指向下一个节点的指针。
2)链队列有两个指针:队首指针和队尾指针。初始时,队首指针和队尾指针都指向空结点。
3)队首指针指向链队列中的第一个结点,队尾指针指向链队列中的最后一个结点。
4)当队列为空时,队首指针和队尾指针都指向空结点。
5)当队列不为空时,队首指针指向队列中的第一个结点,队尾指针指向队列中的最后一个结点。
6)在链队列中插入一个元素时,需要创建一个新结点,并将其插入到队尾节点的后面。
7)在链队列中删除一个元素时,需要删除队首结点,并将队首指针指向下一个节点。
3.2、链队列中的操作
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构体
typedef struct Node
{
int data; // 数据域
struct Node* next; // 指针域
} Node;
// 定义链队列结构体
typedef struct
{
Node* front; // 队头指针
Node* rear; // 队尾指针
} Queue;
// 初始化队列
void initQueue(Queue* q)
{
q->front=NULL;
q->rear=NULL;
}
// 判断队列是否为空
int isEmpty(Queue* q)
{
return q->front==NULL;
}
// 入队操作
void enqueue(Queue* q, int data)
{
Node* newNode=(Node*)malloc(sizeof(Node));
newNode->data=data;
newNode->next=NULL;
if(isEmpty(q))
{
q->front=newNode;
q->rear=newNode;
}
else
{
q->rear->next=newNode;
q->rear=newNode;
}
}
// 出队操作
int dequeue(Queue* q)
{
if(isEmpty(q))
{
printf("Queue is empty.\n");
return -1;
}
Node* node=q->front;
int data=node->data;
q->front=node->next;
if(q->front==NULL)
{
q->rear=NULL;
}
free(node);
return data;
}
// 获取队头元素
int getFront(Queue* q)
{
if(isEmpty(q))
{
printf("Queue is empty.\n");
return -1;
}
return q->front->data;
}
// 获取队列长度
int getSize(Queue* q)
{
Node* node=q->front;
int size=0;
while(node!=NULL)
{
size++;
node=node->next;
}
return size;
}
// 清空队列
void clearQueue(Queue* q)
{
Node* node=q->front;
while(node!=NULL)
{
Node* temp=node;
node=node->next;
free(temp);
}
q->front=NULL;
q->rear=NULL;
}
// 打印队列元素
void printQueue(Queue* q)
{
Node* node=q->front;
printf("Queue: ");
while (node!=NULL)
{
printf("%d ",node->data);
node=node->next;
}
printf("\n");
}
int main() {
Queue queue;
initQueue(&queue);
printf("Is queue empty? %s\n", isEmpty(&queue) ? "Yes" : "No");
enqueue(&queue, 1);
enqueue(&queue, 2);
enqueue(&queue, 3);
printf("Queue size: %d\n", getSize(&queue));
printQueue(&queue);
printf("Front element: %d\n", getFront(&queue));
dequeue(&queue);
printf("Queue size: %d\n", getSize(&queue));
printQueue(&queue);
clearQueue(&queue);
printf("Is queue empty? %s\n", isEmpty(&queue) ? "Yes" : "No");
return 0;
}
Is queue empty? Yes
Queue size: 3
Queue: 1 2 3
Front element: 1
Queue size: 2
Queue: 2 3
Is queue empty? Yes
4、双端队列
4.1、双端队列的定义
双端队列(double-ended queue),或简称为deque,是一种具有两个端点的队列数据结构,可以从两端插入和删除元素。
双端队列可以在队列的头部和尾部进行插入和删除操作,因此它既具备了栈的特性(LIFO,后进先出),也具备了队列的特性(FIFO,先进先出)。这使得双端队列在特定情况下非常有用。
双端队列的一些常见操作包括:
- push_front(x):将元素x插入双端队列的头部。
- push_back(x):将元素x插入双端队列的尾部。
- pop_front():删除双端队列头部的元素,并返回删除的元素。
- pop_back():删除双端队列尾部的元素,并返回删除的元素。
- front():返回双端队列头部的元素,不删除元素。
- back():返回双端队列尾部的元素,不删除元素。
- empty():判断双端队列是否为空。
- size():返回双端队列中元素的个数。
双端队列可以用数组或链表来实现。采用数组实现时,需要考虑插入和删除操作对数组大小的影响,可能需要进行动态调整。采用链表实现时,插入或删除操作的时间复杂度为O(1)。
4.2、特殊的双端队列
输出受限制的双端队列:
输入受限制的双端队列: