队列(Queue)

「循环队列 + 链式队列 + 任务调度 Demo」

一、基本概念

队列是一种逻辑结构,是一种特殊的线性表。特殊在:

只能在固定的两端操作线性表

只要满足上述条件,那么这种特殊的线性表就会呈现一种 “先进先出,后进后出” 的逻辑,这种逻辑就被称为队列。

队列示意图

由于约定了只能在线性表固定的两端进行操作,于是给队列这种特殊的线性表的插入删除,起个特殊的名称:

  • 队头:可以删除节点(减少数据) 的一端
  • 队尾:可以插入节点(添加数据) 的一端
  • 入队:将节点插入到队尾之后,函数名通常为 enQueue()
  • 出队:将队头节点从队列中剔除,函数名通常为 outQueue()
  • 取队头取得队头元素,但不出队,函数名通常为 front()

由于这种固定两端操作的简单约定,队列获得了 “先进先出” 的基本特性,如下图所示:

先进先出示意图

二、顺序存储的队列:循环队列

与其他的逻辑结构类似,队列可以采用顺序存储形成循环队列,也可以采用链式存储形成链式队列。顺序存储的队列之所以被称为循环队列,是因为可以利用更新队头队尾的下标信息,来循环地利用整个数组,出队入队时也不必移动当中的数据。循环队列示意图如下所示:

循环队列示意图

从上述图中可以观察到,需要牺牲至少数组中的一个存储位置,来区分循环队列中的满队和空队。满队和空队的约定如下:

  • frontrear 相等时,队列为空
  • rear 循环加一与 front 相等时,队列为满
循环队列空满判断

与其他数据结构一样,管理循环队列除了需要一块连续的内存之外,还需要记录队列的总容量、当前队列的元素个数、当前队头、队尾元素位置,为了便于管理,通常将这些信息统一于在一个管理结构体之中:

struct seqQueue
{
    datatype *data;  // 循环队列入口
    int capacity;    // 循环队列总容量
    int front;       // 循环队列队头元素下标
    int rear;        // 循环队列队尾元素下标
};

三、 循环队列操作示例

① 设计管理结构体以及数据类型

// 设计一个任务节点
typedef struct Task
{
    void *(*funcPtr)(void *arg);
    void *arg;
} Task_t;

typedef struct QueueCntl
{
    int Size;
    Task_t *Data;
    int F;
    int R;
} Cntl_t, *P_Cntl_t;

③ 初始化空循环队列

P_Cntl_t QueueInit( unsigned int size )
{
    if (size < 2)
    {
        printf("循环队列的最小元素必须大于等于2 ... \n") ;
        return NULL ;
    }
    
    // 申请管理结构体
    P_Cntl_t cntl = calloc( 1, sizeof(Cntl_t) );
    if (cntl == NULL)
    {
        perror("calloc error");
        return NULL ;
    }

    // 初始化管理结构体各项成员
    cntl->Size = size ;
    // 设置管理结构体中的队头队尾下标为0
    // 队头  = 队尾  表示队列为空
    cntl->F = cntl->R = 0 ;

    // 申请顺序表的入口地址
    cntl->Data = calloc( size , sizeof(Task_t) );

    return cntl ;
}

③ 入队

int inQueue( P_Cntl_t cntl , Task_t * task )
{
    // 检测当前队列是否已满
    if ( isFull( cntl ) )
    {
        printf("队列已满..\n");
        return -1 ;
    }
    

    // 添加到队尾下标
    cntl->Data[cntl->R] = * task ;

    // 更新队尾下标
    cntl->R = (cntl->R + 1)%cntl->Size;

    // 返回当前等待的任务数量
    return (cntl->R - cntl->F + cntl->Size)%cntl->Size ;
}

④ 出队

Task_t  outQueue( P_Cntl_t cntl )
{
    // 用于临时存储队头元素
    Task_t tmpTask  = {0};
    // 判断队列是否为空
    if (isEmpty(cntl))
    {
        printf("队列为空..\n");
        return tmpTask ;
    }

    // 把对头元素取出
    tmpTask = cntl->Data[cntl->F];

    // 更新对头下标
    cntl->F = (cntl->F + 1) % cntl->Size ; 

    // 返回队头元素
    return tmpTask ;
}

⑤ 主函数(如何处理队列的元素)

int main(int argc, char const *argv[])
{

    // 循环队列初始化
    P_Cntl_t cntl = QueueInit( 10 );
    

    /////////////////////入队/////////////////////
    // 获得新的数据
    Task_t task1 = { 0 };

    // 任务1 购买咖啡
    int Num = 123 ;
    task1.funcPtr = BuyCoffee;
    task1.arg = &Num ;
    inQueue( cntl , &task1 );

    // 任务2 购买鸡腿
    float f = 456.123 ;
    task1.funcPtr = GetTheChicken ;
    task1.arg = &f ;
    // 把新输入入队
    inQueue( cntl , &task1 );

    // 任务3 购买猪脚饭
    task1.funcPtr = BuyPorkKnuckleRice ;
    task1.arg = "广东打工人的浪漫" ;
    // 把新输入入队
    inQueue( cntl , &task1 );

    // 任务4 学习新知识
    LearningData_t LD = {
        .Time = 2 ,
        .Book = "《从入门到放弃》",
        .Score =  60 
    };
    task1.funcPtr = Learning ;
    task1.arg = &LD;
    inQueue( cntl , &task1 );


    /////////////////////出队/////////////////////
    for (int i = 0; i < 5; i++)
    {
        Task_t OutTask = outQueue( cntl );
        if ( OutTask.funcPtr == NULL )
        {
            printf("没有更多任务了..\n");
            break;
        }

        // 执行出队任务
        OutTask.funcPtr( OutTask.arg );     
    }
    
    return 0;
}

