嵌入式 - 数据结构与算法:(1-9)数据结构 - 队列(Queue)

上一篇 下一篇

目 录


队列(Queue)

核心思想:先进先出(FIFO, First In First Out) ------ 排队买票,先来先服务!

1)队列的定义与特点

  • 定义: 队列是一种 先进先出(FIFO, First In First Out)的线性表 ,只允许在一端插入(入队,Enqueue) ,在另一端删除(出队,Dequeue),允许插入的一端称为队尾(rear),允许删除的一端则称为队头(front)。

  • 特点:

    • 入队:从 队尾 添加元素
    • 出队:从 队头 移除元素
    • 不允许中间插入或随机访问
  • 描述:

    复制代码
    入队 → [ C ][ B ][ A ] → 出队
            ↑          ↑
          队尾(rear)  队头(front)
  • 举例:

    假设有队列 q = ( a 1 , a 2 , . . , a n ) q=(a_1,a_2,..,a_n) q=(a1,a2,..,an) ,那么 a 1 a_1 a1 就是队头元素, a n a_n an 就是队尾元素。队列中的元素是按照 a 1 , a 2 , . . , a n a_1,a_2,..,a_n a1,a2,..,an 的顺序进入的,退出队列也只能按照这个次序依次退出,也就是说,只有在 a 1 , a 2 , . . . , a n − 1 a_1,a_2,...,a_{n-1} a1,a2,...,an−1 都离开队列之后, a n a_n an 才能退出队列。

2)队列的两种实现方式

对比项 顺序队列(数组) 链队列(链表)
内存分配 连续、需预设容量 动态、按需分配
空间效率 可能浪费(假溢出) 无浪费
时间复杂度 Enqueue/Dequeue: O(1) Enqueue/Dequeue: O(1)
实现难度 中等(需处理循环) 简单(天然动态)

3)普通顺序队列(存在"假溢出"问题)

缺点:空间利用率低,可能提前报"满"

3.1)基础结构设计

c 复制代码
#define MAX_SIZE 100  // 固定容量

typedef int QDataType;

typedef struct SeqQueue {
    QDataType data[MAX_SIZE];	// 定义一个大数组
    int front;  // 队头下标(指向第一个元素)
    int rear;   // 队尾下标(指向最后一个元素的下一个位置)
} SeqQueue;

3.2)假溢出问题

在使用数组实现的普通顺序队列中,会出现一种看似"队列已满",但实际上数组前面还有空闲空间的情况。这种"满"并不是真正的空间耗尽,而是由于队列的线性结构和操作方式导致的逻辑浪费 ,因此称为 "假溢出"(False Overflow)。示例如下:

复制代码
初始:front = 0, rear = 0 → 空队
入队 A,B,C:front=0, rear=3 → [A][B][C][ ][ ]
出队 A,B:front=2, rear=3 → [ ][ ][C][ ][ ]
再入队 D,E,F:front=2, rear=6 → [ ][ ][C][D][E][F]
此时 rear == MAX_SIZE,无法再入队!
但前面还有 2 个空位 → 假溢出

简单说就是:队头的数据后来出队了,但是由于只能从队尾入队,所以空出来的队头就没法再被利用到。

3.3)核心操作(有缺陷)

  • 初始化

    c 复制代码
    void QueueInit(SeqQueue* q) {
        q->front = q->rear = 0;
    }
  • 入队(不检查假溢出)

    c 复制代码
    bool QueuePush(SeqQueue* q, QDataType x) {
        if (q->rear >= MAX_SIZE) {
            return false; // 队满(可能是假溢出!)
        }
        q->data[q->rear++] = x;
        return true;
    }
  • 判断队列是否为空

    c 复制代码
    if (q->front == q->rear) 
    {
    	return false; // 队空
    }

    初始化时,front 和 rear 的值相等;后续入队出队时,当所有元素都出队了时,front 又会遇到 rear。所以可以通过看这两个的值是不是相等来判断队列是否为空。

  • 判断队列是否已满

    c 复制代码
    if (q->rear == MAX_SIZE) 
    {
    	return false; // 已满
    }
  • 出队

    c 复制代码
    /* 出队:成功返回 true 并将元素存入 *out_val;失败返回 false */
    bool QueuePop(SeqQueue* q, QDataType* out_val) {
        if (q == NULL || out_val == NULL) {
            return false; // 防御性检查
        }
        
        // 判断队列是否为空
        if (q->front == q->rear) {
            return false; // 队空,无法出队
        }
        
        // 取出队头元素
        *out_val = q->data[q->front];
        q->front++; // 队头指针后移
        
        return true;
    }

