当前位置: 首页 > news >正文

【数据结构】栈和队列——队列

目录

    • 队列
      • 队列的基本概念
      • 顺序队列
        • 顺序队列的定义与初始化
        • 顺序队列的入队/出队
        • 顺序队列的判空/判满
        • 获取队头元素
      • 循环队列
        • 循环队列的概念
        • 循环队列的状态判断
        • 循环队列的定义
        • 循环队列的初始化
        • 循环队列的判空操作
        • 循环队列的判满操作
        • 循环队列的入队操作
        • 循环队列的出队操作
        • 获取队头元素
      • 链式队列
        • 链式队列的概念
        • 链式队列的定义
        • 链式队列的初始化
        • 链式队列的判空操作
        • 链式队列的入队操作
        • 链式队列的出队操作

队列

队列的基本概念

队列的定义:

  • 队列(Queue)是一种受限制的线性表
  • 特点是:先进先出(FIFO, First In First Out)
    • 元素只能在 队尾(rear) 插入。
    • 元素只能在 队头(front) 删除。

可以想象一下:就像你排队买票,先到的人先买,后到的人只能排在后面,不能插队。

队列的基本操作:

  1. 初始化队列:创建一个空队列。
  2. 入队(EnQueue):新元素加入到队尾。
  3. 出队(DeQueue):删除队头的元素,并返回它。
  4. 取队头元素(GetFront):只读取队头的元素,但不删除。
  5. 判空(IsEmpty):判断队列里是否还有元素。
  6. 判满(IsFull)(针对顺序存储的队列):判断队列是否已满。

队列的分类:

  1. 顺序队列:用数组实现,队头队尾用下标表示。
    • 缺点:出队时会产生“假溢出”现象(队头不断右移,空间浪费)。
  2. 链式队列:用链表实现,动态分配内存,不存在“溢出”,更灵活。
  3. 循环队列:对顺序队列的改进,把数组头尾“连成环”,解决假溢出问题。
    • 公式:(rear + 1) % MaxSize == front 表示队满。

**图示理解:**假设队列最多能存放 5 个元素:

初始:front=0, rear=0, 队列空
[ ] [ ] [ ] [ ] [ ]  入队 A:rear=1
[A] [ ] [ ] [ ] [ ]  入队 B:rear=2
[A] [B] [ ] [ ] [ ]  出队(删除 A):front=1
[ ] [B] [ ] [ ] [ ]  入队 C:rear=3
[ ] [B] [C] [ ] [ ]  依次进行……
  1. 为什么顺序存储一定会“溢出”?

    • 顺序存储结构(顺序表、顺序队列、顺序栈…)的特点是:
      👉 用一块 连续的内存空间 来保存数据。

    • 定义时必然有一个 MaxSize(不管是静态数组还是动态 malloc 出来的数组)。

    • 插入/入队/入栈超过 MaxSize 时,就会发生 溢出

    • 即使用 realloc 扩容,本质上还是“再申请更大的一块连续空间 → 搬数据”,最终也受系统内存限制。

  2. 为什么链式存储不会“假溢出”?

    • 链式存储结构(链表、链式队列、链式栈…)的特点是:
      👉 每个结点通过 指针域 连接到下一个结点。

    • 不需要一块连续的内存,理论上只要能 malloc 出一个新结点,就能继续插入。

    • 所以它没有固定的 MaxSize,也不会产生“假溢出”问题。

  3. 系统层面的最终约束

    不管是 顺序存储 还是 链式存储

    • 如果内存耗尽(比如系统没有空闲空间再给你 malloc 结点 / 数组),都会失败。

    • 区别在于:

      • 顺序存储:即使内存够,但你的 MaxSize 限死了容量 → 提前溢出。

      • 链式存储:只要内存够,就能继续扩展,没有人为的 MaxSize

  4. 总结一句话 ✅

    • 顺序存储类(顺序表/队列/栈):因为有 MaxSize,会溢出;

    • 链式存储类(链表/链式队列/链式栈):没有固定上限,只要内存够就能继续;

    • 最终大家都受系统内存限制,内存不足时都会“溢出”。

顺序队列

顺序队列是用 数组 来实现的队列。

特点:

  1. 使用数组连续存储元素
  2. 有两个指标:
    • front:队头位置
    • rear:队尾下一个空位位置

注意:这里 rear 指向的是 下一个可插入元素的位置,不是队尾元素的位置。

  1. 判断队空和队满:
    • 队空:front == rear
    • 队满(顺序队列固定大小):rear == maxsize
  2. 固定大小顺序队列可能会出现“假溢出”:数组还有空位,但由于前面有元素出队,rear到达数组末尾也会认为队满。