四、链式队列

链式队列的组织形式与链表无异,只不过插入删除被约束在固定的两端。为了便于操作,通常也会创建所谓管理结构体,用来存储队头指针、队尾指针、队列元素个数等信息:

链式队列

从上图可以看到,链式队列主要控制队头和队尾,由于管理结构体中保存了当前队列元素个数size,因此可以不必设计链表的头节点,初始化空队列时只需要让队头队尾指针同时指向空即可。

① 节点设计

// 设计数据的类型
typedef struct 
{
    void * (*ptr) (void *);
    void * arg ;
}Task_t ;


// 节点类型
typedef struct Node
{
    // 数据域
    Task_t Data ;

    // 指针域
    struct Node * Next ;

}Node_t;


// 【也可以尝试设计一个没有管理结构体的链式队列】
// 设计链式队列的管理结构体
typedef struct Cntl
{
    int Count ;
    Node_t * Head ;
    Node_t * Tail ;
    
}Cntl_t;

② 初始化空队列

初始化空队列
Cntl_t * InitQueue( void )
{

    // 申请管理结构体的内存空间
    Cntl_t * cntl = calloc(  1 , sizeof(Cntl_t) );
    if (cntl == NULL)
    {
        perror("calloc Cntl Error");
        return NULL ;
    }

    // 初始化各项成员
    cntl->Count = 0 ;
    cntl->Head = cntl->Tail = NULL ;

    // 返回管理结构体
    return cntl ;

}

③ 入队

入队操作需要考虑当前队列是否为空,如果为空则直接让头尾指针指向新节点即可。

入队操作
// 当队列为空时直接把新节点放入即可
if ( cntl->Count == 0 )
{
    cntl->Head = cntl->Tail =  NewNode ;
    cntl->Count ++ ;
    return cntl->Count ;
}

如果非空则需要把新节点放入到末尾几点的后面。
入队非空操作
// 让末尾节点的后继指针指向新节点
cntl->Tail->Next = NewNode ;

// 让末尾指针指向新节点
cntl->Tail = NewNode ;

// 更新记录任务等待的数量
cntl->Count ++ ;

完整代码

int enQueue( Cntl_t * cntl , Node_t * NewNode )
{
    // 如果不需要添加新节点,则直接返回当前任务数
    if (NewNode == NULL)
    {
        return cntl->Count ;
    }

    // 当队列为空时直接把新节点放入即可
    if ( cntl->Count == 0 )
    {
        cntl->Head = cntl->Tail =  NewNode ;
        cntl->Count ++ ;
        return cntl->Count ;
    }
    
    
    // 让末尾节点的后继指针指向新节点
    cntl->Tail->Next = NewNode ;

    // 让末尾指针指向新节点
    cntl->Tail = NewNode ;

    // 更新记录任务等待的数量
    cntl->Count ++ ;

    return cntl->Count ;
}

④ 出队

出队时,可以直接根据计数值Count判断队列是否为空,如果非空,则把对头元素出队,并跟新对头指针以及计数值即可。

出队操作
Node_t * outQueue( Cntl_t * cntl )
{
    if (cntl->Count == 0)
    {
        printf("队列为空..\n");
        return NULL ;
    }

    Node_t *tmp  = cntl->Head ;
    cntl->Head = tmp->Next ;
    tmp->Next = NULL ;
    cntl->Count -- ;
    
    return tmp;
}

⑤ 主函数

#include <stdio.h>
#include "../inc/myTypes.h"
#include "../inc/Queue.h"
#include <stdlib.h>

void * mountainClimbing( void * arg )
{
    const char * msg =  (const char *) arg ;
    printf("爬山任务[%s]..\n" , msg );
}

void * swimming( void * arg )
{
    int Num =  *(int *) arg ;
    printf("游泳任务[%d]..\n" , Num );
}


void * basketball( void * arg )
{
    float f =  *(float *) arg ;
    printf("篮球任务[%f]..\n" , f );
}
 


int main(int argc, char const *argv[])
{

    // 初始化链式队列
    Cntl_t * cntl = InitQueue( );

    // 获得新数据
    Task_t task = {
        .ptr =  mountainClimbing ,
        .arg = "好累的呀"
    };
    // 获得新节点
    Node_t * NewNode = getNewNode( &task );
    // 把新节点入队
    enQueue( cntl , NewNode );

    task.ptr = swimming ;
    int Num = 123 ;
    task.arg = &Num;
    // 获得新节点
    NewNode = getNewNode( &task );
    // 把新节点入队
    enQueue( cntl , NewNode );


    task.ptr = basketball ;
    float f = 123 ;
    task.arg = &f;
    // 获得新节点
    NewNode = getNewNode( &task );
    // 把新节点入队
    enQueue( cntl , NewNode );

    
    for (int i = 0; i < 4; i++)
    {
        // 出队 获得任务节点
        Node_t * outTask = outQueue( cntl );
        if (outTask == NULL)
        {
            printf("没有任务了..\n");
            break; 
        }
        
        // 执行任务节点中的具体任务
        outTask->Data.ptr( outTask->Data.arg );
        free( outTask );
    }
    

    task.ptr = basketball ;
    f = 444.45 ;
    task.arg = &f;
    // 获得新节点
    NewNode = getNewNode( &task );
    // 把新节点入队
    enQueue( cntl , NewNode );

    // 出队 获得任务节点
    Node_t * outTask = outQueue( cntl );
    // 执行任务节点中的具体任务
    outTask->Data.ptr( outTask->Data.arg );
    free( outTask );

    // 销毁队列
    

    return 0;
}

运行结果:

初始化空队列
posted @ 2025-10-28 08:42  林明杰  阅读(11)  评论(0)    收藏  举报