4)循环队列(解决假溢出)

有几个重要的计算公式

改进思路:

  • 将数组视为环形结构
  • 使用 模运算 % MAX_SIZE 实现"首尾相连",队尾用 (rear + 1) % MAX_SIZE 表示(队尾永远指向 rear 后面一个)。

判空与判满,为区分"空"和"满",会牺牲一个存储单元 (容量 = MAX_SIZE - 1,最后空一个会显示已满):

状态 条件
队空 front == rear
队满 (rear + 1) % MAX_SIZE == front(假设 MAX_SIZE=8,front=0,rear=7,则为满)

图解:

结构(同普通顺序队列):

c 复制代码
#define MAX_SIZE 100

typedef struct CircularQueue {
    QDataType data[MAX_SIZE];
    int front;  // 队头下标
    int rear;   // 队尾下标(指向下一个空位)![3](图片/3.png)
} CircularQueue;

4.1)核心操作(正确实现)

初始化:

c 复制代码
void QueueInit(CircularQueue* q) {
    q->front = q->rear = 0;
}

入队:

c 复制代码
bool QueuePush(CircularQueue* q, QDataType x) {
    if ((q->rear + 1) % MAX_SIZE == q->front) {
        return false; // 队满
    }
    q->data[q->rear] = x;
    q->rear = (q->rear + 1) % MAX_SIZE;		// !!!!!!!!!!!!
    return true;
}

出队:

c 复制代码
bool QueuePop(CircularQueue* q) {
    if (q->front == q->rear) {
        return false; // 队空
    }
    q->front = (q->front + 1) % MAX_SIZE;	// !!!!!!!!!!!!
    return true;
}

获取队头/队尾:

c 复制代码
QDataType QueueFront(CircularQueue* q) {
    return q->data[q->front];
}

QDataType QueueBack(CircularQueue* q) {
    // rear 指向下一个空位,所以队尾是 (rear - 1 + MAX_SIZE) % MAX_SIZE
    return q->data[(q->rear - 1 + MAX_SIZE) % MAX_SIZE];	// !!!!!!!!!!!!
}

获取大小:

c 复制代码
int QueueSize(CircularQueue* q) {
    return (q->rear - q->front + MAX_SIZE) % MAX_SIZE;	// !!!!!!!!!!!!
}

5)链队列(动态实现,推荐)

链队列的头和尾是自己定的:可以用首元节点充当队头,用尾节点当队尾;也可以反过来。

结构设计:

c 复制代码
/* 定义队列节点 */
typedef struct QueueNode {
    QDataType data;				// 数据域
    struct QueueNode* next;		// 指针域
} QueueNode;

/* 定义队列链表 */
typedef struct LinkQueue {
    QueueNode* head; // 队头(出队端)
    QueueNode* tail; // 队尾(入队端)
    int size;
} LinkQueue;

💡 保留 tail 指针使入队操作 O(1)

具体的常用操作见下面的完整代码实现。

6)C语言完整代码实现(循环队列/链队列)

6.1)循环队列

circular_queue.h

c 复制代码
#ifndef CIRCULAR_QUEUE_H
#define CIRCULAR_QUEUE_H

#include <stdbool.h>

#define MAX_QSIZE 100  // 最大容量为 MAX_QSIZE - 1

typedef int QDataType;

typedef struct CircularQueue {
    QDataType data[MAX_QSIZE];
    int front;
    int rear;
} CircularQueue;

void QueueInit(CircularQueue* q);
void QueueDestroy(CircularQueue* q); // 循环队列无需 destroy,留空
bool QueuePush(CircularQueue* q, QDataType x);
bool QueuePop(CircularQueue* q);
QDataType QueueFront(CircularQueue* q);
QDataType QueueBack(CircularQueue* q);
bool QueueEmpty(CircularQueue* q);
int QueueSize(CircularQueue* q);

#endif

circular_queue.c

c 复制代码
#include "circular_queue.h"

void QueueInit(CircularQueue* q) {
    q->front = q->rear = 0;
}

void QueueDestroy(CircularQueue* q) {
    // 顺序结构,无需释放
}

bool QueuePush(CircularQueue* q, QDataType x) {
    if ((q->rear + 1) % MAX_QSIZE == q->front) {
        return false; // 队满
    }
    q->data[q->rear] = x;
    q->rear = (q->rear + 1) % MAX_QSIZE;
    return true;
}