顺序队列的定义与初始化

顺序队列的定义:

#define MAXSIZE 100typedef struct {int data[MAXSIZE]; // 存储元素int front;         // 队头int rear;          // 队尾下一个位置
} SqQueue;

这部分定义了一个结构体SqQueue(通常表示 “顺序队列”,Sequential Queue),用于描述队列的存储结构,包含 3 个成员:

  • int data[MAXSIZE]:一个 int 类型的数组,用于实际存储队列中的元素,数组大小由MAXSIZE指定(即最多可存储 100 个 int 元素)。
  • int front:表示 “队头指针”,记录队列中第一个元素的位置(索引)。通过front可以快速获取队头元素。
  • int rear:表示 “队尾指针的下一个位置”,记录队列中最后一个元素的下一个空闲位置(索引)。当有新元素入队时,会将元素存放在rear指向的位置,再更新rear

顺序队列的初始化:

void InitQueue(SqQueue *Q) {Q->front = 0;Q->rear = 0;
}
  • Q->front = 0;:将队列的队头指针 front 初始化为 0(指向数组的起始索引)。
  • Q->rear = 0;:将队列的队尾指针 rear 也初始化为 0(同样指向数组的起始索引)。
顺序队列的入队/出队

入队操作:

int EnQueue(SqQueue *Q, int e) {if(Q->rear == MAXSIZE) // 队满return 0;Q->data[Q->rear] = e;Q->rear++;return 1;
}
  1. 函数定义int EnQueue(SqQueue *Q, int e)
    • 返回值类型为 int:通常用 1 表示入队成功,0 表示入队失败(队列已满)。
    • 参数 SqQueue *Q:指向队列结构体的指针,用于操作目标队列(通过指针修改队列的实际数据)。
    • 参数 int e:要插入队列的元素(int 类型)。
  2. 核心逻辑
    • 第一步(判断队列是否已满):
      if(Q->rear == MAXSIZE) return 0;
      检查队尾指针 rear 是否等于最大容量 MAXSIZE。由于 rear 表示 “队尾元素的下一个位置”,当 rear 达到 MAXSIZE 时,说明数组 data 已被填满(没有空闲位置),此时队列已满,无法插入新元素,返回 0 表示失败。
    • 第二步(插入元素):
      Q->data[Q->rear] = e;
      若队列未满,将元素 e 存入 data 数组中 rear 指向的位置(即当前队尾的下一个空闲位置)。
    • 第三步(更新队尾指针):
      Q->rear++;
      插入元素后,将 rear 指针向后移动一位(指向新的队尾下一个位置),为下一次入队做准备。
    • 第四步(返回成功标识):
      return 1;
      入队操作完成,返回 1 表示成功。

出队操作:

int DeQueue(SqQueue *Q, int *e) {if(Q->front == Q->rear) // 队空return 0;*e = Q->data[Q->front];Q->front++;return 1;
}
  1. 函数定义int DeQueue(SqQueue *Q, int *e)
    • 返回值类型为 int:用 1 表示出队成功,0 表示出队失败(队列已空)。
    • 参数 SqQueue *Q:指向队列结构体的指针,用于操作目标队列。
    • 参数 int *e:一个 int 类型的指针,用于 “传出” 被删除的队头元素(通过指针可以将函数内部的值传递到函数外部)。
  2. 核心逻辑
    • 第一步(判断队列是否为空):
      if(Q->front == Q->rear) return 0;
      检查队头指针 front 是否等于队尾指针 rear。根据队列约定,当两者相等时表示队列为空(没有元素可删除),此时返回 0 表示出队失败。
    • 第二步(获取并传出队头元素):
      *e = Q->data[Q->front];
      若队列非空,将 front 指向的队头元素(data[front])赋值给指针 e 指向的变量(即通过 e 将队头元素 “带出去” 给函数调用者)。
    • 第三步(更新队头指针):
      Q->front++;
      删除队头元素后,将 front 指针向后移动一位(指向新的队头元素),完成出队操作。
    • 第四步(返回成功标识):
      return 1;
      出队操作完成,返回 1 表示成功。
顺序队列的判空/判满

判空操作:

// 判断队空
int QueueEmpty(SqQueue *Q) {return Q->front == Q->rear;
}
  1. 函数定义int QueueEmpty(SqQueue *Q)
    • 返回值类型为 int:在 C 语言中,通常用 1 表示 “真”(队列为空),0 表示 “假”(队列非空)。
    • 参数 SqQueue *Q:指向队列结构体的指针,用于访问队列的 frontrear 指针。
  2. 核心逻辑
    函数体只有一行:return Q->front == Q->rear;
    • 表达式 Q->front == Q->rear 用于判断 “队头指针” 和 “队尾指针” 是否指向同一个位置。
    • 根据队列的约定(结合之前的初始化、入队、出队逻辑):当 frontrear 相等时,队列中没有任何元素(为空);当两者不相等时,队列中至少有一个元素(非空)。
    • 该表达式的结果是一个 “布尔值”:若相等则为 1(真),表示队空;若不相等则为 0(假),表示队列非空。

判满操作:

// 判断队满
int QueueFull(SqQueue *Q) {return Q->rear == MAXSIZE;
}
  1. 函数定义int QueueFull(SqQueue *Q)
    • 返回值类型为 int:在 C 语言中,通常用 1 表示 “真”(队列已满),0 表示 “假”(队列未满)。
    • 参数 SqQueue *Q:指向队列结构体的指针,用于访问队列的 rear 指针和最大容量 MAXSIZE
  2. 核心逻辑
    函数体只有一行:return Q->rear == MAXSIZE;
    • 表达式 Q->rear == MAXSIZE 用于判断 “队尾指针的下一个位置” 是否已达到队列的最大容量(MAXSIZE)。
    • 根据之前的入队逻辑(EnQueue 函数),rear 始终指向队尾元素的下一个空闲位置,且每次入队后 rear 会递增(rear++)。因此,当 rear 等于 MAXSIZE 时,意味着数组 data 的所有位置(0MAXSIZE-1)都已被元素占用,没有空闲空间可供新元素插入 —— 即队列已满。
    • 该表达式的结果为 “布尔值”:若相等则返回 1(真,表示队满);若不相等则返回 0(假,表示队列未满)。
获取队头元素

获取队头操作:

// 获取队头元素
int GetHead(SqQueue *Q, int *e) {if (QueueEmpty(Q)) return 0;*e = Q->data[Q->front];return 1;
}
  1. 函数定义int GetHead(SqQueue *Q, int *e)
    • 返回值类型为 int:用 1 表示获取成功(队列非空),0 表示获取失败(队列为空)。
    • 参数 SqQueue *Q:指向队列结构体的指针,用于访问队列的元素和指针。
    • 参数 int *e:int 类型的指针,用于 “传出” 获取到的队头元素(通过指针将队头元素的值传递给函数外部)。
  2. 核心逻辑
    • 第一步(判断队列是否为空):
      if (QueueEmpty(Q)) return 0;
      调用之前定义的 QueueEmpty 函数判断队列是否为空。若队空(front == rear),则没有队头元素可获取,返回 0 表示失败。
    • 第二步(获取队头元素并传出):
      *e = Q->data[Q->front];
      若队列非空,直接读取 front 指针指向的元素(data[front],即队头元素),并通过指针 e 将其值传递给外部变量。
    • 第三步(返回成功标识):
      return 1;
      成功获取队头元素后,返回 1 表示操作成功。

队列状态:

  • 队空:front == rear
  • 队满(固定大小顺序队列):rear == MAXSIZE

⚠️ 注意:顺序队列在多次入队出队后,前面出队留下的空间不会自动利用,所以容易“假溢出”。

动态顺序队列:

如果用 malloc 动态分配数组,就可以避免固定大小队列的溢出问题:

  1. 当队尾到达数组末尾但队头有空位时,可以 搬移元素扩容数组
  2. 使用循环队列(Circular Queue)可以更高效利用空间

循环队列

顺序队列有个大问题就是会出现 “假溢出” ——明明数组前面有空位置,但 rear 到了末尾就不能再入队了。
👉 为了解决这个问题,就出现了 循环队列(Circular Queue)

循环队列的概念

循环队列就是把顺序队列的存储空间 看成一个环形,利用取模运算 (mod)frontrear 在数组末尾能“绕回”开头,循环使用存储空间。

指针变化:

  • 队头指针 front:指向队头元素
  • 队尾指针 rear:指向 下一个要插入的位置

在这里插入图片描述

虽然循环队列可以看成环形,但实际上仍然是一个顺序结构

循环队列的状态判断

为了避免 front == rear 同时表示队满和队空的问题,通常采用两种方法之一:

  1. 少用一个存储单元(最常用)
    • 队空:front == rear
    • 队满:(rear + 1) % MaxSize == front
      这样最多只能存 MaxSize - 1 个元素。
  2. 增加一个计数器
    • 用一个变量记录队列中元素的数量
    • 队空:size == 0
    • 队满:size == MaxSize
