C语言数据结构——队列

本文详细介绍了队列的基本概念,包括FIFO原则,以及数组和链表两种常见实现方式。探讨了队列在操作系统、网络通信和异步编程等领域的应用,并通过LeetCode题目展示了如何用队列实现栈。最后强调了队列在数据结构学习中的重要性。
摘要由CSDN通过智能技术生成

目录

0.前言

1.队列的基本概念

2.队列的实现

2.1实现方式

2.2具体实现

3.队列的应用场景

4.一道队列的算法题(LeetCode225. 用队列实现栈)

5.结语


(图像由AI生成) 

0.前言

在计算机科学领域,数据结构是组织和存储数据的一种方式,它允许我们以有效的方式对数据进行访问和修改。今天,我们将探讨一种基础但极其重要的数据结构——队列。通过学习,我们不仅会了解队列的理论基础,还会深入其实现方式,探讨其应用场景,并通过解决一个实际问题来巩固我们的理解。

1.队列的基本概念

队列是一种基础但非常重要的线性数据结构,它在计算机科学和编程中有着广泛的应用。队列遵循先进先出(FIFO, First-In-First-Out)的原则,这意味着最先被加入队列的元素将是最先被移除的。这种特性使得队列非常适合于那些需要按照顺序处理元素的场景。

基本操作

队列的操作通常包括:

  • 入队(Push):在队列的末尾添加一个新的元素。
  • 出队(Pop):移除队列前端的元素。
  • 查看队首(Peek/Front):获取队列前端的元素,但不移除它。
  • 检查队列是否为空(IsEmpty):判断队列中是否有元素。
  • 获取队列大小(Size):获取队列中元素的数量。

特性

  • 有序性:队列保持元素的添加顺序,确保第一个加入的元素将是第一个被移除。
  • 动态性:队列可以动态地增长和缩减,随着元素的入队和出队操作。
  • 操作限制:在队列中,只能在一端(队尾)添加元素,在另一端(队首)移除元素。

2.队列的实现

2.1实现方式

队列可以通过不同的方式实现,其中最常见的两种是:

  1. 数组实现:使用数组存储队列中的元素。这种实现方式简单直观,但可能需要处理数组的动态扩容问题或者实现循环队列来优化空间利用。
  2. 链表实现:使用链表存储队列中的元素。链表实现的队列可以动态地增长,不需要担心空间限制,但每次操作可能涉及更多的内存分配和释放。

综合以上因素以及队列只需要“尾插”和“头删”的特性,我们最终决定用带头单向不循环链表来实现队列。

2.2具体实现

在这一部分,我们将详细介绍如何使用不带头单向不循环链表来实现队列。这种实现方式利用了链表的动态分配特性,允许队列在运行时根据需要增长或缩减。下面是具体的实现方法:

队列的结构定义

首先,我们定义了两个结构体:queueNodequeuequeueNode 结构体表示队列中的节点,包含数据域 _data 和指向下一个节点的指针 _nextqueue 结构体则表示队列本身,包含指向队列头部和尾部的指针 _head_tail

typedef struct queueNode
{
    QDataType _data;           // 节点存储的数据
    struct queueNode* _next;   // 指向下一个节点的指针
} queueNode;

typedef struct queue
{
    queueNode* _head;          // 指向队列头部的指针
    queueNode* _tail;          // 指向队列尾部的指针
} queue;

初始化队列

queueInit 函数用于初始化队列,设置 _head_tail 指针都为 NULL,表示一个空队列。

void queueInit(queue* pq)
{
    assert(pq);
    pq->_head = pq->_tail = NULL;
}

销毁队列

queueDestroy 函数用于销毁队列,释放所有节点占用的内存,并将 _head_tail 指针重置为 NULL

void queueDestroy(queue* pq)
{
    queueNode* cur = pq->_head;
    while (cur)
    {
        queueNode* next = cur->_next;
        free(cur);
        cur = next;
    }
    pq->_head = pq->_tail = NULL;
}

入队操作

queuePush 函数用于在队列尾部添加新节点。如果队列为空,新节点即成为队列的头部和尾部;否则,将新节点链接到原尾节点的 _next 指针,并更新 _tail 指针。

void queuePush(queue* pq, QDataType x)
{
    assert(pq);
    queueNode* newNode = (queueNode*)malloc(sizeof(queueNode));
    if (newNode == NULL)
    {
        printf("malloc fail\n");
        exit(-1);
    }
    newNode->_data = x;
    newNode->_next = NULL;
    if (pq->_head == NULL)
    {
        pq->_head = pq->_tail = newNode;
    }
    else
    {
        pq->_tail->_next = newNode;
        pq->_tail = newNode;
    }
}

出队操作

queuePop 函数用于移除队列头部的节点。如果队列在移除节点后变为空,需要同时将 _tail 指针置为 NULL

