小肥柴慢慢学习数据结构笔记(C篇)(4-1 队列Queue)
目录
4-1 队列概念
如下图,(一般)队列相当于一个管道,元素类似于管道里的小球;规定仅允许从管道的末尾放入小球,仅允许从管道的头部取出小球;这样就自然形成了一个“先进先出的结构”(FIFO),让所有小球在管道中排队。
由于需要存储“一组”数据,队列所以既可以选择使用链表实现,也可以选择使用数组实现;其中链表实现相对简单,因为只需要一个单向链表,保留删除头结点(出队列)和尾插(入队列)两个操作就能复用之前的很多代码了;若使用数组实现则有一个容积问题和回转问题,下面我们分别实现这两套方案的代码。
4-2 基于链表的实现
关键操作:
(1)Enqueue( ) 入队列 :链表尾插
(2)Dequeue( ) 出队列 :删除头结点
上代码:
(1)QueueLink.h
这里我们简单实现了链表中尾插和删头两个操作,也可以复用之前实现的单链表;所有函数名字清晰,不做解释。
typedef int ElementType;
#ifndef _Queue_Link_h
#define _Queue_Link_h
typedef struct Node{
ElementType data;
struct Node *next;
}Node;
struct QueueLink{
Node *Front;
Node *Rear;
};
typedef struct QueueLink *Queue;
int IsEmpty( Queue Q );
Queue CreateQueue( );
void DisposeQueue( Queue Q );
void Enqueue( ElementType X, Queue Q );
ElementType Front( Queue Q );
void Dequeue( Queue Q );
ElementType FrontAndDequeue( Queue Q );
#endif
(2)QueueLink.c
#include <stdio.h>
#include <stdlib.h>
#include "QueueLink.h"
int IsEmpty( Queue Q ){
return Q->Front == NULL;
}
Queue CreateQueue( ){
Queue Q = malloc(sizeof(struct QueueLink));
if( Q == NULL )
printf( "CreateQueue: Out of space!!!\n" );
else
Q->Front = Q->Rear = NULL;
return Q;
}
void Enqueue( ElementType X, Queue Q ){
Node *node = (Node *)malloc(sizeof(Node));
if(node == NULL)
printf( "Enqueue: Out of space!!!\n" );
else{
node->data = X;
node->next = NULL;
if(Q->Rear==NULL){
Q->Front = node;
Q->Rear = node;
} else {
Q->Rear->next = node;
Q->Rear = node;
}
}
}
void Dequeue( Queue Q ){
if( IsEmpty( Q ) )
printf( "Empty queue\n" );
else {
Node * node = Q->Front;
if(Q->Front == Q->Rear)
Q->Front = Q->Rear = NULL;
else
Q->Front = Q->Front->next;
free(node);
}
}
ElementType Front( Queue Q ){
if( !IsEmpty( Q ) )
return Q->Front->data;
printf( "Empty queue\n" );
return 0;
}
ElementType FrontAndDequeue( Queue Q ){
int data = 0;
if( IsEmpty( Q ) )
printf( "Empty queue\n" );
else {
Node *node = Q->Front;
data = node->data;
if(Q->Front == Q->Rear)
Q->Front = Q->Rear = NULL;
else
Q->Front = Q->Front->next;
free(node);
}
return data;
}
void DisposeQueue( Queue Q ){
if(Q != NULL){
Node *cur = Q->Front;
while(cur->next != NULL){
Node *node = cur;
free(node);
cur = cur->next;
}
Q->Front = NULL;
Q->Rear = NULL;
free(Q);
Q = NULL;
}
}
void showQueue(Queue Q){
Node *cur = Q->Front;
while(cur->next != NULL){
printf(" %d", cur->data);
cur = cur->next;
}
printf(" %d\n", cur->data);
}
这里的dequeue( )也可以改为返回被删除的头结点元素,按需实现。
(3)测试文件Main.c
#include <stdio.h>
#include <stdlib.h>
#include "QueueLink.h"
int main(int argc, char *argv[]) {
int i, size = 5;
Queue Q = CreateQueue();
printf( "Enqueue start:\n" );
for(i=0; i<size; i++)
Enqueue(i+1, Q);
printf("\nShow Queue: ");
showQueue(Q);
printf("\nFront Element:%d", Front(Q));
printf("\nShow Queue: ");
showQueue(Q);
printf("\nFrontAndDequeue Element:%d", FrontAndDequeue(Q));
printf("\nShow Queue: ");
showQueue(Q);
printf("\nDequeue:");
Dequeue(Q);
printf("\nShow Queue: ");
showQueue(Q);
DisposeQueue(Q);
return 0;
}
【测试结果】
4-3 基于数组的实现(循环队列)
对于基于数组的队列来讲,有以下几个问题需要明确:(对比ArrayList,其实也可以用ArrayList为蓝本来实现队列)
(1)标记数组中最后一个有效元素的索引为rear,此时如果添加新元素,那么rear++,且将新元素放入已经更新的arr[rear]中;
(2)标记数组中第一个有效元素的索引为front,此时如果删除元素,则front++,也就是“避开”了旧头元素;
(3)假设设定数组的容量是capacity,如果在元素填充满后继续添加元素,那么必然rear出现“回绕”,也就是重新从index=0开始,后续继续添加就是覆盖了。
(4)同理如果rear<front,说明已经回绕了,假设front==size-1,此时出队列front也需要回绕。
我认为初学者并不需要画出环形图来表达以上过程,一个简单的线形图足矣帮助理解问题,特别是对两个基准front和rear的移动,综上分析:
(1)对front和rear两个标记,解决回绕是一个共性问题;
(2)循环队列的核心就是front和rear;
(3)检测队列为空是非常重要的;
(4)每个人对front和rear的实现细节上可能存在差异。
接下来上代码:
(1)QueueArray.h
typedef int ElementType;
#ifndef _Queue_Array_h
#define _Queue_Array_h
struct QueueRecord{
int Capacity;
int Front;
int Rear;
int Size;
ElementType *Array;
};
typedef struct QueueRecord *Queue;
int IsEmpty( Queue Q );
int IsFull( Queue Q );
Queue CreateQueue( int MaxElements );
void DisposeQueue( Queue Q );
void MakeEmpty( Queue Q );
void Enqueue( ElementType X, Queue Q );
ElementType Front( Queue Q );
void Dequeue( Queue Q );
ElementType FrontAndDequeue( Queue Q );
#endif /* _Queue_h */
(2)QueueArray.c
#include <stdio.h>
#include <stdlib.h>
#include "QueueArray.h"
#define MinQueueSize ( 5 )
int IsEmpty( Queue Q ){
return Q->Size == 0;
}
int IsFull( Queue Q ){
return Q->Size == Q->Capacity;
}
Queue CreateQueue( int MaxElements ){
Queue Q = NULL;
if( MaxElements < MinQueueSize ){
printf("Queue size is too small\n");
return NULL;
}
Q = malloc( sizeof( struct QueueRecord ) );
if( Q == NULL ){
printf( "Out of space!!!\n" );
return NULL;
}
Q->Array = malloc( sizeof( ElementType ) * MaxElements );
if( Q->Array == NULL ){
printf( "Out of space!!!\n" );
free(Q);
return NULL;
}
Q->Capacity = MaxElements;
MakeEmpty( Q );
return Q;
}
void MakeEmpty( Queue Q ){
Q->Size = 0;
Q->Front = 1;
Q->Rear = 0;
}
void DisposeQueue( Queue Q ){
if( Q != NULL ){
free( Q->Array );
free( Q );
}
}
static int Succ( int Value, Queue Q ){
if( ++Value == Q->Capacity )
Value = 0;
return Value;
}
void Enqueue( ElementType X, Queue Q ){
if( IsFull( Q ) ){
printf( "Full queue\n" );
} else {
Q->Size++;
Q->Rear = Succ( Q->Rear, Q );
Q->Array[ Q->Rear ] = X;
}
}
ElementType Front( Queue Q ){
if( !IsEmpty( Q ) )
return Q->Array[ Q->Front ];
printf( "Empty queue\n" );
return 0;
}
void Dequeue( Queue Q ){
if( IsEmpty( Q ) )
printf( "Empty queue\n" );
else {
Q->Size--;
Q->Front = Succ( Q->Front, Q );
}
}
ElementType FrontAndDequeue( Queue Q ){
ElementType X = 0;
if( IsEmpty( Q ) )
printf( "Empty queue\n" );
else {
Q->Size--;
X = Q->Array[ Q->Front ];
Q->Front = Succ( Q->Front, Q );
}
return X;
}
void showQueue(Queue Q){
int cur = Q->Front;
while(cur != Q->Rear){
printf(" %d", Q->Array[cur]);
cur = Succ(cur, Q);
}
printf(" %d\n", Q->Array[cur]);
}
有几个细节需要思考:
<1> 对于回绕,我们抽象出一个共性工具函数Succ()
static int Succ( int Value, Queue Q ){
if( ++Value == Q->Capacity )
Value = 0;
return Value;
}
这里入参value其实就是index,如果++之后到达capacity,就应该回绕了(value=0);否则什么都不做。
<2> 初始化函数CreateQueue( )中,使用MakeEmpty( )来初始化front和rear
void MakeEmpty( Queue Q ){
Q->Size = 0;
Q->Front = 1;
Q->Rear = 0;
}
用size来判断empty和full
int IsEmpty( Queue Q ){
return Q->Size == 0;
}
int IsFull( Queue Q ){
return Q->Size == Q->Capacity;
}
而front和rear不参与以上两个函数,这里是一个小trick:原书作者希望使用rear=front-1来判空,以满足某些程序设计人员依靠基准来隐式推算类似队列大小等数据,但实际上我们的模仿黑皮书的代码并不是完美的,市面上还有很多别的形式的实现,特别对于回绕,使用%计算:
1> 首先front=rear=0初始化;
2> 接着使用取模运算实现回转
Q->rear = (Q->rear+1)%MAXSIZE; //入队
Q->front = (Q->front+1)%MAXSIZE; //出队
3)还能使用两个基准计算元素个数,从而不用维护size属性(有优势也有劣势,优势就是节省了一点点空间,劣势就是每次获取size都要重新计算一遍)
//返回长度
int getLenth(LoopQueue *Q) {
return (Q->rear - Q->front + MAXSIZE)%MAXSIZE;
}
有兴趣的同学,可以参考文献[1]重新改写下,万事皆有两面性,建议再看看参考文献[2],也重写一遍;还是那句话:根据具体需要适度改进数据结构。且后续使用数据结构的时候,除非性能压榨,否则是不会改动已经设计好的数据结构的;当然我对黑皮书中这段代码的描述也并不是很满意,如果说不使用%运算是因为其相对效率不高(真的只是相对的,并没有那么不堪),那这种设计还是可以接受的。
(3)Main.c
#include <stdio.h>
#include <stdlib.h>
#include "QueueArray.h"
int main(int argc, char *argv[]) {
int i, size = 5;
Queue Q = CreateQueue(size);
printf( "Enqueue start:\n" );
for(i=0; i<size; i++)
Enqueue(i+1, Q);
printf("\nShow Queue: ");
showQueue(Q);
printf("Front Element:%d\n", Front(Q));
printf("\nShow Queue: ");
showQueue(Q);
printf("FrontAndDequeue Element:%d\n", FrontAndDequeue(Q));
printf("\nShow Queue: ");
showQueue(Q);
printf("Dequeue:\n");
Dequeue(Q);
printf("\nShow Queue: ");
showQueue(Q);
return 0;
}
【测试结果】
4-4 严版教材的实现
1.单链队列
(1)c3-2.h
// c3-2.h 单链队列--队列的链式存储结构
typedef struct QNode
{
QElemType data;
QNode *next;
}*QueuePtr;
struct LinkQueue
{
QueuePtr front,rear; // 队头、队尾指针
};
(2)bo3-2.cpp,相关实现也非常简单
// bo3-2.cpp 链队列(存储结构由c3-2.h定义)的基本操作(9个)
Status InitQueue(LinkQueue &Q)
{ // 构造一个空队列Q
if(!(Q.front=Q.rear=(QueuePtr)malloc(sizeof(QNode))))
exit(OVERFLOW);
Q.front->next=NULL;
return OK;
}
Status DestroyQueue(LinkQueue &Q)
{ // 销毁队列Q(无论空否均可)
while(Q.front)
{
Q.rear=Q.front->next;
free(Q.front);
Q.front=Q.rear;
}
return OK;
}
Status ClearQueue(LinkQueue &Q)
{ // 将Q清为空队列
QueuePtr p,q;
Q.rear=Q.front;
p=Q.front->next;
Q.front->next=NULL;
while(p)
{
q=p;
p=p->next;
free(q);
}
return OK;
}
Status QueueEmpty(LinkQueue Q)
{ // 若Q为空队列,则返回TRUE,否则返回FALSE
if(Q.front==Q.rear)
return TRUE;
else
return FALSE;
}
int QueueLength(LinkQueue Q)
{ // 求队列的长度
int i=0;
QueuePtr p;
p=Q.front;
while(Q.rear!=p)
{
i++;
p=p->next;
}
return i;
}
Status GetHead(LinkQueue Q,QElemType &e)
{ // 若队列不空,则用e返回Q的队头元素,并返回OK,否则返回ERROR
QueuePtr p;
if(Q.front==Q.rear)
return ERROR;
p=Q.front->next;
e=p->data;
return OK;
}
Status EnQueue(LinkQueue &Q,QElemType e)
{ // 插入元素e为Q的新的队尾元素
QueuePtr p;
if(!(p=(QueuePtr)malloc(sizeof(QNode)))) // 存储分配失败
exit(OVERFLOW);
p->data=e;
p->next=NULL;
Q.rear->next=p;
Q.rear=p;
return OK;
}
Status DeQueue(LinkQueue &Q,QElemType &e)
{ // 若队列不空,删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR
QueuePtr p;
if(Q.front==Q.rear)
return ERROR;
p=Q.front->next;
e=p->data;
Q.front->next=p->next;
if(Q.rear==p)
Q.rear=Q.front;
free(p);
return OK;
}
Status QueueTraverse(LinkQueue Q,void(*vi)(QElemType))
{ // 从队头到队尾依次对队列Q中每个元素调用函数vi()。一旦vi失败,则操作失败
QueuePtr p;
p=Q.front->next;
while(p)
{
vi(p->data);
p=p->next;
}
printf("\n");
return OK;
}
2.循环队列
(1)c3-3.h 队列的顺序存储结构(可用于循环队列和非循环队列)
#define MAXQSIZE 5 // 最大队列长度(对于循环队列,最大队列长度要减1)
struct SqQueue
{
QElemType *base; // 初始化的动态分配存储空间
int front; // 头指针,若队列不空,指向队列头元素
int rear; // 尾指针,若队列不空,指向队列尾元素的下一个位置
};
(2)bo3-3.cpp 循环队列,这里多了一个base基址,给后续拓展操作流出尾巴
// bo3-3.cpp 循环队列(存储结构由c3-3.h定义)的基本操作(9个)
Status InitQueue(SqQueue &Q)
{ // 构造一个空队列Q
Q.base=(QElemType *)malloc(MAXQSIZE*sizeof(QElemType));
if(!Q.base) // 存储分配失败
exit(OVERFLOW);
Q.front=Q.rear=0;
return OK;
}
Status DestroyQueue(SqQueue &Q)
{ // 销毁队列Q,Q不再存在
if(Q.base)
free(Q.base);
Q.base=NULL;
Q.front=Q.rear=0;
return OK;
}
Status ClearQueue(SqQueue &Q)
{ // 将Q清为空队列
Q.front=Q.rear=0;
return OK;
}
Status QueueEmpty(SqQueue Q)
{ // 若队列Q为空队列,则返回TRUE,否则返回FALSE
if(Q.front==Q.rear) // 队列空的标志
return TRUE;
else
return FALSE;
}
int QueueLength(SqQueue Q)
{ // 返回Q的元素个数,即队列的长度
return(Q.rear-Q.front+MAXQSIZE)%MAXQSIZE;
}
Status GetHead(SqQueue Q,QElemType &e)
{ // 若队列不空,则用e返回Q的队头元素,并返回OK,否则返回ERROR
if(Q.front==Q.rear) // 队列空
return ERROR;
e=*(Q.base+Q.front);
return OK;
}
Status EnQueue(SqQueue &Q,QElemType e)
{ // 插入元素e为Q的新的队尾元素
if((Q.rear+1)%MAXQSIZE==Q.front) // 队列满
return ERROR;
Q.base[Q.rear]=e;
Q.rear=(Q.rear+1)%MAXQSIZE;
return OK;
}
Status DeQueue(SqQueue &Q,QElemType &e)
{ // 若队列不空,则删除Q的队头元素,用e返回其值,并返回OK;否则返回ERROR
if(Q.front==Q.rear) // 队列空
return ERROR;
e=Q.base[Q.front];
Q.front=(Q.front+1)%MAXQSIZE;
return OK;
}
Status QueueTraverse(SqQueue Q,void(*vi)(QElemType))
{ // 从队头到队尾依次对队列Q中每个元素调用函数vi().一旦vi失败,则操作失败
int i;
i=Q.front;
while(i!=Q.rear)
{
vi(*(Q.base+i));
i=(i+1)%MAXQSIZE;
}
printf("\n");
return OK;
}
(3)顺序队列:这个扩容操作一言难尽
// bo3-4.cpp 顺序队列(非循环,存储结构由c3-3.h定义)的基本操作(9个)
Status InitQueue(SqQueue &Q)
{ // 构造一个空队列Q
Q.base=(QElemType *)malloc(MAXQSIZE*sizeof(QElemType));
if(!Q.base) // 存储分配失败
exit(OVERFLOW);
Q.front=Q.rear=0;
return OK;
}
Status DestroyQueue(SqQueue &Q)
{ // 销毁队列Q,Q不再存在
if(Q.base)
free(Q.base);
Q.base=NULL;
Q.front=Q.rear=0;
return OK;
}
Status ClearQueue(SqQueue &Q)
{ // 将Q清为空队列
Q.front=Q.rear=0;
return OK;
}
Status QueueEmpty(SqQueue Q)
{ // 若队列Q为空队列,则返回TRUE,否则返回FALSE
if(Q.front==Q.rear) // 队列空的标志
return TRUE;
else
return FALSE;
}
int QueueLength(SqQueue Q)
{ // 返回Q的元素个数,即队列的长度
return(Q.rear-Q.front);
}
Status GetHead(SqQueue Q,QElemType &e)
{ // 若队列不空,则用e返回Q的队头元素,并返回OK,否则返回ERROR
if(Q.front==Q.rear) // 队列空
return ERROR;
e=*(Q.base+Q.front);
return OK;
}
Status EnQueue(SqQueue &Q,QElemType e)
{ // 插入元素e为Q的新的队尾元素
if(Q.rear>=MAXQSIZE)
{ // 队列满,增加1个存储单元
Q.base=(QElemType *)realloc(Q.base,(Q.rear+1)*sizeof(QElemType));
if(!Q.base) // 增加单元失败
return ERROR;
}
*(Q.base+Q.rear)=e;
Q.rear++;
return OK;
}
Status DeQueue(SqQueue &Q,QElemType &e)
{ // 若队列不空,则删除Q的队头元素,用e返回其值,并返回OK,否则返回ERROR
if(Q.front==Q.rear) // 队列空
return ERROR;
e=Q.base[Q.front];
Q.front=Q.front+1;
return OK;
}
Status QueueTraverse(SqQueue Q,void(*vi)(QElemType))
{ // 从队头到队尾依次对队列Q中每个元素调用函数vi()。一旦vi失败,则操作失败
int i;
i=Q.front;
while(i!=Q.rear)
{
vi(*(Q.base+i));
i++;
}
printf("\n");
return OK;
}
(4)还有链栈结构,但我觉得这个结构并不是那么的实用,就不贴出来了,其实上面的非循环队列的实现,是没有必要封装的那么复杂的,完全可以用最简单数组和两个基准去实现其核心功能,简单的才是最好的,这也是我看过Linux内核代码以及对《啊哈,算法》这本书作者思想的理解,数据结构本身就是为了更好的实现程序算法而服务的,千万不能陷入为了数据结构和设计数据结构的小路。
4-5 Linux代码中Queue案例
这里的典型例子就是kfifo,网上大多数帖子都已2.6版本为例做代码讲解。具体信息如下(截图,侵删):
相关代码如下:
//根据给定buffer创建一个kfifo
struct kfifo *kfifo_init(unsigned char *buffer, unsigned int size,
gfp_t gfp_mask, spinlock_t *lock);
//给定size分配buffer和kfifo
struct kfifo *kfifo_alloc(unsigned int size, gfp_t gfp_mask,
spinlock_t *lock);
//释放kfifo空间
void kfifo_free(struct kfifo *fifo);
//向kfifo中添加数据
unsigned int kfifo_put(struct kfifo *fifo,
const unsigned char *buffer, unsigned int len);
//从kfifo中取数据
unsigned int kfifo_get(struct kfifo *fifo,
unsigned char *buffer, unsigned int len);
//获取kfifo中有数据的buffer大小
unsigned int kfifo_len(struct kfifo *fifo);
实际到了3.9中:
/**
* kfifo_init - initialize a fifo using a preallocated buffer
* @fifo: the fifo to assign the buffer
* @buffer: the preallocated buffer to be used
* @size: the size of the internal buffer, this have to be a power of 2
*
* This macro initialize a fifo using a preallocated buffer.
*
* The numer of elements will be rounded-up to a power of 2.
* Return 0 if no error, otherwise an error code.
*/
#define kfifo_init(fifo, buffer, size) \
({ \
typeof((fifo) + 1) __tmp = (fifo); \
struct __kfifo *__kfifo = &__tmp->kfifo; \
__is_kfifo_ptr(__tmp) ? \
__kfifo_init(__kfifo, buffer, size, sizeof(*__tmp->type)) : \
-EINVAL; \
})
进一步看细节
int __kfifo_init(struct __kfifo *fifo, void *buffer,
unsigned int size, size_t esize)
{
size /= esize;
size = roundup_pow_of_two(size);
fifo->in = 0;
fifo->out = 0;
fifo->esize = esize;
fifo->data = buffer;
if (size < 2) {
fifo->mask = 0;
return -EINVAL;
}
fifo->mask = size - 1;
return 0;
}
这里的in和out不就是对应rear和front吗?
unsigned int __kfifo_in(struct __kfifo *fifo,
const void *buf, unsigned int len)
{
unsigned int l;
l = kfifo_unused(fifo);
if (len > l)
len = l;
kfifo_copy_in(fifo, buf, len, fifo->in);
fifo->in += len;
return len;
}
其实还有一个常见的例子:Workqueue,基于链表的queue,有兴趣的同学可以自己查看下参考文献[7]/[8]。
小结:使用queue的目的就是让元素(事件/作业/任务)排队等待处理,因为它仅有一个出口和一个入口。
[1] 数据结构(C语言)-循环队列基本操作
[2] 数据结构(C语言)-链队列基本操作
[3] Linux内核中常用的数据结构和算法
[4] 0Linux 内核:匠心独运之无锁环形队列
[5] Linux内核数据结构之kfifo详解
[6] linux内核之Kfifo环形队列
[7] Linux-workqueue讲解(Linux3.9内核)
[8] Linux内核中的Workqueue机制分析