循环队列的定义
#define MAXQSIZE 100  // 最大队列长度typedef int QElemType; // 这里可以根据需要定义成int/char/struct等typedef struct {QElemType *base; // 存储空间的基址(动态分配)int front;       // 队头指针,指向队头元素int rear;        // 队尾指针,指向队尾元素的下一个位置
} SqQueue;
  • QElemType *base
    一个指向 QElemType 类型的指针,用于指向队列元素的存储空间(通常是动态分配的数组)。“基址” 表示这个指针指向整个存储空间的起始位置。
  • int front
    队头指针(此处用整数表示数组索引),指向队列中第一个元素的位置。
  • int rear
    队尾指针(同样用整数表示数组索引),指向队列中最后一个元素的下一个位置(即下一个入队元素将要存放的位置)。
循环队列的初始化
// 初始化队列
int InitQueue(SqQueue *Q) {Q->base = (QElemType *)malloc(MAXQSIZE * sizeof(QElemType));if (!Q->base) exit(0); // 分配失败Q->front = Q->rear = 0;return 1;
}

函数定义:int InitQueue(SqQueue *Q)

  • 函数名 InitQueue:表示 “初始化队列”(Initialize Queue)。
  • 参数 SqQueue *Q:传入队列结构体的指针,用于操作具体的队列实例(通过指针修改原队列的成员)。
  • 返回值 int:通常用 1 表示初始化成功,0 表示失败(此处成功返回 1,失败直接退出程序)。

函数内部逻辑:

  1. 分配存储空间
    Q->base = (QElemType *)malloc(MAXQSIZE * sizeof(QElemType));
    
    • malloc 函数动态分配一块内存,大小为 MAXQSIZE * sizeof(QElemType)
      • MAXQSIZE 是队列最大长度(之前定义的 100);
      • sizeof(QElemType) 是单个元素的字节大小(由 QElemType 类型决定,如 int 占 4 字节)。
    • 分配的内存用于存储队列元素,将其首地址强制转换为 QElemType* 类型后,赋值给队列的 base 成员(即指向存储空间的基址)。
  2. 检查内存分配是否成功
    if (!Q->base) exit(0); // 分配失败
    
    • 如果malloc分配内存失败,会返回 NULL,此时 Q->baseNULL!Q->base 为真)。
    • 遇到这种情况,调用 exit(0) 直接终止程序(实际开发中可能更优雅地返回错误码,此处简化处理)。
  3. 初始化队头和队尾指针
    Q->front = Q->rear = 0;
    
    • 初始化时队列是空的,因此将 front(队头指针)和 rear(队尾指针)都设置为 0(指向数组的起始位置)。
    • 在顺序队列中,front == rear 是 “队列空” 的判断条件。
  4. 返回成功标志
    return 1;
    
    • 所有初始化操作完成后,返回1表示队列初始化成功。
循环队列的判空操作
// 队空
int QueueEmpty(SqQueue Q) {return Q.front == Q.rear;
}

函数定义:int QueueEmpty(SqQueue Q)

  • 函数名 QueueEmpty:表示 “队列是否为空”(Queue Empty)。
  • 参数 SqQueue Q:传入队列结构体的(而非指针),因为判断队列是否为空只需要读取队列的状态,不需要修改队列,所以传值即可。
  • 返回值 int:在 C 语言中,通常用 1(非 0 值)表示 “真”(队列空),0 表示 “假”(队列非空)。

函数逻辑:return Q.front == Q.rear;

这行代码的核心是比较队头指针front和队尾指针rear是否相等

  • Q.front == Q.rear时,表达式结果为1(真),函数返回1,表示队列是空的;
  • Q.front != Q.rear时,表达式结果为0(假),函数返回0,表示队列中存在元素(非空)。

为什么front == rear能判断队空?

这与队列的设计逻辑一致:

  • 初始化队列时(InitQueue 函数),frontrear 被同时设置为 0Q->front = Q->rear = 0),此时队列为空;
  • 当队列中的元素全部出队后,front 会逐渐移动到与 rear 相同的位置,此时队列再次为空。

因此,frontrear 相等是 “队列空” 的标志性条件。

循环队列的判满操作
// 队满
int QueueFull(SqQueue Q) {return (Q.rear + 1) % MAXQSIZE == Q.front;
}

函数定义:int QueueFull(SqQueue Q)

  • 函数名 QueueFull:表示 “队列是否已满”(Queue Full)。
  • 参数 SqQueue Q:传入队列结构体的(无需修改队列,仅读取状态)。
  • 返回值 int:用 1 表示 “队列已满”(真),0表示 “队列未满”(假)。

核心判断逻辑:return (Q.rear + 1) % MAXQSIZE == Q.front;

