程序猿必修课之数据结构(八)队列

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xiaomai949804781/article/details/71081028

文本同步发布于简书:http://www.jianshu.com/p/5d020c00fcb8

上一章:程序猿必修课之数据结构(七)栈2

队列的定义

队列(Queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。

队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。

队列的抽象数据类型

队列是特殊的线性表,因此它的各种操作类似线性表,不同的是插入数据只能在队尾进行,删除数据只能在队头进行。

ADT 队列(Queue)

Data
    同线性表。元素的类型相同,相邻元素具有前驱和后继关系。

Operation
    initQueue(*Q): 队列初始化,建立一个空队列。
    destroyQueue(*Q): 若队列 Q 存在,就销毁它。
    clearQueue(*Q): 将队列清空。
    isEmpty(*Q): 若队列为空,返回 true;否则返回 false。
    getHead(Q, *e): 若队列 Q 存在且不为空,则用 e 返回队列 Q 中的队头元素。
    enQueue(*Q, e): 若队列 Que 存在,插入新元素 e 到队列 Q 中并成为队尾元素。
    deQueue(*Q, *e): 删除队列 Q 中队头元素,并用 e 返回。
    getLength(Q): 返回队列 Q 的元素个数。

endADT 

循环队列

我们把头尾相接的队列的顺序存储结构称为循环队列。

用 front 指针指向队头元素,用 rear 指针指向队尾元素的下一个位置(为了避免数组越界,队尾元素后要留一个空闲单元,保存 rear 指针)。所以当 front == rear 时,表示队列为空;当数组中只有一个空闲单元时队列就满了。

因为是循环队列,所以 rear 可能比 front 大,也可能比 fornt 小,如下图所示:

可以看出,它们可能只相差一个位置,但也可能相差整整一圈。所以若队列的最大容量为 queueSize,那么队列满的条件是:(rear + 1) % queueSize == front。

当 rear > front 时(即左图),队列的长度为 rear - front;当 rear < front 时(即右图),队列的长度由两部分构成,一部分是 queueSize - front,另一部分是 0 + rear,所以队列长度为 rear - front + queueSize。所以通用的计算队列长度的公式为:

(rear - front + QueueSize) % queueSize

循环队列的顺序存储结构

define MAXSIZE 10
typedef struct {
  int data[MAXSIZE];
  int front;
  int rear;
} Queue;

初始化:

void initQueue(Queue *q) {
  q->front = 0;
  q->rear = 0;
}

求队列长度

int queueLength(Queue q) {
  return (q->rear - q->front + MAXSIZE) % MAXSIZE;
}

入队操作:

boolean enQueue(Queue *q, int e) {
  /* 队列已满 */
  if ((q->rear + 1) % MAXSIZE == q->front)
    return false;
  q->data[q->rear] = e;
  q->rear = (q->rear + 1) %  MAXSIZE;
  return true; 
}

出队操作:

boolean deQueue(Queue *q, int *e) {
  /* 队列为空 */
  if (q->front == q->rear)
    return false;
  *e = q->data[q->front];
  q->front = (q->front + 1) % MAXSIZE;
  return true;
}

虽然循环队列解决了出队时算法的时间复杂度不高的问题,但是它仍然有数组可能会溢出的问题,所以还需要链式存储结构来解决这个问题。

队列的链式存储结构及实现

队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,简称为链队列。

为了方便操作,队头指针指向链队列的头结点;尾指针指向终端结点。当 front 和 rear 都指向头结点时,队列为空。

链队列的结构为:

typedef struct QNode {
  int data;
  struct QNode *next;
}QNode, *pQueue;

typedef struct{
  pQueue front;
  pQueue rear;
}LinkQueue;

入队操作

boolean enQueue(LinkQueue *q, int e) {
  pQueue s = (pQueue)malloc(sizeof(QNode));
  if(!s)
    return false;
  s->data = e;
  s->next = NULL;
  q->rear->next = s;
  q->rear = s;
  return true;
}

出队操作

出队操作就是头结点的后继结点 p 出队,将头结点的后继改为 p 的后继结点,如果p 是 rear 指向的结点,那么还要将 rear 指向头结点。

boolean deQueue(LinkQueue *q, int *e) {
  pQueue p;
  if (q->front == q->rear)
    return false;
  p = q->front->next;
  *e = p->data;
  q->front->next = p->next;
  if(q->rear == p)
    q->rear = q->front;
  free(p);
  return true;
}

比较循环队列和链队列

从时间复杂度,它们的基本操作都是 O(1);但是循环队列需要先申请好空间,使用期间不释放,而对于链队列,每次申请和释放结点也会存在一些时间开销。

从空间上来说,循环队列必须有一个固定的长度,这样就可能存在空间浪费的问题;而链队列不存在这个问题,尽管它需要一个指针域。

总的来说,在可以确定队列长度最大值的情况下,建议用循环队列,反之用链队列。

猜你喜欢

转载自blog.csdn.net/xiaomai949804781/article/details/71081028