MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C 语言实现队列深入讲解

2022-05-206.0k 阅读

队列的基本概念

队列(Queue)是一种特殊的线性数据结构,它按照先进先出(First In First Out,FIFO)的原则存储数据,先进入的数据被先读取出来。可以将队列想象成日常生活中的排队场景,比如在银行排队办理业务,先到的人先接受服务,后到的人在队尾依次等待。在计算机科学中,队列常用于处理需要按顺序处理的数据,例如任务调度、打印队列等。

队列的操作

  1. 入队(Enqueue):将一个新元素添加到队列的末尾。这就好比在排队时,新到来的人站到队伍的最后面。
  2. 出队(Dequeue):从队列的头部移除一个元素。类似排队时,排在最前面的人办理完业务离开队伍。
  3. 获取队头元素(Front):查看队列头部的元素,但并不移除它。如同在排队时,我们可以看到排在最前面的人是谁,但他还没有离开队伍。
  4. 获取队尾元素(Rear):查看队列尾部的元素。类似于看到队伍的最后一个人。
  5. 判断队列是否为空(IsEmpty):检查队列中是否有元素。如果队列为空,就像排队的队伍没有人一样。
  6. 获取队列长度(Size):返回队列中元素的个数。等同于队伍中的人数。

C 语言实现队列的方式

在 C 语言中,我们可以使用数组或链表来实现队列。下面我们将分别介绍这两种实现方式。

使用数组实现队列

使用数组实现队列是一种较为简单直观的方法。我们定义一个数组来存储队列中的元素,并使用两个变量来记录队列的头部和尾部位置。

代码示例

#include <stdio.h>
#include <stdlib.h>

#define MAX_SIZE 100

typedef struct {
    int data[MAX_SIZE];
    int front;
    int rear;
} Queue;

// 初始化队列
void initQueue(Queue *q) {
    q->front = 0;
    q->rear = 0;
}

// 判断队列是否为空
int isQueueEmpty(Queue *q) {
    return q->front == q->rear;
}

// 判断队列是否已满
int isQueueFull(Queue *q) {
    return (q->rear + 1) % MAX_SIZE == q->front;
}

// 入队操作
void enqueue(Queue *q, int value) {
    if (isQueueFull(q)) {
        printf("队列已满,无法入队\n");
        return;
    }
    q->data[q->rear] = value;
    q->rear = (q->rear + 1) % MAX_SIZE;
}

// 出队操作
int dequeue(Queue *q) {
    if (isQueueEmpty(q)) {
        printf("队列为空,无法出队\n");
        exit(1);
    }
    int value = q->data[q->front];
    q->front = (q->front + 1) % MAX_SIZE;
    return value;
}

// 获取队头元素
int getFront(Queue *q) {
    if (isQueueEmpty(q)) {
        printf("队列为空,无法获取队头元素\n");
        exit(1);
    }
    return q->data[q->front];
}

// 获取队尾元素
int getRear(Queue *q) {
    if (isQueueEmpty(q)) {
        printf("队列为空,无法获取队尾元素\n");
        exit(1);
    }
    return q->data[(q->rear - 1 + MAX_SIZE) % MAX_SIZE];
}

// 获取队列长度
int getQueueSize(Queue *q) {
    return (q->rear - q->front + MAX_SIZE) % MAX_SIZE;
}

int main() {
    Queue q;
    initQueue(&q);

    enqueue(&q, 10);
    enqueue(&q, 20);
    enqueue(&q, 30);

    printf("队列长度: %d\n", getQueueSize(&q));
    printf("队头元素: %d\n", getFront(&q));
    printf("队尾元素: %d\n", getRear(&q));

    printf("出队元素: %d\n", dequeue(&q));
    printf("队列长度: %d\n", getQueueSize(&q));

    return 0;
}

代码解析

  1. 结构体定义:我们定义了一个 Queue 结构体,其中包含一个 data 数组用于存储队列元素,frontrear 变量分别表示队列的头部和尾部位置。
  2. 初始化队列initQueue 函数将 frontrear 初始化为 0。
  3. 判断队列是否为空isQueueEmpty 函数通过判断 frontrear 是否相等来确定队列是否为空。
  4. 判断队列是否已满isQueueFull 函数使用取模运算来判断队列是否已满,因为我们使用循环队列来避免数组越界。
  5. 入队操作enqueue 函数将新元素添加到 rear 位置,然后更新 rear。如果队列已满,给出提示并返回。
  6. 出队操作dequeue 函数返回 front 位置的元素,然后更新 front。如果队列为空,输出错误信息并退出程序。
  7. 获取队头和队尾元素getFrontgetRear 函数分别返回队头和队尾元素,同样在队列为空时给出错误提示。
  8. 获取队列长度getQueueSize 函数通过 rearfront 的差值并结合取模运算来计算队列长度。

