26考研——栈、队列和数组_队列(3)

二、队列

1、队列的基本概念

队列的定义

队列(Queue)简称队列,也是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一端进行删除。向队列中插入元素称为入队或进队;删除元素称为出队或离队。这和我们日常生活中的排队是一致的,最早排队的也是最早离队的,其操作的特性是先进先出(First In First Out,FIFO)。

队列的结构
  • 队头(Front):允许删除的一端,也称队首。
  • 队尾(Rear):允许插入的一端。
  • 空队列:不含任何元素的空表。
队列是先进先出结构

"吃多了拉就是一个队列结构"形象地描述了队列的先进先出特性,即最先进入队列的元素最先被移除。

队列的基本操作

初始化队列
  • InitQueue(&Q):初始化队列,构造一个空队列 Q
判断队列是否为空
  • QueueEmpty(Q):判队列空,若队列 Q 为空返回 true,否则返回 false
入队操作
  • EnQueue(&Q, x):入队,若队列 Q 未满,将 x 加入,使之成为新的队尾。
出队操作
  • DeQueue(&Q, &x):出队,若队列 Q 非空,删除队首元素,并用 x 返回。
读取队首元素
  • GetHead(Q, &x):读队首元素,若队列 Q 非空,则将队首元素赋值给 x
注意事项

需要注意的是,栈和队列是操作受限的线性表,因此不是任何对线性表的操作都可以作为栈和队列的操作。比如,不可以随便读取栈或队列中间的某个数据。

2、队列的顺序存储结构

顺序队列

顺序队列是队列的顺序实现,利用顺序存储结构(数组)实现的队列。

顺序队列的实现

顺序队列通过分配一块连续的存储单元存放队列中的元素,并附设两个指针:队首指针 front 指向队首元素,队尾指针 rear 指向队尾元素的下一个位置。

顺序队列的操作
  • 入队操作:队不满时,先送值到队尾元素,再将队尾指针加 1。
  • 出队操作:队不空时,先取队首元素值,再将队首指针加 1。
  • 判空条件Q.front == Q.rear,该条件可以作为队列判空的条件。
普通顺序队列的"假溢出"现象

在普通顺序队列中,由于队首指针 front 和队尾指针 rear 的定义不同,可能会出现"假溢出"现象。即当 rear 指针达到数组的最大索引时,即使数组中仍有空位,也无法继续入队,导致溢出。

这种现象并不是真正的溢出,因为在 data 数组中依然存在可以存放元素的空位置。为了避免这种现象,可以采用循环队列的方式,通过取模操作使得 frontrear 指针循环使用数组空间。

代码实操

结构定义
c 复制代码
typedef struct SeqQueue
{
    ElemType data[MAX_SIZE]; // 队列空间,固定大小
    int front;               // 队头指针
    int rear;                // 队尾指针
} SeqQueue;
初始化
c 复制代码
void initQueue(SeqQueue *pq)
{
    pq->front = pq->rear = 0; // 初始化队头和队尾指针
}
判空
c 复制代码
bool empty(SeqQueue *pq)
{
    return pq->front == pq->rear; // 队头和队尾指针相等时队列为空
}
入队
c 复制代码
void enQueue(SeqQueue *pq, ElemType x)
{
    if (pq->rear >= MAX_SIZE) // 检查队列是否已满
    {
        printf("队列空间已满. %d 不能入队.\n", x);
        return;
    }
    pq->data[pq->rear] = x; // 插入数据
    pq->rear++;             // 队尾指针后移
}
出队
c 复制代码
void deQueue(SeqQueue *pq)
{
    if (empty(pq)) // 检查队列是否为空
        return;
    pq->front++;   // 队头指针后移
}
取队头元素
c 复制代码
ElemType front(SeqQueue *pq)
{
    return pq->data[pq->front]; // 返回队头元素
}
打印队列
c 复制代码
void printQueue(SeqQueue *pq)
{
    for (int i = pq->front; i < pq->rear; ++i) // 遍历队列
        printf("%d ", pq->data[i]);
    printf("\n");
}
队列长度
c 复制代码
int sizeQueue(SeqQueue *pq)
{
    return pq->rear - pq->front; // 队列长度为队尾指针与队头指针之差
}

顺序队列使用数组存储队列元素,队头指针和队尾指针分别指向队列的头部和尾部。入队时在队尾插入元素,出队时移除队头元素。顺序队列的缺点是无法动态扩展,且存在"假溢出"问题。

3、队列的链式存储结构

链式队列