void queuePop(queue* pq)
{
    assert(pq && pq->_head);
    queueNode* next = pq->_head->_next;
    free(pq->_head);
    pq->_head = next;
    if (pq->_head == NULL)
    {
        pq->_tail = NULL;
    }
}

另外我们提供了一些基础的队列操作函数,如 queueFrontqueueBack 分别返回队列的头部和尾部元素,queueEmpty 检查队列是否为空,queueSize 返回队列中元素的数量。完整代码如下:

//queue.h
#pragma once
#ifndef QUEUE_H
#define QUEUE_H
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int QDataType;
typedef struct queueNode
{
	QDataType _data;
	struct queueNode* _next;
}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 queueEmpty(queue* pq);//判断队列是否为空,为空返回1,否则返回0
int queueSize(queue* pq);//返回队列中元素的个数

#endif // !QUEUE_H
//queue.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"queue.h"
void queueInit(queue* pq)
{
	assert(pq);
	pq->_head = pq->_tail = NULL;
}

void queueDestroy(queue* pq)
{
	queueNode* cur = pq->_head;
	while (cur)
	{
		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));
	if (newNode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	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 && pq->_head);
	queueNode*next = pq->_head->_next;
	free(pq->_head);
	pq->_head = next;
	if(pq->_head==NULL)
	{
		pq->_tail = NULL;
	}
}

QDataType queueFront(queue* pq)
{
	assert(pq && pq->_head);
	return pq->_head->_data;
}

QDataType queueBack(queue* pq)
{
	assert(pq && pq->_tail);
	return pq->_tail->_data;
}

int queueEmpty(queue* pq)
{
	assert(pq);
	return pq->_head == NULL ? 1 : 0;
}

int queueSize(queue* pq)
{
	assert(pq);
	queueNode* cur = pq->_head;
	int count = 0;
	while (cur)
	{
		count++;
		cur = cur->_next;
	}
	return count;
}

3.队列的应用场景

队列作为一种基本的数据结构,在计算机科学及其相关领域中有着广泛的应用。其先进先出(FIFO)的特性使得队列非常适合于处理需要按顺序执行的任务。以下是队列在不同领域中的一些典型应用场景:(以下查询自网络)

  • 操作系统
  • 在操作系统中,队列被用于多种任务调度和管理过程。例如,CPU调度算法如先来先服务(FCFS)直接使用队列的FIFO特性来管理进程。进程控制块(PCB)根据进程到达的顺序排队等待CPU时间。此外,I/O请求管理也常使用队列,设备请求按照到达顺序排队等待处理。
  • 网络通信
  • 在网络通信中,队列用于管理数据包的传输。数据包在进入网络设备如路由器或交换机时排队等待处理。这种机制帮助控制网络拥塞,确保数据包以合理的顺序被转发。
  • 异步编程
  • 在异步编程模型中,队列用于管理事件或消息。应用程序将事件放入队列中,事件循环依次处理这些事件。这种模型在JavaScript等语言的事件驱动编程中尤为常见。
  • 打印任务管理
  • 在打印任务管理中,打印任务按照提交的顺序排队等待打印。这确保了文档将按照用户提交的顺序被打印,避免了因资源竞争造成的混乱。

