【数据结构】队列(Queue)详解

一、队列的概念

队列(Queue):一种线性表数据结构,是一种只允许在表的一端进行插入操作,而在表的另一端进行删除操作的线性表。

顾名思义,队列模拟了排队现象,即新来的人不断加入队列尾部,而位于队列头部的人逐个离开。我们将队列头部称为 “队首”,尾部称为 “队尾”,将把元素加入队尾的操作称为 “入队”,删除队首元素的操作称为 “出队

请添加图片描述

和栈不同的是,队列是一种 「先进先出(First In First Out)」 的线性表,简称为 「FIFO 结构」

二、队列的类型

和栈类似类似,队列有也同样的两种存储表示方法:「顺序结构」「链式结构」

  • 「顺序存储的队列」:利用一组地址连续的存储单元依次存放队列中从队头到队尾的元素,同时使用指针指向队头元素和队尾元素。
  • 「链式存储的队列」:利用单链表的方式来实现队列。队列中元素按照插入顺序依次插入到链表的第一个节点之后,并使用队头指针指向链表头节点位置,也就是队头元素,尾指针指向链表尾部位置,也就是队尾元素。

这两种结构用哪个实现比较好呢?很明显,队列不适合数组的结构,因为出数据之后就要挪动数据,很麻烦,所以采用链式结构来实现。

而链式结构的队列一般选择链表的头作为队头,尾作为队尾

三、队列的常用操作

  • 入队(QueuePush): 将一个元素加入队尾。

  • 出队(QueuePop): 移除并返回队头元素。

  • 获取队头元素(QueueFront): 返回队头元素但不删除。

  • 获取队尾元素(QueueBack): 返回队尾元素,通常用于确认最后加入的元素。

  • 检查队列是否为空(QueueEmpty): 判断队列中是否还有元素。

  • 获取队列大小(QueueSize): 统计队列中当前元素的个数(一般需要遍历整个链表,时间复杂度为 O(n))。

四、队列的实现

1. 结构体定义和接口函数声明(Queue.h)

//Queue.h
#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>  // 之后会用到的头文件

typedef int QDataType;

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

typedef struct Queue {
	QueueNode* head;  // 头指针,负责出队
	QueueNode* tail;  // 尾指针,负责入队
}Queue;

// 队列初始化
void QueueInit(Queue* pq);
// 销毁队列
void QueueDestroy(Queue* pq);
// 入队
void QueuePush(Queue* pq, QDataType x);
// 出队
void QueuePop(Queue* pq);
// 获取队头元素
QDataType QueueFront(Queue* pq);
// 获取队尾元素
QDataType QueueBack(Queue* pq);
// 获取队列的大小
int QueueSize(Queue* pq);
// 队列的判空
bool QueueEmpty(Queue* pq);

这里我们定义了两个指针。一个指向头,而一个指向尾,这便于我们入队的操作,这样就不用每次都遍历整个链表。

2. 接口函数的实现(Queue.c)

队列的初始化

QueueInit:初始化队列,将队列的 headtail 均置为 NULL,表示队列为空。

void QueueInit(Queue* pq) {
    assert(pq);  // 断言总是用好处的,可以防止别人用函数的时候用错
    pq->head = NULL;
    pq->tail = NULL;
}

队列的销毁

QueueDestroy:遍历队列,从队头开始依次释放每个节点,最后将 headtail 置为 NULL

void QueueDestroy(Queue* pq){
    QueueNode* cur = pq->head;
    while (cur != NULL) {
        QueueNode* next = cur->next;
        free(cur);
        cur = next;
    }
    pq->head = pq->tail = NULL;  // 注意置空
}

入队

QueuePush:首先分配一个新节点,将数据存入其中,并将 next 指针置为 NULL。如果队列为空,则更新 headtail 均指向新节点。如果队列不为空,则将当前队尾的 next 指向新节点,并更新 tail

注:分配新节点的操作可以再单独写一个函数来实现,这里我就没有单独写了。

void QueuePush(Queue* pq, QDataType x){
    assert(pq);
    QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));  // 分配新节点
    newNode->data = x;  // 初始化新节点
    newNode->next = NULL;
    if (pq->head == NULL) {  // 如果队列为空
        pq->head = pq->tail = newNode;
    }
    else {  // 不为空
        pq->tail->next = newNode;
        pq->tail = newNode;
    }
}