这行代码的设计是为了避免普通顺序队列的 “假溢出” 问题,我们需要结合 “循环队列” 的思想理解:

  1. 为什么不能用 Q.rear == MAXQSIZE 判断队满?
    普通顺序队列中,如果简单用 “队尾指针 rear 达到最大长度 MAXQSIZE” 判断队满,会出现假溢出
    • 例如:队列初始为空(front=0, rear=0),依次入队元素后rear逐渐增大到MAXQSIZE(此时看似满了),但如果此时有元素出队,front会向后移动(比如front=2),队列前面会出现空位(索引 0、1),但rear已达最大值,无法再入队,导致空间浪费。
  2. 循环队列的解决方案:
    将队列的存储空间视为一个环形结构(通过取模运算实现 “绕回”),此时队满的判断条件设计为:(rear + 1) % MAXQSIZE == front
    含义是:当队尾指针 rear下一个位置(加 1 后),通过取模 MAXQSIZE 绕回环形空间后,与队头指针 front 重合时,说明队列已满。
  3. 取模运算的作用:
    % MAXQSIZE 确保指针在达到数组边界(MAXQSIZE-1)后,能 “绕回” 到起始位置(0),形成环形。例如:
    • MAXQSIZE=100,当 rear=99(最后一个索引)时,rear+1=100100 % 100 = 0,此时若 front=0,则满足队满条件。
  4. 为什么要留一个空位?
    这种判断方式会故意保留一个空闲位置(即rear指向的位置始终为空),目的是区分 “队满” 和 “队空”:
    • 队空条件:front == rear(两者指向同一位置,且该位置无元素);
    • 队满条件:(rear+1) % MAXQSIZE == frontrear 的下一个位置是 front,中间无空闲空间)。

​ 如果不留空位,当队列满时也会出现 front == rear,就无法区分是 “空” 还是 “满” 了。

循环队列的入队操作
int EnQueue(SqQueue *Q, QElemType e) {if (QueueFull(*Q)) return 0; // 队满Q->base[Q->rear] = e;Q->rear = (Q->rear + 1) % MAXQSIZE;return 1;
}

函数定义:int EnQueue(SqQueue *Q, QElemType e)

  • 函数名 EnQueue:表示 “入队”(Enqueue,即元素进入队列)。
  • 参数 SqQueue *Q:队列结构体的指针,用于修改队列的状态(如队尾指针 rear 和存储元素)。
  • 参数 QElemType e:要插入队列的元素(类型由 QElemType 定义,如 int)。
  • 返回值 int1 表示入队成功,0 表示入队失败(队列已满时)。

函数内部逻辑:

  1. 判断队列是否已满
    if (QueueFull(*Q)) return 0; // 队满
    
    • 调用之前定义的 QueueFull 函数判断队列是否已满(传入 *Q 是因为 QueueFull 参数是结构体值)。
    • 如果队列已满,无法插入新元素,直接返回 0 表示入队失败。
  2. 将元素存入队尾
    Q->base[Q->rear] = e;
    
    • 队列未满时,将元素 e 存入 base 数组中 rear 指针指向的位置。
    • 回忆之前的设计:rear 始终指向队尾元素的下一个空闲位置,因此这里直接赋值即可将 e 作为新的队尾元素。
  3. 更新队尾指针
    Q->rear = (Q->rear + 1) % MAXQSIZE;
    
    • 插入元素后,队尾指针需要向后移动一位,指向新的空闲位置。
    • 由于是循环队列,通过 % MAXQSIZE 实现 “绕回” 效果:当 rear 达到最大索引(MAXQSIZE-1)时,加 1 后取模会回到 0,继续使用前面的空闲空间,避免 “假溢出”。
  4. 返回成功标志
    return 1;
    
    • 所有操作完成后,返回 1 表示元素 e 成功入队。
循环队列的出队操作
int DeQueue(SqQueue *Q, QElemType *e) {if (QueueEmpty(*Q)) return 0; // 队空*e = Q->base[Q->front];Q->front = (Q->front + 1) % MAXQSIZE;return 1;
}

函数定义:int DeQueue(SqQueue *Q, QElemType *e)

  • 函数名 DeQueue:表示 “出队”(Dequeue,即元素离开队列)。
  • 参数 SqQueue *Q:队列结构体的指针,用于修改队列的状态(如队头指针 front)。
  • 参数 QElemType *e:指向 QElemType 类型的指针,用于接收出队的元素值(将队头元素 “带出去”)。
  • 返回值 int1 表示出队成功,0 表示出队失败(队列空时)。