使用链表实现队列

使用链表实现队列可以避免数组实现中可能出现的固定大小限制。链表中的每个节点存储一个元素,并且节点之间通过指针连接。

代码示例

#include <stdio.h>
#include <stdlib.h>

typedef struct QueueNode {
    int data;
    struct QueueNode *next;
} QueueNode;

typedef struct {
    QueueNode *front;
    QueueNode *rear;
} Queue;

// 初始化队列
void initQueue(Queue *q) {
    q->front = NULL;
    q->rear = NULL;
}

// 判断队列是否为空
int isQueueEmpty(Queue *q) {
    return q->front == NULL;
}

// 入队操作
void enqueue(Queue *q, int value) {
    QueueNode *newNode = (QueueNode *)malloc(sizeof(QueueNode));
    newNode->data = value;
    newNode->next = NULL;

    if (isQueueEmpty(q)) {
        q->front = newNode;
        q->rear = newNode;
    } else {
        q->rear->next = newNode;
        q->rear = newNode;
    }
}

// 出队操作
int dequeue(Queue *q) {
    if (isQueueEmpty(q)) {
        printf("队列为空,无法出队\n");
        exit(1);
    }
    QueueNode *temp = q->front;
    int value = temp->data;
    q->front = q->front->next;
    free(temp);
    return value;
}

// 获取队头元素
int getFront(Queue *q) {
    if (isQueueEmpty(q)) {
        printf("队列为空,无法获取队头元素\n");
        exit(1);
    }
    return q->front->data;
}

// 获取队尾元素
int getRear(Queue *q) {
    if (isQueueEmpty(q)) {
        printf("队列为空,无法获取队尾元素\n");
        exit(1);
    }
    return q->rear->data;
}

// 获取队列长度
int getQueueSize(Queue *q) {
    int count = 0;
    QueueNode *current = q->front;
    while (current != NULL) {
        count++;
        current = current->next;
    }
    return count;
}

int main() {
    Queue q;
    initQueue(&q);

    enqueue(&q, 10);
    enqueue(&q, 20);
    enqueue(&q, 30);

    printf("队列长度: %d\n", getQueueSize(&q));
    printf("队头元素: %d\n", getFront(&q));
    printf("队尾元素: %d\n", getRear(&q));

    printf("出队元素: %d\n", dequeue(&q));
    printf("队列长度: %d\n", getQueueSize(&q));

    return 0;
}

代码解析

  1. 结构体定义:我们定义了两个结构体,QueueNode 用于表示链表中的节点,包含数据和指向下一个节点的指针;Queue 结构体包含 frontrear 指针,分别指向队列的头部和尾部节点。
  2. 初始化队列initQueue 函数将 frontrear 初始化为 NULL
  3. 判断队列是否为空isQueueEmpty 函数通过判断 front 是否为 NULL 来确定队列是否为空。
  4. 入队操作enqueue 函数创建一个新节点,将其数据设置为传入的值,并将其 next 指针设置为 NULL。如果队列为空,将 frontrear 都指向新节点;否则,将新节点添加到 rear 节点的后面,并更新 rear
  5. 出队操作dequeue 函数保存 front 节点的数据,然后将 front 指针移动到下一个节点,并释放原来的 front 节点。如果队列为空,输出错误信息并退出程序。
  6. 获取队头和队尾元素getFrontgetRear 函数分别返回 frontrear 节点的数据,同样在队列为空时给出错误提示。
  7. 获取队列长度getQueueSize 函数通过遍历链表并计数节点的方式来获取队列长度。

循环队列的优化

在使用数组实现队列时,普通的线性队列可能会出现假溢出的问题,即数组的前端有空闲空间,但后端已满,导致无法继续入队。为了解决这个问题,我们可以使用循环队列。

循环队列的原理

循环队列通过将数组视为一个环形结构,使得 rear 指针在到达数组末尾时可以回到数组开头。当 rear 到达数组末尾时,如果 front 前面有空闲空间,rear 可以从数组开头继续存储元素。