出队

QueuePop:移除队头元素,首先检查队列不为空。保存队头节点的下一个节点指针,再来释放当前队头(注意顺序)。更新 head 指向下一个节点。
注意:如果队列中只有一个节点,释放后 head 变为 NULL,此时应将 tail 也置为 NULL,防止产生野指针。

void QueuePop(Queue* pq){
    assert(pq);
    assert(!QueueEmpty(pq));  // 确保队列非空

	QueueNode* next = pq->head->next;
	free(pq->head);
	pq->head = next;
	if (pq->head == NULL) {  // 出队之后检查队列是否为空,为空则置空尾指针
    	pq->tail = NULL;
	}
}

获取队头元素

QueueFront:获取队头元素的数据,但不删除。所以直接返回 head 指针指向的节点数据即可。

QDataType QueueFront(Queue* pq){
    assert(pq);
    assert(!QueueEmpty(pq));  // 确保队列非空

	return pq->head->data;
}

获取队尾元素

QueueBack: 获取队尾元素的数据,不删除。 直接返回 tail 指针指向的节点数据即可。

QDataType QueueBack(Queue* pq){
    assert(pq);
    assert(!QueueEmpty(pq));

	return pq->tail->data;

}

获取队列的大小

QueueSize:返回队列中元素的个数。从 head 开始遍历链表,计数直至链表尾部。

int QueueSize(Queue* pq){
    assert(pq);

	int n = 0;
	QueueNode* cur = pq->head;
	while (cur) {
    	++n;
    	cur = cur->next;
	}
	return n;
}

队列的判空

QueueEmpty: 检查队列是否为空,判断 head 是否为 NULL 即可

bool QueueEmpty(Queue* pq){
    assert(pq);
    return pq->head == NULL;
}

完整代码

#include "Queue.h"

void QueueInit(Queue* pq) {
	assert(pq);
	pq->head = NULL;
	pq->tail = NULL;
}

void QueueDestroy(Queue* pq){
	QueueNode* cur = pq->head;
	while (cur != NULL) {
		QueueNode* next = cur->next;
		free(cur);
		cur = next;
	}
	pq->head = pq->tail = NULL;
}

void QueuePush(Queue* pq, QDataType x){
	assert(pq);
	QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
	newNode->data = x;
	newNode->next = NULL;
	if (pq->head == NULL) {
		pq->head = pq->tail = newNode;
	}
	else {
		pq->tail->next = newNode;
		pq->tail = newNode;
	}
}

void QueuePop(Queue* pq){
	assert(pq);
	assert(!QueueEmpty(pq));

	QueueNode* next = pq->head->next;
	free(pq->head);
	pq->head = next;
	//注意:当只有一个节点的时候,free 完了之后没有对 tail 进行处理就会变成野指针
	if (pq->head == NULL) {
		pq->tail = NULL;
	}
}

QDataType QueueFront(Queue* pq){
	assert(pq);
	assert(!QueueEmpty(pq));

	return pq->head->data;
}

QDataType QueueBack(Queue* pq){
	assert(pq);
	assert(!QueueEmpty(pq));

	return pq->tail->data;
}

int QueueSize(Queue* pq){
	assert(pq);

	int n = 0;
	QueueNode* cur = pq->head;
	while (cur) {
		++n;
		cur = cur->next;
	}
	return n;
}

bool QueueEmpty(Queue* pq){
	assert(pq);
	return pq->head == NULL;
}

3. 测试代码示例(test.c)

以下测试代码展示了如何使用上述接口对队列进行操作:

#include "Queue.h"

void TestQueue1() {
    Queue q;
    QueueInit(&q);
    QueuePush(&q, 1);
    QueuePush(&q, 2);
    QueuePush(&q, 3);
    QueuePush(&q, 4);

    printf("%d\n", QueueFront(&q));
    printf("%d\n", QueueBack(&q));

    while (!QueueEmpty(&q)) {
        QDataType front = QueueFront(&q);
        printf("%d ", front);
        QueuePop(&q);
    }
    printf("\n");
}

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

运行结果如下:

请添加图片描述

六、结语

本文介绍了链式队列的基本原理与常用操作,同时详细解析了每个接口函数的设计与实现思路。希望这篇文章能帮助大家初步理解队列数据结构及其实现细节。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值