队列(Queue)

概念

队列(Queue)是只允许在一端进行插入工作,而在另一端进行删除操作的线性表。队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头

队列的抽象数据类型:

ADT 队列(Queue)
Data
   同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
   InitQueue(*Q):初始化操作,建立一个空队列Q。
   DestoryQueue(*Q):若队列Q存在,则销毁它。
   ClearQueue(*Q):将队列Q清空。
   QueueEmpty(Q):若队列为空,则返回true,否则返回false。
   GetHead(Q,*e):若队列Q存在且非空,用e来返回队列Q的队头元素。
   EnQueue(*Q,e):若队列Q存在,插入新元素e到队列Q中成为队尾元素。
   DeQueue(*Q,*e):删除队列Q的队头元素,并用e返回其值。
   QueueLength(Q):返回队列Q的元素个数。

队列的顺序存储

假设队列有n个元素,则顺序存储的队列需建立一个大于n的数组,并将n个元素存储在数组的前n个单元中,数组下标为0的一端为队头。所谓的插入队列操作,其实是在队尾追加一个元素,不需要移动任何元素,时间复杂度为 O(1) 。和栈不同,队列的出列操作在队头进行,即下标为0的位置,即队列中的所有元素都要向前移动,保证队列的队头在下标为0的位置且不为空,此时的时间复杂度为 O(n)

仔细想想,为何出队列是一定要全部移动呢?若不限制队列的元素不需要存储在数组的前n个单元,即队头不需再下标为0的位置,这样能提高队列出列的性能。

因此,引入两个指针,front指针指向队头元素,rear指针指向队尾指针元素的下移位置,这样当front等于rear时,该队列为空队列。

假设这个队列的总数不超过5个,但是因为数组末尾元素已经占用,再向后加,就会产生数组越界的错误,可实际上,队列在数组前面的位置还是空闲的,这种情况就是”假溢出”现象。

循环队列

要解决”假溢出”问题的方法就是后面满了,从头再开始,即头尾相接的循环,队列的这种头尾相接的顺序存储结构被称为循环队列。以上述为例,数组末尾元素占用后,将尾指针改为指向下标为0的位置,继续插入。

又有问题来了,空队列时,front==rear,而队列满时,还是front==rear,那如何判断队列是空还是满呢?

第一种方法,设置标志变量,当front==rear且flag==0时队列为空,当front==rear且flag==1时队列为满;

第二种方法,当front==rear时,队列为空,当队列满是,修改其条件,保留一个元素空间(当队列满是,数组中还有一个空闲单位)。假设队列的最大尺寸为QueueSize,则队列满时的条件为(rear+1)%QueueSize==front。(因为rear可能大于也可能小于font,但它们之间总是相差一个元素空间)

循环队列的顺序存储结构代码如下,部分方法也如下:

typedef int QElemType;//QElemType根据实际情况而定,这里设置为int
/*循环队列的顺序存储结构*/
typedef struct{
   QElemType data[MAXSIZE];
   int front;  //头指针
   int rear;  //尾指针,若队列不为空,指向队尾元素的下一位置
}SqQueue;

/*初始化一个空队列*/
Status InitQueue(SqQueue *Q){
   Q->front=0;
   Q->rear=0;
   return OK;
}

/*返回Q的元素个数,即队列的长度*/
int QueueLength(SqQueue Q){
   return (Q.rear-Q.front+MAXSIZE)%MAXSIZE;
}

/*若队列未满,则插入元素e为Q的新的队尾元素*/
Status EnQueue(SqQueue *Q,QElemType e){
   if((Q->rear+1)%MAXSIZE==Q->front) return ERROR;//队列满了
   Q->data[Q->rear]=e;  //插入队尾元素
   //队尾指针后移一位,若到数组最后转向数组头部
   Q->rear=(Q->rear+1)%MAXSIZE;  
   return OK;
}

/*若队列不为空,则删除队列Q的队头元素,并用e返回其值*/
Status DeQueue(SqQueue *Q,QElemType *e){
   if(Q->rear==Q->front) return ERROR;//队列为空
   *e=Q->data[Q->front]; //将队头元素赋值给e
   //队头指针后移一位,若到数组最后转向数组头部
   Q->front=(Q->front+1)%MAXSIZE; 
   return OK;
}

若单是顺序存储,不是循环队列,算法的时间性能不高,但是循环队列又面临数组可能溢出问题,所以需要好好研究下队列的链式存储结构。

队列的链式存储结构

队列的链式存储结构,其实是线性表的单链表,只不过是只能尾进头出而已,将其简称为链队列。将队头指针指向链队列的头结点队尾指针指向终端结点当队列为空时,front和rear均指向头结点。

链队列的结构代码如下:

typedef int QElemType;
typedef struct QNode{  //结点结构
   QElemType data;
   struct QNode *next;
}QNode,*QueuePtr;

typedef struct{  //链队列结构
   QueuePtr front,rear; /*队头、队尾指针*/
}LinkQueue;

下面详细介绍链队列的入队、出队操作及具体代码:

/*插入元素e为Q新的队尾元素*/
Status EnQueue(LinkQueue *Q,QElemType e){
   //首先生成一个新的元素
   QueuePtr s=(QueuePtr)malloc (sizeof(QNode));
   if(! s) exit(OVERFLOW); //存储分配失败
   s->data=e;  //设置元素的值
   s->next=NULL;  //末尾元素,下一结点为空
   Q->rear->next=s;  //插入新元素,将原队尾元素的指针域指向新结点
   Q->rear=s;  //将队尾指针后移,指向新元素
   return OK;
}

/*若队列不为空时,删除队列Q的队头元素,并用e来返回其值,并返回OK,否则返回ERROR*/
Status DeQueue(LinkQueue *Q,QElemType *e){
   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;
}

循环队列与链队列的对比:
时间上,基本操作均为常数时间 O(1) ,不过循环队列事先申请存储空间,且使用期间不释放,而链队列,每次申请和释放结点均会产生时间开销,若入出队频繁,二者还是存在差异的。就空间上来说,循环队列有一固定长度,所以存在空间浪费的情况,虽然链队列不存在这个情况,但会产生空间开销,在可接受范围内,所以空间上链队列要更灵活些。

总而言之,在能确定队列长度最大值的情况下,建议使用循环队列,如果无法预估队列长度时,使用链队列

  • 5
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值