来自队列的知识

332 阅读8分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

队列


本文部分截图来自王道考研

书籍参考:[1]王晓东.数据结构(语言描述)[M].北京:电子工业出版社.2019前言

前言

队列是一种特殊的表,只能在队首进行删除操作,也可以叫做出队操作。同时也只能在队尾进行插入操作,也称为入队操作。所以队列的修改是按先进先出的规则进行的,又称为FIFO表

假设队列为a(1),a(2),....a(n),那么a(1)就是队首元素,a(n)为队尾元素。其中队列中的元素是按a(1),a(2),....a(n)这样的顺序入队,那么出队,也只能是这样的顺序,所以第一个出队的元素就是a(1),最后一个出队的元素是a(n)。

  • QueueEmpty(Q):测试队列Q是否为空
  • QueueFull(Q):测试队列Q是否已满
  • QueueFirst(Q):返回队列Q的队首元素
  • QueueLast(Q):返回队列Q的队尾元素
  • EnterQueue(Q,x):在队列Q的队尾插入元素x
  • DeleteQueue(Q):删除队列Q的队首元素

队列的链式方式

和栈一样,可以用任何一种实现表的方式来用于实现队列,用指针实现一个队列,其实就是一个实现一个单链表,队列结点的类型与单链表结点类型相同

  • 代码实现
队列链式方式的实现
// 队列-链表
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>typedef struct qnode{ // 队列结点结构
    int data; // 队列元素
    struct qnode *next; // 指向下一结点的指针
}Qnode;
​
typedef Qnode *QLink; // 队列结点指针类型typedef struct lque{ // 队列结构
    QLink front; // 队首指针
    QLink rear; // 队尾指针
}Lqueue;
​
typedef Lqueue *Queue; // 队列指针类型
​
QLink NewQnode(){
    /* 产生一个新队列结点 */
    return (QLink)malloc(sizeof(Qnode));
}
​
Queue QueueInit(){
    /* 初始化队列 */
    Queue Q = (Queue)malloc(sizeof(*Q)); // 申请内存
    Q->front = Q->rear = NULL; // 将队首和队尾置为空指针,创建一个队列
    return Q;
}
​
void testQueue(){
    Queue Q;
    Q = QueueInit(Q);
}
​
int main(){
    testQueue();
    return 0;
}

先创建队列结点的指针,方便后面新增结点。接着再创建一个队列类型。队列由队首和队尾构成。初始化队列的时候,将队首和队尾同时指向NULL。这种方式,其实是用了不带头结点的单链表,也可以写成带头节点的单链表形式。先通过队列结点指针,创建一个头节点,然后再将next指向NULL,最后将队首和队尾指针同时指向头节点

Queue QueueInit(Queue Q){
    Q.front = Q.rear = (QLink)malloc(sizeof(Qnode));
    Q.front->next = NULL;
    return Q;
}

Q.fronQ.rear队首和队尾分别指向一个头节点,然后将队首的下一个结点置空,这样就完成了一个带头节点方式的队列

队列链式方式基本操作

判断非空

int QueueEmpty(Queue Q){
    /* 检查是否为空队列 */
    return Q->front == NULL;
}

返回队首元素

int QueueFirst(Queue Q){
    /* 返回队首元素 */
    if (QueueEmpty(Q)){
        exit(1);
    }
    return Q->front->data;
}

返回队尾元素

int QueueLast(Queue Q){
    /* 返回队尾元素 */
    if (QueueEmpty(Q)){
        exit(1);
    }
    return Q->rear->data;
}

入队操作

void EnterQueue(Queue Q, int x){
    /* 入队操作 */
    QLink p = NewQnode(); // 创建一个新结点
    p->data = x;
    p->next = NULL;
    if (QueueEmpty(Q)){
        // 如果是一个空队列,则队首为p
        Q->front = p;
    }else{
        // 如果不是非空队列,这令p为队尾的下一个结点
        Q->rear->next = p;
    }
    // 将队尾变为p
    Q->rear = p;
}
​

这是不带头结点方式的入队操作,那么队列的frontrear此时都是NULL,那么对于第一个结点入队,要有一些特殊的操作。如果队列为空队列,那么令队首为新插入的结点。如果是非空队列,直接让新插入的结点等于队首结点的下一个结点就好了,最后不管是不是第一个结点入队,都需要将队首转移到新结点上。

出队操作

int DeleteQueue(Queue Q){
    /* 出队操作 */
    if (QueueEmpty(Q)){
        // 判断非空
        exit(1);
    }
    int x = Q->front->data;
    Q->front = Q->front->next; // 另队首等于当前队首的下一个结点
    return x;
}

没啥好说的,就是简单的让队首变成当前队首的下一个结点就好了,把当前队首剔除队列,最后再返回一下删除元素。

输出操作

