队列(Queue)
「循环队列 + 链式队列 + 任务调度 Demo」
一、基本概念
队列是一种逻辑结构,是一种特殊的线性表。特殊在:
只能在固定的两端操作线性表
只要满足上述条件,那么这种特殊的线性表就会呈现一种 “先进先出,后进后出” 的逻辑,这种逻辑就被称为队列。
由于约定了只能在线性表固定的两端进行操作,于是给队列这种特殊的线性表的插入删除,起个特殊的名称:
- 队头:可以删除节点(减少数据) 的一端
- 队尾:可以插入节点(添加数据) 的一端
- 入队:将节点插入到队尾之后,函数名通常为
enQueue() - 出队:将队头节点从队列中剔除,函数名通常为
outQueue() - 取队头:取得队头元素,但不出队,函数名通常为
front()
由于这种固定两端操作的简单约定,队列获得了 “先进先出” 的基本特性,如下图所示:
二、顺序存储的队列:循环队列
与其他的逻辑结构类似,队列可以采用顺序存储形成循环队列,也可以采用链式存储形成链式队列。顺序存储的队列之所以被称为循环队列,是因为可以利用更新队头队尾的下标信息,来循环地利用整个数组,出队入队时也不必移动当中的数据。循环队列示意图如下所示:
从上述图中可以观察到,需要牺牲至少数组中的一个存储位置,来区分循环队列中的满队和空队。满队和空队的约定如下:
- 当
front与rear相等时,队列为空 - 当
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;
}
运行结果:

浙公网安备 33010602011771号