链式队列是队列的链式实现,利用链式存储结构(链表)实现的队列。使用链表实现队列结构,只允许在链表的一头(表头)删除,另一头(表尾)插入。

链式队列的特点
  • 适合于数据元素变动比较大的情形。
  • 不存在队列满且产生溢出的问题。
  • 便于多个队列共享存储空间和提高其效率。
链式队列的结构

链式队列实际上是一个同时有队首指针和队尾指针的单链表:

  • 队首指针指向队头结点。
  • 队尾指针指向队尾结点,即单链表的最后一个结点。
链式队列的操作
  • 入队操作 :建立一个新结点,将新结点插入到链表的尾部,并让 Q.rear 指向这个新插入的结点(若原队列为空队,则令 Q.front 也指向该结点)。
  • 出队操作 :首先判断队是否为空,若不空,则取出队首元素,将其从链表中删除,并让 Q.front 指向下一个结点(若该结点为最后一个结点,则置 Q.frontQ.rear 都为 NULL)。
链式队列的判空
  • 不带头结点时,当 Q.front == NULLQ.rear == NULL 时,链式队列为空。
带头结点的链式队列

通常将链式队列设计成一个带头结点的单链表,这样插入和删除操作就统一了,操作更加方便。

链式队列的优势
  • 用单链表表示的链式队列特别适合于数据元素变动比较大的情形。
  • 假如程序中要使用多个队列,最好使用链式队列,这样就不会出现存储分配不合理和"溢出"的问题。

代码实操

结构定义
c 复制代码
typedef struct LinkQueueNode
{
    ElemType data;                // 队列元素
    struct LinkQueueNode *next;   // 指向下一个结点
} LinkQueueNode;