函数内部逻辑:

  1. 判断队列是否为空
    if (QueueEmpty(*Q)) return 0; // 队空
    
    • 调用之前定义的 QueueEmpty 函数判断队列是否为空(传入 *Q,因为 QueueEmpty 需要结构体值)。
    • 如果队列为空,没有元素可出队,直接返回 0 表示操作失败。
  2. 取出队头元素并通过指针传出
    *e = Q->base[Q->front];
    
    • 队列非空时,队头指针 front 指向的位置就是队头元素(根据队列设计,front 始终指向队头元素)。
    • base[front](队头元素)赋值给 *e(通过指针 e 将元素值传出函数,供外部使用)。
  3. 更新队头指针
    Q->front = (Q->front + 1) % MAXQSIZE;
    
    • 移除队头元素后,队头指针需要向后移动一位,指向新的队头元素。
    • 同样使用 % MAXQSIZE 实现循环效果:当 front 达到最大索引(MAXQSIZE-1)时,加 1 后取模会绕回 0,继续维护环形结构。
  4. 返回成功标志
    return 1;
    
    • 所有操作完成后,返回 1 表示队头元素成功出队。

严格来说,出队(DeQueue)不会真的把数组里的元素“删除”掉

出队其实只是“逻辑删除”,通过移动 front 指针来忽略这个元素,而不是物理删除。

获取队头元素
int GetHead(SqQueue Q, QElemType *e) {if (QueueEmpty(Q)) return 0;*e = Q.base[Q.front];return 1;
}

函数定义:int GetHead(SqQueue Q, QElemType *e)

  • 函数名 GetHead:表示 “获取队头”(Get Head),即读取队列的第一个元素。
  • 参数 SqQueue Q:传入队列结构体的(仅读取队列状态,无需修改队列,因此传值即可)。
  • 参数 QElemType *e:指向 QElemType 类型的指针,用于接收队头元素的值(将读取到的队头元素 “带出去” 供外部使用)。
  • 返回值 int1 表示获取成功(队列非空),0 表示获取失败(队列空,无队头元素)。

函数内部逻辑:

  1. 判断队列是否为空
    if (QueueEmpty(Q)) return 0;
    
    • 调用 QueueEmpty 函数判断队列是否为空:若为空(front == rear),则没有队头元素可获取,返回 0 表示失败。
  2. 读取队头元素并通过指针传出
    *e = Q.base[Q.front];
    
    • 队列非空时,根据循环队列的设计,front 指针始终指向队头元素,因此直接通过 Q.base[Q.front] 即可访问队头元素。
    • 将读取到的队头元素赋值给 *e(通过指针 e 传出,供函数外部使用)。
  3. 返回成功标志
    return 1;
    
    • 成功读取队头元素后,返回 1 表示操作成功。

核心特点:仅读取,不修改队列

与出队操作 DeQueue 的关键区别在于:

  • GetHead读取队头元素,不会改变队列的结构(front 指针位置不变,队列中的元素数量也不变);
  • DeQueue移除队头元素,并修改 front 指针位置(队列元素数量减少)。

链式队列

链式队列的概念

队列:先进先出(FIFO)。

顺序队列:用数组实现,空间固定。

链式队列:用链表实现,动态分配内存,理论上只要内存够就不会溢出。

链式队列一般用 单链表 来实现,通常带有 头指针(front)尾指针(rear)

  • front:指向队头结点(一般是头结点,方便操作)。
  • rear:指向队尾结点,方便在队尾插入。

链式队列的基本原理

链式队列通过链表的方式存储数据,遵循 “先进先出(FIFO)” 原则:

  • 入队(添加元素):在队尾(rear 指向的结点后)插入新结点,更新 rear 指针。
  • 出队(删除元素):移除队头(front 指向的头结点的下一个结点)元素,更新 front 指针的指向。
链式队列的定义
// 队列结点
typedef struct QNode {int data;struct QNode *next;
} QNode;// 队列(带头结点)
typedef struct {QNode *front; // 队头指针QNode *rear;  // 队尾指针
} LinkQueue;
  • QNode 是队列的基本组成单元(结点),用于存储队列中的单个元素。
  • data:存储具体的数据(此处定义为 int 类型,实际可根据需求修改)。
  • next:是一个指向 QNode 类型的指针,用于连接下一个结点,形成链表结构。
  • LinkQueue 是队列的管理结构,用于整体管理队列(通过队头和队尾指针操作队列)。
  • front:指向队列的头结点(或队头元素,取决于实现细节,带头结点时通常指向头结点)。
  • rear:指向队列的最后一个元素(队尾结点)。