循环队列的实现细节

  1. 初始化:与普通数组队列类似,初始化 frontrear 为 0。
  2. 入队:在入队时,计算新的 rear 位置使用取模运算 (rear + 1) % MAX_SIZE。如果新的 rear 等于 front,则表示队列已满。
  3. 出队:出队时,计算新的 front 位置同样使用取模运算 (front + 1) % MAX_SIZE

代码改进

#include <stdio.h>
#include <stdlib.h>

#define MAX_SIZE 100

typedef struct {
    int data[MAX_SIZE];
    int front;
    int rear;
} Queue;

// 初始化队列
void initQueue(Queue *q) {
    q->front = 0;
    q->rear = 0;
}

// 判断队列是否为空
int isQueueEmpty(Queue *q) {
    return q->front == q->rear;
}

// 判断队列是否已满
int isQueueFull(Queue *q) {
    return (q->rear + 1) % MAX_SIZE == q->front;
}

// 入队操作
void enqueue(Queue *q, int value) {
    if (isQueueFull(q)) {
        printf("队列已满,无法入队\n");
        return;
    }
    q->data[q->rear] = value;
    q->rear = (q->rear + 1) % MAX_SIZE;
}

// 出队操作
int dequeue(Queue *q) {
    if (isQueueEmpty(q)) {
        printf("队列为空,无法出队\n");
        exit(1);
    }
    int value = q->data[q->front];
    q->front = (q->front + 1) % MAX_SIZE;
    return value;
}

// 获取队头元素
int getFront(Queue *q) {
    if (isQueueEmpty(q)) {
        printf("队列为空,无法获取队头元素\n");
        exit(1);
    }
    return q->data[q->front];
}

// 获取队尾元素
int getRear(Queue *q) {
    if (isQueueEmpty(q)) {
        printf("队列为空,无法获取队尾元素\n");
        exit(1);
    }
    return q->data[(q->rear - 1 + MAX_SIZE) % MAX_SIZE];
}

// 获取队列长度
int getQueueSize(Queue *q) {
    return (q->rear - q->front + MAX_SIZE) % MAX_SIZE;
}

int main() {
    Queue q;
    initQueue(&q);

    enqueue(&q, 10);
    enqueue(&q, 20);
    enqueue(&q, 30);

    printf("队列长度: %d\n", getQueueSize(&q));
    printf("队头元素: %d\n", getFront(&q));
    printf("队尾元素: %d\n", getRear(&q));

    printf("出队元素: %d\n", dequeue(&q));
    printf("队列长度: %d\n", getQueueSize(&q));

    return 0;
}

通过上述代码,我们可以看到循环队列的实现避免了假溢出问题,提高了数组空间的利用率。

队列在实际应用中的场景

  1. 任务调度:在操作系统中,任务调度器通常使用队列来管理等待执行的任务。新的任务被添加到队列的末尾,而调度器从队列头部取出任务并执行,确保任务按照提交的顺序依次处理。
  2. 打印队列:在打印机的工作过程中,打印任务会被放入一个队列中。打印机按照队列的顺序依次打印文档,避免多个任务同时争抢打印机资源,保证打印工作的有序进行。
  3. 广度优先搜索(BFS):在图论算法中,广度优先搜索算法使用队列来遍历图的节点。从起始节点开始,将其邻居节点依次加入队列,然后从队列中取出节点并继续扩展其邻居,从而实现广度优先的遍历。
  4. 消息队列:在分布式系统中,消息队列用于在不同的组件之间传递消息。生产者将消息发送到队列,消费者从队列中取出消息进行处理,实现异步通信和解耦系统组件。

队列实现中的常见问题与解决方法

  1. 内存管理问题(链表实现):在链表实现队列时,需要注意内存的分配和释放。每次入队时要确保正确分配内存给新节点,而出队时要及时释放已移除节点的内存,避免内存泄漏。
  2. 数组越界问题(数组实现):使用数组实现队列时,要特别注意数组索引的边界检查。循环队列通过取模运算来处理边界,但仍然需要正确判断队列是否已满和为空,防止数组越界访问。
  3. 性能问题:对于频繁的入队和出队操作,链表实现可能在内存分配和释放上有一定开销,而数组实现可能受限于固定大小。在实际应用中,需要根据具体需求和数据规模选择合适的实现方式,或者对实现进行优化,例如使用动态数组来解决数组固定大小的问题。

通过以上对 C 语言实现队列的深入讲解,包括基本概念、数组和链表两种实现方式、循环队列的优化、实际应用场景以及常见问题解决方法,希望读者对队列这一数据结构在 C 语言中的实现和应用有更全面深入的理解,能够在实际编程中灵活运用队列来解决各种问题。