数据结构中用数组或者链表实现队列

队列简介

队列是一种线性数据结构,遵循“先进先出”(FIFO)原则,即第一个进入队列的元素将第一个被移除。想象一个排队的队伍,排在最前面的人会先被服务。队列广泛应用于打印任务管理、网页服务器请求处理或模拟现实场景,如排队系统。

本文将探讨两种常见的队列实现方式:基于数组的循环队列和基于链表的队列。我们将提供代码示例,详细解释其工作原理,并进行对比分析。


队列操作

在深入实现之前,先定义队列的关键操作:

  • 入队(Enqueue):在队列尾部添加一个元素。

  • 出队(Dequeue):从队列头部移除一个元素。

  • 查看队头(Front):查看队列头部的元素但不移除。

  • 是否为空(IsEmpty):检查队列是否为空。

  • 是否已满(IsFull):检查队列是否已达到最大容量(对于固定大小的队列)。


基于数组的队列:循环队列

当使用数组实现队列时,面临一个挑战:数组大小固定,一旦队尾到达数组末尾,即使开头有空位(因出队而空出),也无法添加更多元素。为解决此问题,我们采用循环队列,将数组视为环形,队尾到达末尾时可绕回开头。

结构

我们定义一个循环队列结构,包括:

  • 一个数组存储元素。

  • 两个整数 frontrear,分别跟踪队头和队尾位置。

  • 数组大小。

在循环队列中,通常留一个空位以区分满和空状态。

初始化
  • frontrear 设置为 0。

是否为空
  • front == rear 时,队列为空。

是否已满
  • (rear + 1) % MAXSIZE == front 时,队列已满。

入队
  • 如果队列已满,返回失败。

  • 否则,将元素放入 data[rear],然后 rear = (rear + 1) % MAXSIZE

出队
  • 如果队列为空,返回失败。

  • 否则,获取 element = data[front],然后 front = (front + 1) % MAXSIZE,返回 element

查看队头
  • 如果队列为空,返回失败。

  • 否则,返回 data[front]

基于数组的队列实现(循环队列)

基于数组的队列使用固定大小的数组存储元素,为了高效利用空间,通常采用循环队列的方式实现。循环队列通过模运算(%)实现数组的“循环”,当队列尾部到达数组末尾时,可以从数组开头继续添加元素。

代码示例

以下是基于数组的循环队列实现

#define MAXSIZE 1024 // 队列的最大容量

typedef struct {
    int data[MAXSIZE]; // 存储元素的数组,最大容量为 MAXSIZE - 1
    int front;         // 队头索引
    int rear;          // 队尾索引
} ArrayQueue;

// 初始化队列
void initArrayQueue(ArrayQueue* q) {
    q->front = 0; // 队头索引初始化为 0
    q->rear = 0;  // 队尾索引初始化为 0
}

// 检查队列是否为空
int isEmpty(ArrayQueue* q) {
    return q->front == q->rear; // 当 front == rear 时,队列为空
}

// 检查队列是否已满
int isFull(ArrayQueue* q) {
    return (q->rear + 1) % MAXSIZE == q->front; // 当 (rear + 1) % MAXSIZE == front 时,队列已满
}

// 入队
int enqueue(ArrayQueue* q, int element) {
    if (isFull(q)) {
        return 0; // 队列已满,返回 0 表示失败
    }
    q->data[q->rear] = element; // 在队尾添加元素
    q->rear = (q->rear + 1) % MAXSIZE; // 更新队尾索引,循环
    return 1; // 成功
}

// 出队
int dequeue(ArrayQueue* q) {
    if (isEmpty(q)) {
        return -1; // 队列为空,返回 -1 表示失败
    }
    int element = q->data[q->front]; // 获取队头元素
    q->front = (q->front + 1) % MAXSIZE; // 更新队头索引,循环
    return element; // 返回出队元素
}

// 查看队头元素
int front(ArrayQueue* q) {
    if (isEmpty(q)) {
        return -1; // 队列为空,返回 -1
    }
    return q->data[q->front]; // 返回队头元素
}
详细解释
  • 为什么使用循环队列?
    循环队列允许队列在数组中“循环”,当队尾(rear)到达数组末尾时,可以从数组开头继续添加元素,从而提高空间利用率。例如,如果数组大小为 5,当 rear = 4 时,(rear + 1) % 5 = 0,即下一个位置是 0。

  • front 和 rear 的含义

    • front 指向队头元素(即将被移除的元素)。

    • rear 指向队尾的下一个位置(因为入队时,我们先放元素再移动 rear)。

  • isEmpty 和 isFull 的判断

    • front == rear 时,队列为空。

    • (rear + 1) % MAXSIZE == front 时,队列已满(留一个空间区分空和满)。这是为了避免空队列和满队列的歧义。

  • % MAXSIZE 的作用
    实现循环,使用模运算使索引在 0 到 MAXSIZE-1 之间循环。例如,如果 MAXSIZE = 5,当 rear = 4 时,(rear + 1) % 5 = 0,即下一个位置是 0。

  • 注意
    由于留一个空间区分空和满,队列的最大容量为 MAXSIZE - 1。例如,如果 MAXSIZE = 1024,队列最多能存储 1023 个元素。

  • 为什么 return -1 表示失败?
    dequeuefront 中,返回 -1 表示队列为空,无法操作。这是 C 语言中常见的约定,因为 -1 不是有效索引,适合表示“失败”。