链式队列的初始化
void InitQueue(LinkQueue *Q) {QNode *dummy = (QNode *)malloc(sizeof(QNode)); // 建立头结点dummy->next = NULL;Q->front = Q->rear = dummy;  // front和rear都指向头结点
}

InitQueue(LinkQueue *Q) 的作用是对一个 LinkQueue 类型的队列进行初始化,使其成为一个空队列(但包含头结点),为后续的入队、出队等操作做好准备。

  1. 创建头结点
    QNode *dummy = (QNode *)malloc(sizeof(QNode)); // 建立头结点
    
    • 通过 malloc 动态分配一块 QNode 大小的内存,创建一个头结点(用 dummy 指针临时指向)。
    • 头结点是一个不存储实际数据的结点,仅用于简化队列操作(例如统一空队列和非空队列的处理逻辑)。
  2. 初始化头结点的指针
    dummy->next = NULL;
    
    • 头结点的 next 指针设为 NULL,表示此时头结点后面没有任何元素(队列初始为空)。
  3. 设置队头和队尾指针
    Q->front = Q->rear = dummy;  // front和rear都指向头结点
    
    • 让队列的 front(队头指针)和 rear(队尾指针)都指向刚创建的头结点。
    • 这是 “带头结点的空队列” 的标准状态:此时队列中没有有效数据元素,frontrear 重合且都指向头结点。

初始化完成后,队列处于空队列状态:

  • 存在一个头结点(不存储数据)。
  • frontrear 指针均指向头结点。
  • 头结点的 nextNULL(表示没有后续元素)。
链式队列的判空操作
int QueueEmpty(LinkQueue Q) {return Q.front == Q.rear;
}
  1. 函数定义int QueueEmpty(LinkQueue Q)
    • 函数名QueueEmpty直观表示 “队列是否为空”。
    • 参数LinkQueue Q表示要判断的链式队列(LinkQueue是链式队列的结构体类型,通常包含队头指针front和队尾指针rear)。
    • 返回值类型int:在 C 语言中常用1表示 “真”(空队列),0表示 “假”(非空队列)。
  2. 函数逻辑return Q.front == Q.rear;
    • 核心判断:比较队列的队头指针(front)队尾指针(rear) 是否指向同一个位置。
    • 在链式队列的典型实现中,当队头指针和队尾指针相等时,意味着队列中没有元素(为空);反之则队列非空。

链式队列的“判满”:

  • 顺序队列:因为数组长度固定,所以可以判断 rear == MAXSIZE-1 来判满。
  • 链式队列:用的是 动态内存分配,理论上只要系统有足够的内存就不会满。

所以链式队列通常 不需要判满

链式队列的入队操作
void EnQueue(LinkQueue *Q, int x) {QNode *node = (QNode *)malloc(sizeof(QNode));node->data = x;node->next = NULL;Q->rear->next = node; // 原队尾指向新节点Q->rear = node;       // 更新队尾指针
}
  1. 创建新结点
    QNode *node = (QNode *)malloc(sizeof(QNode));
    
    • malloc 动态分配一块 QNode 大小的内存,创建一个新结点(node 指针指向该结点),用于存储要入队的数据 x
  2. 初始化新结点
    node->data = x;   // 新结点的数据域存入 x
    node->next = NULL; // 新结点的 next 设为 NULL(因为它将成为队尾,后面暂无其他结点)
    
  3. 将新结点链接到队尾
    Q->rear->next = node; // 原队尾结点的 next 指向新结点
    
    • 队列当前的队尾是 Q->rear 指向的结点,通过 Q->rear->next = node,将新结点链接到原队尾结点的后面,使其成为队列的新尾部。
  4. 更新队尾指针
    Q->rear = node;       // 队尾指针 rear 移动到新结点(新结点成为新的队尾)
    

假设队列初始状态为空队列frontrear 都指向头结点):

  1. 第一次入队(如 EnQueue(Q, 10)):
    • 创建存储 10 的新结点 node1
    • 头结点(Q->rear 此时指向头结点)的 next 指向 node1
    • Q->rear 改为指向 node1(现在 node1 是队尾)。
  2. 第二次入队(如 EnQueue(Q, 20)):
    • 创建存储 20 的新结点 node2
    • 当前队尾 node1next 指向 node2
    • Q->rear 改为指向 node2node2 成为新队尾)。

此时队列结构为:
头结点 -> node1(10) -> node2(20)front 仍指头结点,rear 指向 node2