bool QueuePop(CircularQueue* q) {
    if (q->front == q->rear) {
        return false; // 队空
    }
    q->front = (q->front + 1) % MAX_QSIZE;
    return true;
}

QDataType QueueFront(CircularQueue* q) {
    return q->data[q->front];
}

QDataType QueueBack(CircularQueue* q) {
    return q->data[(q->rear - 1 + MAX_QSIZE) % MAX_QSIZE];
}

bool QueueEmpty(CircularQueue* q) {
    return q->front == q->rear;
}

int QueueSize(CircularQueue* q) {
    return (q->rear - q->front + MAX_QSIZE) % MAX_QSIZE;
}

6.2)链队列

link_queue.h

c 复制代码
#ifndef LINK_QUEUE_H
#define LINK_QUEUE_H

#include <stdbool.h>

typedef int QDataType;

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

typedef struct LinkQueue {
    QueueNode* head;
    QueueNode* tail;
    int size;
} LinkQueue;

void QueueInit(LinkQueue* q);
void QueueDestroy(LinkQueue* q);
void QueuePush(LinkQueue* q, QDataType x);
void QueuePop(LinkQueue* q);
QDataType QueueFront(LinkQueue* q);
QDataType QueueBack(LinkQueue* q);
bool QueueEmpty(LinkQueue* q);
int QueueSize(LinkQueue* q);

#endif

link_queue.c

c 复制代码
#include "link_queue.h"
#include <stdlib.h>
#include <stdio.h>


/**
 * @brief 初始化一个空的链式队列(不包含哨兵头节点)
 * @param q 指向待初始化队列的指针(必须非 NULL)
 * 
 * 将队列的头指针(head)和尾指针(tail)都置为 NULL,
 * 表示队列中没有任何节点;同时将 size 置为 0。
 */
void QueueInit(LinkQueue* q) {
    q->head = q->tail = NULL;  // 头尾指针均为空,表示空队列
    q->size = 0;               // 队列元素个数初始化为 0
}


/**
 * @brief 销毁队列,释放所有节点占用的内存
 * @param q 指向待销毁队列的指针
 * 
 * 通过不断调用 QueuePop() 逐个删除队列中的节点,
 * 直到队列变空。这样可以确保所有 malloc 分配的内存都被 free。
 */
void QueueDestroy(LinkQueue* q) {
    // 循环直到队列为空
    while (!QueueEmpty(q)) {
        QueuePop(q);  // 每次弹出队头节点并释放其内存
    }
    // 此时 head 和 tail 已为 NULL,size 为 0,队列完全清空
}


/**
 * @brief 入队操作:将元素 x 添加到队列尾部
 * @param q 指向队列的指针
 * @param x 要入队的数据(类型为 QDataType)
 * 
 * 创建一个新节点,将其链接到当前 tail 节点之后,
 * 并更新 tail 指针。若队列原本为空,则 head 和 tail 都指向新节点。
 */
void QueuePush(LinkQueue* q, QDataType x) {
    // 动态分配一个新队列节点
    QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
    // 如果内存分配失败,程序异常终止(实际项目中可改为返回错误码)
    if (!newNode) exit(-1);
    
    newNode->data = x;      // 存储数据
    newNode->next = NULL;   // 新节点是队尾,next 指向 NULL

    // 判断队列是否为空
    if (q->tail == NULL) {
        // 队列为空:新节点既是头也是尾
        q->head = q->tail = newNode;
    } else {
        // 队列非空:将新节点接在原 tail 之后,并更新 tail
        q->tail->next = newNode;
        q->tail = newNode;
    }
    q->size++;  // 队列元素数量加 1
}


/**
 * @brief 出队操作:移除并释放队头元素
 * @param q 指向队列的指针
 * 
 * 注意:此实现不返回被移除的值,仅完成删除。
 * 若队列为空,打印错误信息并终止程序。
 */
void QueuePop(LinkQueue* q) {
    // 检查队列是否为空
    if (QueueEmpty(q)) {
        fprintf(stderr, "Error: Pop from empty queue!\n");
        exit(-1);  // 异常退出,避免非法访问
    }
    
    // 保存待删除的队头节点
    QueueNode* del = q->head;
    // 将 head 指向下一个节点(可能变为 NULL)
    q->head = q->head->next;
    
    // 特殊情况:如果删除后队列变空,需同步更新 tail 为 NULL
    if (q->head == NULL) {
        q->tail = NULL;
    }
    
    // 释放原队头节点的内存
    free(del);
    q->size--;  // 队列元素数量减 1
}