Circular Queue - GeeksforGeeks 中提到,循环队列通过模运算实现循环,留一个空间区分空满,这与我们的实现一致。

性能
  • 入队和出队时间复杂度:O(1),因为涉及简单数组访问和索引更新。

  • 空间复杂度:O(n),其中 n 为 MAXSIZE,在编译时固定。

优点
  • 实现简单,初学者易懂。

  • 由于连续内存,访问速度快,缓存友好。

  • 适合最大大小已知的场景。

缺点
  • 固定大小限制增长;一旦达到 MAXSIZE - 1,无法添加更多元素(本实现未处理动态调整)。

  • 如果队列未满,可能浪费空间,导致内存效率低。

基于链表的队列实现

基于链表的队列使用节点链表存储元素,维护两个指针:front(队头)和 rear(队尾),以实现高效的入队和出队操作。链表队列动态调整大小,仅受可用内存限制。

代码示例

以下是基于链表的队列实现:

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

// 定义节点结构
typedef struct Node {
    int data;          // 存储数据
    struct Node* next; // 指向下一个节点的指针
} Node;

// 定义队列结构
typedef struct {
    Node* front; // 队头指针
    Node* rear;  // 队尾指针
} LinkedListQueue;

// 初始化队列
void initLinkedListQueue(LinkedListQueue* q) {
    q->front = NULL; // 队头指针初始为 NULL
    q->rear = NULL;  // 队尾指针初始为 NULL
}

// 入队
int enqueue(LinkedListQueue* q, int element) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    if (newNode == NULL) {
        return 0; // 内存分配失败,返回 0
    }
    newNode->data = element; // 设置新节点的数据
    newNode->next = NULL;    // 新节点的 next 为 NULL
    if (q->rear == NULL) {   // 队列为空
        q->front = newNode;  // 队头指向新节点
        q->rear = newNode;   // 队尾指向新节点
    } else {
        q->rear->next = newNode; // 将新节点链接到队尾
        q->rear = newNode;       // 更新队尾指针
    }
    return 1; // 成功
}

// 出队
int dequeue(LinkedListQueue* q) {
    if (q->front == NULL) {
        return -1; // 队列为空,返回 -1
    }
    Node* temp = q->front;     // 保存队头节点
    int element = temp->data;  // 获取队头数据
    q->front = q->front->next; // 更新队头指针
    if (q->front == NULL) {    // 队列变为空
        q->rear = NULL;
    }
    free(temp);                // 释放内存
    return element;            // 返回出队元素
}

// 查看队头元素
int front(LinkedListQueue* q) {
    if (q->front == NULL) {
        return -1; // 队列为空,返回 -1
    }
    return q->front->data; // 返回队头数据
}

// 检查队列是否为空
int isEmpty(LinkedListQueue* q) {
    return q->front == NULL; // 如果队头为 NULL,队列为空
}
详细解释
  • front 和 rear 的含义

    • front 指向队头节点(即将被移除的元素)。

    • rear 指向队尾节点(即将添加新元素的位置)。

  • NULL 的作用

    • 当队列为空时,frontrear 都为 NULL,表示没有节点。

    • 当一个节点的 nextNULL 时,表示这是最后一个节点,链表结束。

  • 两个 -> 的用法

    • 例如在入队时,q->rear->next = newNode 表示通过 rear 指针访问队尾节点的 next 成员,并将其指向新节点。

    • 在出队时,q->front->next 表示通过 front 指针访问队头节点的 next 成员,获取下一个节点的指针。

  • 动态大小
    链表队列没有固定大小限制,仅受可用内存限制。例如,在入队时通过 malloc 动态分配新节点。

  • 为什么 return -1 表示失败?
    dequeuefront 中,返回 -1 表示队列为空,无法操作。这是 C 语言中常见的约定,因为 -1 不是有效索引,适合表示“失败”。

Implement Queue using Linked List - GeeksforGeeks 中提到,链表队列通过 frontrear 实现 O(1) 的入队和出队,这与我们的实现一致。

性能
  • 入队和出队时间复杂度:O(1),因为涉及指针操作和动态内存分配。

  • 空间复杂度:O(n),其中 n 为元素数,内存动态分配。

优点
  • 动态大小:队列可按需增长,仅受可用内存限制,适合未知或可变大小。

  • 无浪费空间:只为当前元素分配内存,提高内存效率。

缺点
  • 由于动态内存分配和指针解引用,比数组稍慢,可能缓存效率低。

  • 需要手动管理内存(如释放节点),若处理不当可能导致内存泄漏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值