void PrintQueue(Queue Q){
    QLink p = NewQnode();
    p = Q->front; // 另p等于队首
    while (p != NULL){
        // 如果队首不等于NULL
        printf("%d\n",p->data);
        p = p->next; // 将队首一直往下一个结点递进
    }
    free(p);
}

申请一个p结点,令p等于队首,最后让队首层层往后,直到队尾。再这操作中,把每一个经过的结点里面的元素输出出来,就完成了队列整体的输出。

队列的顺序方式

队列是一种特种的表,所以用数组实习表的方法,也同样可以用来实现队列。只是这样效果并不好。每当执行依次出队操作的时候,就空出了一个位置,为了不浪费空间,每次删除都得所有元素往前移动一位,所以就需要O(n)的时间

为了提高效率,可以将数组变成一个圆环的形式,将Q[0]排在Q[maxsize-1],这种意义的数组,称为循环数组。那么这时就碰到了一个问题,怎么样才知道队列有没有满,如果在队列满的情况下还继续插入,那么队首就可能变成了队尾。有两种方法可以实现,一种是设置一个布尔变量来标明队列是空还是满,另外一种则是约定当数组元素达到maxsize-1的时队列为满。

  • 代码实现(下面所有代码,都使用第二种方式)
队列顺序方式的实现
// 队列的顺序存储
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>

typedef struct aque{ // 队列结构
    int maxsize; // 循环数组大小
    int front; // 队首游标
    int rear; // 队尾游标
    int *queue; // 循环数组
}Aque;

typedef Aque *Queue; // 队列指针类型

Queue QueueInit(int size){
    /* 初始化队列 */
    Queue Q = (Queue)malloc(sizeof(*Q)); // 申请队列空间
    Q->queue = (int *)malloc(size*sizeof(int)); // 申请一个size大小的数组空间
    Q->maxsize = size; 
    Q->front = Q->rear = 0; // 将队首队尾游标置为0
    return Q;
}

void testQueue(){
    Queue Q;
    Q = QueueInit(10);
}

int main(){
	testQueue();
    return 0;
}

分配队列一个size大小的循环数组queue,将队首队尾游标置为0,相当于一个空队列。

队列顺序方式基本操作

判断非空

int QueueEmpty(Queue Q){
    /* 判断非空 */
    return Q->front == Q->rear;
}

判断是否为满队列

int QueueFull(Queue Q){
    /* 判断是否为空队列 */
    return (((Q->rear+1)%Q->maxsize==Q->front)?1:0); // 队尾游标加+1,对数组大小进行取模,然后余数和队首游标进行判断
    // 使用了三目运算符,如果为true,返回1,否则返回0
}

因为队首游标是从0开始,所以要进行加1。接着除于数组的大小,如果余数与队首游标相等,证明已经没有剩余的空间了,那么返回数值1,反之返回0。

返回队首元素

int QueueFirst(Queue Q){
    /* 返回队首元素 */
    if (QueueEmpty(Q)){
        exit(1);
    }
    return Q->queue[(Q->front+1)%Q->maxsize]; // 设置队首元素的下标
}

因为front是从0开始,而maxsize的计数又是从1开始。所以front是指向队首元素的前一位,所以这里要+1

返回队尾元素

int QueueLast(Queue Q){
    /* 返回队尾元素 */
    if (QueueEmpty(Q)){
        return Q->queue[Q->rear];
    }
}

入队操作

void EnterQueue(Queue Q, int x){
    if (QueueFull(Q)){
        exit(1);
    }
    Q->rear = (Q->rear+1)%Q->maxsize; // 因为rear是从0开始,而maxsize是从1开始,所以要+1
    Q->queue[Q->rear] = x;
}

因为在开头说过,采用第二种方法,当队列maxszie-1时判断是满队列,所以设置了font和rear都是从0开始,同时maxsize最大值是从1开始计数,这样就实现了,当等于最大值的时候,实际在内存空间中因为下标计算是rear+1,所以还有一个空位,达到了逻辑意义上的满队列。

出队操作

int DeleteQueue(Queue Q){
    /* 出队操作 */
    if (QueueEmpty(Q)){
        exit(1);
    }
    Q->front = (Q->front+1)%Q->maxsize; // 因为front是从0开始,所以+1
    return Q->queue[Q->front]; // 返回出队的元素
}

这里的front+1与入队操作同理,因为是从1开始,所以要进行加一操作,假社一个空队列,执行了入队操作EnterQueue(Q,x),那么此时Q.front=0Q.rear=(0+1)%10=1Q.queue[1]=x。可以看到,第一个元素的下标为1。这个时候,再执行出队操作DeleteQueue(Q),那么就发现了这些流程,首先对队列进行非空判顶,判定通过后,Q.front=(Q.front+1)%Q.maxsizeQ.front=0等于0,经过以上运算后,Q.front变成1,最后将下标为1的元素返回,成功实现了出队操作。