4.一道队列的算法题(LeetCode225. 用队列实现栈

学习了栈和队列的知识之后,我们不妨来看一道简单的算法题——用队列实现栈。(链接在上面)

原题大致如下:

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(pushtoppop 和 empty)。

实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶。
  • int pop() 移除并返回栈顶元素。
  • int top() 返回栈顶元素。
  • boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。

注意:

  • 你只能使用队列的基本操作 —— 也就是 push to backpeek/pop from frontsize 和 is empty 这些操作。
  • 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。

以下是用C语言的实现代码。由于C语言库函数没有“队列”,我们只能先“造”出一个队列。

typedef int QueueDataType;

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

// 定义队列结构体
typedef struct Queue
{
    Node* head;                 // 指向队列头节点的指针
    Node* tail;                 // 指向队列尾节点的指针
    size_t size;                // 队列中元素的数量
} Queue;

// 初始化队列
void QueueInit(Queue* q)
{
    // 创建哨兵节点
    q->head = q->tail = (Node*)malloc(sizeof(Node));
    q->head->next = NULL;
    q->size = 0;
}

// 向队列中添加元素
void QueuePush(Queue* q, QueueDataType x)
{
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = x;
    newNode->next = NULL;
    q->tail->next = newNode;
    q->tail = newNode;
    q->size++;
}

// 从队列中移除元素
void QueuePop(Queue* q)
{
    assert(q->head->next != NULL); // 确保队列非空
    Node* toDelete = q->head->next;
    q->head->next = toDelete->next;
    if (q->tail == toDelete) // 如果移除的是尾节点,更新tail指向哨兵节点
    {
        q->tail = q->head;
    }
    free(toDelete);
    q->size--;
}

// 获取队列头部元素
QueueDataType QueueFront(Queue* q)
{
    assert(q->head->next != NULL);
    return q->head->next->data;
}

// 获取队列尾部元素
QueueDataType QueueBack(Queue* q)
{
    assert(q->head->next != NULL);
    return q->tail->data;
}

// 检查队列是否为空
bool QueueEmpty(Queue* q)
{
    return q->head->next == NULL;
}

// 获取队列中元素的数量
size_t QueueSize(Queue* q)
{
    return q->size;
}

// 销毁队列,释放内存
void QueueDestroy(Queue* q)
{
    while (q->head->next != NULL)
    {
        QueuePop(q);
    }
    free(q->head);
    q->head = q->tail = NULL;
}

// 定义栈结构体,内部使用两个队列实现
typedef struct {
    Queue q1;   // 主队列
    Queue q2;   // 辅助队列,用于实现栈的pop和top操作
} MyStack;

// 创建并初始化栈
MyStack* myStackCreate() {
    MyStack* stack = (MyStack*)malloc(sizeof(MyStack));
    QueueInit(&(stack->q1));
    QueueInit(&(stack->q2));
    return stack;
}

// 向栈中添加元素
void myStackPush(MyStack* obj, int x) {
    if(QueueEmpty(&(obj->q2))) // 如果q2为空,向q1中添加元素
    {
        QueuePush(&(obj->q1), x);
    }
    else // 如果q1为空,向q2中添加元素
    {
        QueuePush(&(obj->q2), x);
    }
}

// 从栈中移除元素
int myStackPop(MyStack* obj) {
    if(QueueEmpty(&(obj->q2))) // 如果q2为空,从q1中移动元素到q2,直到最后一个元素
    {
        while(obj->q1.size > 1)
        {
            int num = QueueFront(&(obj->q1));
            QueuePop(&(obj->q1));
            QueuePush(&(obj->q2), num);
        }
        int ret = QueueFront(&(obj->q1));
        QueuePop(&(obj->q1));
        return ret;
    }
    else // 如果q1为空,从q2中移动元素到q1,直到最后一个元素
    {
        while(obj->q2.size > 1)
        {
            int num = QueueFront(&(obj->q2));
            QueuePop(&(obj->q2));
            QueuePush(&(obj->q1), num);
        }
        int ret = QueueFront(&(obj->q2));
        QueuePop(&(obj->q2));
        return ret;
    }
}

// 获取栈顶元素
int myStackTop(MyStack* obj) {
    if(QueueEmpty(&(obj->q2))) // 如果q2为空,栈顶元素在q1的尾部
    {
        return QueueBack(&(obj->q1));
    }
    else // 如果q1为空,栈顶元素在q2的尾部
    {
        return QueueBack(&(obj->q2));
    }
}

// 检查栈是否为空
bool myStackEmpty(MyStack* obj) {
    return QueueEmpty(&(obj->q1)) && QueueEmpty(&(obj->q2));
}

// 销毁栈,释放内存
void myStackFree(MyStack* obj) {
    QueueDestroy(&(obj->q1));
    QueueDestroy(&(obj->q2));
    free(obj);
}

实现原理

该实现策略涉及两个队列,q1q2,它们交替作为主队列和辅助队列。这种方法的核心在于利用队列的先进先出(FIFO)特性来模拟栈的后进先出(LIFO)特性。

  • Push 操作:总是向当前非空的队列中添加新元素。如果两个队列都为空,则可以选择任意一个队列进行添加。这保证了所有元素都存储在同一个队列中,而另一个队列处于空闲状态,待下一次poptop操作时使用。

  • Pop 操作:将主队列中的所有元素(除最后一个元素)依次出队并入队到辅助队列中,然后移除并返回主队列中的最后一个元素。通过这种方式,可以实现栈的LIFO特性。操作完成后,主队列和辅助队列的角色会互换。

  • Top 操作:与pop操作类似,但是不移除最后一个元素,只返回其值。

  • Empty 操作:当两个队列都为空时,栈也为空。

代码解析

在以上代码中,Queue结构体用于实现队列的基本功能,而MyStack结构体则使用两个Queue实例来模拟栈的行为。

  • myStackPush函数向当前活跃的队列中添加元素。
  • myStackPop函数通过将主队列中的元素(除了最后一个)转移到辅助队列中,然后移除并返回最后一个元素,实现了栈的pop操作。此后,队列的角色将互换。
  • myStackTop函数与myStackPop类似,但是在返回最后一个元素的值后,不会将其移除。
  • myStackEmpty函数检查两个队列是否都为空,以确定栈是否为空。
  • myStackFree函数负责释放两个队列和栈实例所占用的内存资源。

5.结语

队列是数据结构中的基石之一,了解其原理和实现方式对于学习更复杂的数据结构和算法至关重要。通过实际编码和解决问题,我们可以加深对队列结构和其应用的理解。希望这篇博客能帮助你在数据结构的学习旅程中迈出坚实的一步。

  • 20
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值