/**
 * @brief 获取队头元素的值(不删除)
 * @param q 指向队列的指针
 * @return 队头元素的数据
 * 
 * 若队列为空,直接终止程序(无返回错误机制)。
 */
QDataType QueueFront(LinkQueue* q) {
    if (QueueEmpty(q)) exit(-1);  // 空队列不能取队头
    return q->head->data;         // 返回头节点存储的数据
}


/**
 * @brief 获取队尾元素的值(不删除)
 * @param q 指向队列的指针
 * @return 队尾元素的数据
 * 
 * 由于维护了 tail 指针,可 O(1) 直接访问队尾。
 */
QDataType QueueBack(LinkQueue* q) {
    if (QueueEmpty(q)) exit(-1);  // 空队列不能取队尾
    return q->tail->data;         // 返回尾节点存储的数据
}


/**
 * @brief 判断队列是否为空
 * @param q 指向队列的指针
 * @return true 表示空,false 表示非空
 * 
 * 以 head 是否为 NULL 作为判断依据(也可用 tail 或 size)。
 */
bool QueueEmpty(LinkQueue* q) {
    return q->head == NULL;
}


/**
 * @brief 获取队列当前元素个数
 * @param q 指向队列的指针
 * @return 队列中元素的数量
 * 
 * 由于维护了 size 成员,可 O(1) 直接返回,无需遍历。
 */
int QueueSize(LinkQueue* q) {
    return q->size;
}

6.3)测试主程序 main.c(共用)

c 复制代码
#include <stdio.h>
#include "circular_queue.h"   // 或 #include "link_queue.h"
// 注意:两者不能同时包含(类型名冲突),测试时选其一

int main() {
    // 若测试循环队列:
    CircularQueue q;
    // 若测试链队列:
    // LinkQueue q;
    
    QueueInit(&q);
    
    printf("=== 队列测试 ===\n");
    printf("初始: 空=%s, 大小=%d\n", 
           QueueEmpty(&q) ? "是" : "否", QueueSize(&q));
    
    // 入队
    printf("\n--- 入队 ---\n");
    for (int i = 1; i <= 5; i++) {
        if (QueuePush(&q, i * 10)) {
            printf("入队 %d, 队头=%d, 队尾=%d, 大小=%d\n",
                   i * 10, QueueFront(&q), QueueBack(&q), QueueSize(&q));
        } else {
            printf("队列已满,无法入队 %d\n", i * 10);
        }
    }
    
    // 出队
    printf("\n--- 出队 ---\n");
    while (!QueueEmpty(&q)) {
        printf("出队 %d, 剩余大小=%d\n", QueueFront(&q), QueueSize(&q));
        QueuePop(&q);
    }
    
    QueueDestroy(&q);
    return 0;
}

7)如何选择队列类别

场景 推荐实现
元素数量大致已知,追求速度 循环队列(无 malloc/free 开销)
元素数量不确定,避免溢出 链队列(动态扩展)
内存受限(如嵌入式) 循环队列(固定内存)
学习数据结构原理 两者都实现一遍!
  • 普通顺序队列:有缺陷,仅用于理解概念
  • 循环队列 :顺序实现的正确姿势
  • 链队列 :动态实现的简洁方案

实际开发中,链队列更常用(除非对性能极度敏感)


相关推荐
,,?!,8 小时前
数据结构算法-排序算法
数据结构·算法·排序算法
程序leo源9 小时前
C语言知识总结
c语言·开发语言·c++·经验分享·笔记·青少年编程·c#
‎ദ്ദിᵔ.˛.ᵔ₎9 小时前
C++哈希表
数据结构·c++·散列表
爱编码的小八嘎10 小时前
C语言完美演绎9-30
c语言
爱编码的小八嘎10 小时前
C语言完美演绎9-28
c语言
阿旭超级学得完10 小时前
C++11(初始化)
java·开发语言·数据结构·c++·算法
云淡风轻~窗明几净10 小时前
关于角谷猜想的五行小猜想
数据结构·算法
Languorous.11 小时前
C++数据结构进阶|并查集(Union-Find)详解:从原理到面试实战
数据结构·c++·面试
Languorous.11 小时前
C++数据结构进阶|堆(Heap)详解:从手写实现到面试高频实战
数据结构·c++·面试
笨笨饿11 小时前
80_聊聊SPI以及它们的变体
linux·c语言·网络·stm32·单片机·算法·个人开发