typedef struct LinkQueue
{
    LinkQueueNode *front;         // 队头指针
    LinkQueueNode *rear;          // 队尾指针
} LinkQueue;
初始化
c 复制代码
void initQueue(LinkQueue *pq)
{
    pq->front = (LinkQueueNode *)malloc(sizeof(LinkQueueNode)); // 申请头结点
    pq->rear = pq->front;                                       // 初始化队尾指针
    pq->rear->next = NULL;                                      // 初始化队尾指针的next为NULL
}
判空
c 复制代码
bool empty(LinkQueue *pq)
{
    return pq->front == pq->rear; // 队头和队尾指针相等时队列为空
}
入队
c 复制代码
void enQueue(LinkQueue *pq, ElemType x)
{
    LinkQueueNode *s = (LinkQueueNode *)malloc(sizeof(LinkQueueNode)); // 申请新结点
    s->data = x;
    s->next = NULL; // 新结点的next指针置为NULL

    pq->rear->next = s; // 将新结点插入到队尾
    pq->rear = s;       // 更新队尾指针
}
出队
c 复制代码
void deQueue(LinkQueue *pq)
{
    if (empty(pq)) // 检查队列是否为空
        return;

    LinkQueueNode *p = pq->front->next; // 获取队头结点
    pq->front->next = p->next;          // 更新队头指针

    if (p == pq->rear) // 如果队列中只有一个元素
        pq->rear = pq->front;           // 更新队尾指针

    free(p); // 释放队头结点
}
取队头元素
c 复制代码
ElemType front(LinkQueue *pq)
{
    return pq->front->next->data; // 返回队头元素
}
打印队列
c 复制代码
void printQueue(LinkQueue *pq)
{
    LinkQueueNode *p = pq->front->next; // 从队头开始遍历
    while (p != NULL)
    {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

链式队列使用链表实现队列,动态分配内存,避免了固定大小的限制。队头和队尾指针分别指向队列的头部和尾部,入队时在队尾插入结点,出队时移除队头结点。

4、循环队列

循环队列

循环队列主要解决顺序队列中的"假溢出"现象,使队列空间能够重复使用。实际上,循环队列仍然是数组,只是将其想象为一个环状的空间。

循环队列的基本概念
  • 将顺序队列臆造为一个环状的空间,即把存储队列元素的表从逻辑上视为一个环,称为循环队列。
  • 当队首指针 Q.front = MaxSize - 1 后,再前进一个位置就自动到 0,这可以利用除法取模运算(%)来实现。
循环队列面临的问题
  1. 如何循环的问题

    • 队首指针进 1: Q . f r o n t = ( Q . f r o n t + 1 ) % M a x S i z e Q.front = (Q.front + 1) \% MaxSize Q.front=(Q.front+1)%MaxSize。
    • 队尾指针进 1: Q . r e a r = ( Q . r e a r + 1 ) % M a x S i z e Q.rear = (Q.rear + 1) \% MaxSize Q.rear=(Q.rear+1)%MaxSize。
    • 队列长度: ( Q . r e a r + M a x S i z e − Q . f r o n t ) % M a x S i z e (Q.rear + MaxSize - Q.front) \% MaxSize (Q.rear+MaxSize−Q.front)%MaxSize。
    • 出入队时:指针都按顺时针方向进 1。
  2. 如何区分空与满的状态

    • 方法一 :牺牲一个单元来区分队空和队满,入队时少用一个队列单元,约定以"队首指针在队尾指针的下一位置作为队满的标志"。
      • 队满条件: ( Q . r e a r + 1 ) % M a x S i z e = = Q . f r o n t (Q.rear + 1) \% MaxSize == Q.front (Q.rear+1)%MaxSize==Q.front。
      • 队空条件: Q . f r o n t = = Q . r e a r Q.front == Q.rear Q.front==Q.rear。
      • 队列中元素的个数: ( Q . r e a r − Q . f r o n t + M a x S i z e ) % M a x S i z e (Q.rear - Q.front + MaxSize) \% MaxSize (Q.rear−Q.front+MaxSize)%MaxSize。
    • 方法二 :类型中增设 size 数据成员,表示元素个数。
      • 若删除成功,则 size 减 1,若插入成功,则 size 加 1,队空时 Q.size == 0;队满时 Q.size == MaxSize,两种情况都有 Q.front == Q.rear
    • 方法三 :类型中增设 tag 数据成员,以区分是队满还是队空。
      • 删除成功置 tag = 0,若导致 Q.front == Q.rear,则为队空;插入成功置 tag = 1,若导致 Q.front == Q.rear,则为队满。
循环队列的判空和判满条件
  • 队空的条件是 Q.front == Q.rear
  • 队满的条件是 (Q.rear + 1)\% MaxSize == Q.front(方法一)。

通过这些方法,循环队列能够有效地解决顺序队列中的"假溢出"问题,并能够更合理地利用存储空间。

代码实操

结构定义
c 复制代码
typedef struct CircleQueue
{
    ElemType *data;       // 动态分配的队列空间
    int front;            // 队头指针
    int rear;             // 队尾指针
    int capacity;         // 队列容量
} CircleQueue;
初始化
c 复制代码
void initQueue(CircleQueue *pq, int sz)
{
    pq->data = (int *)malloc(sizeof(int) * (sz + 1)); // 分配空间,sz+1用于区分队空和队满
    if (pq->data == NULL)
    {
        printf("申请空间失败.\n");
        exit(1);
    }
    pq->capacity = sz + 1;
    pq->front = pq->rear = 0; // 初始化队头和队尾指针
}
判空
c 复制代码
bool empty(CircleQueue *pq)
{
    return pq->front == pq->rear; // 队头和队尾指针相等时队列为空
}
判断队列是否已满
c 复制代码
bool full(CircleQueue *pq)
{
    return (pq->rear + 1) % pq->capacity == pq->front; // 队尾指针的下一个位置是队头时队列已满
}
入队
c 复制代码
void enQueue(CircleQueue *pq, ElemType x)
{
    if (full(pq)) // 检查队列是否已满
    {
        printf("队列空间已满. %d 不能入队.\n", x);
        return;
    }
    pq->data[pq->rear] = x; // 插入数据
    pq->rear = (pq->rear + 1) % pq->capacity; // 队尾指针循环移动
}
出队
c 复制代码
void deQueue(CircleQueue *pq)
{
    if (empty(pq)) // 检查队列是否为空
        return;
    pq->front = (pq->front + 1) % pq->capacity; // 队头指针循环移动
}
打印队列
c 复制代码
void printQueue(CircleQueue *pq)
{
    for (int i = pq->front; i != pq->rear;)
    {
        printf("%d ", pq->data[i]);
        i = (i + 1) % pq->capacity; // 循环遍历队列
    }
    printf("\n");
}

循环队列通过取模操作实现队列的循环特性,解决了顺序队列的"假溢出"问题。通过队头和队尾指针的相对位置判断队列的空满状态。

5、双端队列

双端队列

双端队列是指允许两端都可以进行插入和删除操作的线性表。双端队列两端的地位是平等的,为了方便理解,将左端也视为前端,右端也视为后端。

双端队列的操作
  • 在双端队列入队时,前端进的元素排列在队列中后端进的元素的前面,后端进的元素排列在队列中前端进的元素的后面。
  • 在双端队列出队时,无论是前端还是后端出队,先出的元素排列在后出的元素的前面。
输入受限的双端队列

允许在一端进行插入和删除,但在另一端只允许删除的双端队列称为输入受限的双端队列。若限定双端队列从某个端点插入的元素只能从该端点删除,则该双端队列就蜕变为两个栈底相邻接的栈。

输出受限的双端队列

允许在一端进行插入和删除,但在另一端只允许插入的双端队列称为输出受限的双端队列。

总结
  • 双端队列提供了更灵活的插入和删除操作,适用于需要在两端进行操作的场景。
  • 输入受限和输出受限的双端队列各有其特定的应用场景和操作限制。
  • 实际双端队列的考题通常不会过于复杂,只需判断序列是否满足题设条件,代入验证即可。

6、队列的应用

队列在层次遍历中的应用

应用场景

在信息处理中有一大类问题需要逐层或逐行处理。这类问题的解决方法往往是在处理当前层或当前行时就对下一层或下一行做预处理,把处理顺序安排好,等到当前层或当前行处理完毕,就可以处理下一层或下一行。使用队列是为了保存下一步的处理顺序。

层次遍历过程

下面用二叉树层次遍历的例子,说明队列的应用。

遍历步骤
  1. 根结点入队
  2. 若队空(所有结点都已处理完毕),则结束遍历;否则重复步骤 3 操作。
  3. 队列中第一个结点出队,并访问之。若其有左孩子,则将左孩子入队;若其有右孩子,则将右孩子入队,返回步骤 2。
遍历示例

层次遍历该二叉树的过程:

说明 队内 队外
1 A 入 A
2 A 出,BC 入 BC A
3 B 出,D 入 CD AB
4 C 出,EF 入 DEF ABC
5 D 出,G 入 EFG ABCD
6 E 出,HI 入 FGHI ABCDE
7 F 出 GHI ABCDEF
8 GHI 出 ABCDEFGHI

通过以上步骤,实现了二叉树的层次遍历。

队列在计算机系统中的应用

应用概述

队列在计算机系统中的应用非常广泛,主要解决以下两个问题:

  1. 主机与外部设备之间速度不匹配的问题。
  2. 多用户引起的资源竞争问题。
缓冲区的逻辑结构
问题描述

主机输出数据给打印机打印时,输出数据的速度比打印数据的速度快得多,直接将数据送给打印机打印会导致速度不匹配。

解决方案

设置一个打印数据缓冲区,主机将数据写入缓冲区,写满后暂停输出并转做其他事情。打印机从缓冲区中按先进先出原则依次取出数据打印,完成后再向主机请求数据。

结果

这种方式既保证了打印数据的正确性,又提高了主机的效率。打印数据缓冲区中存储的数据就是一个队列。

多队列出队/入队操作的应用
问题描述

在多终端的计算机系统中,多个用户需要CPU运行各自的程序,通过终端向操作系统提出占用CPU的请求。

解决方案

操作系统按照请求的时间顺序,将请求排成一个队列,每次将CPU分配给队首请求的用户使用。程序运行结束或用完规定时间后,令其出队,再将CPU分配给新的队首请求的用户。

结果

这种方式能够满足每个用户的请求,同时使CPU能够正常运行。

四、参考资料

鲍鱼科技课件

b站免费王道课后题讲解:

网课全程班:

26王道考研书

相关推荐
UP_Continue1 小时前
排序--归并排序
数据结构
飞鼠_2 小时前
详解数据结构之树、二叉树、二叉搜索树详解 C++实现
开发语言·数据结构·c++
傍晚冰川2 小时前
【STM32】最后一刷-江科大Flash闪存-学习笔记
笔记·科技·stm32·单片机·嵌入式硬件·学习·实时音视频
吴梓穆2 小时前
UE5学习笔记 FPS游戏制作33 游戏保存
笔记·学习·ue5
丶Darling.2 小时前
26考研 | 王道 |数据结构 | 第二章 线性表
数据结构·考研
IT19952 小时前
uniapp笔记-自定义分类组件
前端·笔记·uni-app
学也不会2 小时前
d2025331
java·数据结构·算法
Aurora_wmroy3 小时前
算法竞赛备赛——【数据结构】并查集
数据结构·c++·算法·蓝桥杯
pystraf4 小时前
P8310 〈 TREEのOI 2022 Spring 〉Essential Operations Solution
数据结构·c++·算法·线段树·洛谷
·醉挽清风·4 小时前
学习笔记—数据结构—二叉树(链式)
c语言·数据结构·c++·笔记·学习·算法