链式队列的出队操作
int DeQueue(LinkQueue *Q, int *x) {if (Q->front == Q->rear) return 0; // 空队列QNode *p = Q->front->next; // 第一个有效结点*x = p->data;Q->front->next = p->next;  // 头结点指向下一个if (Q->rear == p) {        // 若出队的是最后一个结点Q->rear = Q->front;    // rear回到头结点}free(p);return 1;
}
  1. 判断队列是否为空
    if (Q->front == Q->rear) return 0; // 空队列
    
    • 若队头指针 front 和队尾指针 rear 指向同一位置(即空队列),无法出队,返回 0 表示失败。
  2. 定位队头有效元素
    QNode *p = Q->front->next; // 第一个有效结点
    
    • front 指向头结点,头结点的 next 才是队列中第一个存储有效数据的结点(队头元素),用 p 指针临时指向该结点(即要删除的结点)。
  3. 获取出队元素的值
    *x = p->data;
    
    • 将待删除结点 p 中存储的数据存入 x 指向的内存(调用者通过 x 即可获取出队元素)。
  4. 移除队头元素
    Q->front->next = p->next;  // 头结点指向下一个
    
    • 让头结点的 next 跳过 p 结点,直接指向 p 的下一个结点(p->next),相当于将 p 从链表中 “摘除”,此时队列的新队头变为原第二个元素。
  5. 处理 “删除最后一个元素” 的边界情况
    if (Q->rear == p) {        // 若出队的是最后一个结点Q->rear = Q->front;    // rear回到头结点
    }
    
    • p 是队列中最后一个有效元素(即 rear 也指向 p)时,删除 p 后队列变为空队列。
    • 此时需将 rear 重新指向头结点(与 front 保持一致),符合 “空队列时 front == rear” 的规则。
  6. 释放内存并返回成功标志
    free(p);  // 释放被删除结点的内存,避免泄漏
    return 1; // 出队成功
    

假设队列当前状态为:
头结点(front) -> node1(10) -> node2(20) -> node3(30)(rear)

  1. 第一次出队(删除 node1):
    • p 指向 node1,*x 被赋值为 10。
    • 头结点的 next 改为指向 node2。
    • 此时 rear 仍指向 node3(非最后一个元素),无需调整 rear
    • 释放 node1 内存,队列变为:头结点 -> node2(20) -> node3(30)(rear)
  2. 继续出队直到只剩最后一个元素(删除 node2、node3):
    • 删除 node3 时,p 指向 node3,且 rear == p(node3 是最后一个元素)。
    • 头结点的 next 改为 NULL(因为 node3 的 next 是 NULL)。
    • 执行 Q->rear = Q->frontrear 回到头结点。
    • 释放 node3 内存,队列变为空队列(front == rear 都指向头结点)。
http://www.dtcms.com/a/349552.html

相关文章:

  • 向量库Qdrant vs Milvus 系统详细对比
  • 线性回归入门:从原理到实战的完整指南
  • 数据结构——线性表(链表,力扣中等篇,技巧型)
  • Postman 模拟mcp tool调用过程
  • 【数据结构】顺序表详解
  • Flink hop window(滑动窗口)详解
  • leetcode 498. 对角线遍历 中等
  • Linux下的软件编程——网络编程(http)
  • C++14 到 C++20 全面解析:语言新特性、标准库演进与实战案例
  • 【二叉树 - LeetCode】617. 合并二叉树
  • [QMT量化交易小白入门]-八十三、8月因为通信行业,QMT平台ETF轮动策略年化达到了168.56%
  • 降本增效:基于 JavaScript 的 AI 编程 IDE 上下文压缩优化方案
  • CloudBase云开发MCP + CodeBuddy IDE:打造智能化全栈理财助手的完整实践
  • 本地生活新风口:“我店模式”入局正当时??
  • Web程序设计
  • 【前端安全】前端安全第一课:防止 XSS 和 CSRF 攻击的常见手法
  • 新型HTTP走私攻击技术使攻击者可注入恶意请求
  • 从0死磕全栈第1天:从写一个React的hello world开始
  • k8s笔记04-常用部署命令
  • 血缘元数据采集开放标准:OpenLineage Integrations Apache Spark Quickstart with Jupyter
  • SDC命令详解:使用set_timing_derate命令进行约束
  • 基于C语言实现的KV存储引擎(二)
  • ‌重塑培训架构,助力企业人才战略升级‌
  • 【C语言16天强化训练】从基础入门到进阶:Day 10
  • CPLD与FPGA
  • 《Password Guessing Using Large Language Models》——论文阅读
  • 企业级Java项目整合ELK日志收集分析可视化
  • [论文阅读]RQ-RAG: Learning to Refine Queries for Retrieval Augmented Generation
  • 大模型知识--MCP
  • 无人机芯片休眠模式解析