队列的定义
定义
队列是只允许在一端进行插入操作,而在另一端进行删除操纵的特殊的线性表。
允许进行元素插入操作的一端称为队尾(rear),允许进行元素删除操作的一端称为队头(front)。在队尾插入元素的操作称为入队(Enqueue),在队头删除元素的操作称为出队(Dequeue)如图1-1-1所示。

特点
先进先出(First In First Out)
#队列的抽象数据类型
同样是线性表,队列也有类似线性表的各种操作,不同的就是插入数据只能在队尾进行,输出数据只能在队头进行。
1 2 3 4 5 6 7 8 9 10 11 12 13
| ADT 队列(Queue) Data 同线性表。元素具有相同的类型,相邻元素具有线性关系。 Operation InitQueue(*Q):初始化操作,建立一个空队列Q。 DestroyQueue(*Q):若队列Q存在,则销毁它。 ClearQueue(*Q):将队列Q清空。 QueueEmpty(*Q):若队列Q为空,则返回true,否则返回false。 GetHead(Q,*e):若队列Q存在且非空,用e返回队列Q的队头元素。 EnQueue(*Q,e):若队列Q存在,插入新元素e到队列Q中并成为队尾元素。 DeQueue(*Q,*e):删除队列Q中队头元素并用e返回其值。 QueueLength(Q):返回队列Q的元素个数。
|
循环队列
栈作为一种特殊的线性表,栈有链表栈和顺序栈两种实现方式。同样对于队列来说,队列也是一种特殊的线性表,因此也同样有两种实现方式,但队列更为特殊一点,实现队列常用的两种方式分别是循环队列和链式队列。
顺序队列
在引入循环队列之前,我们先来分析一下队列顺序存储的不足。
和顺序栈相类似,在队列的顺序存储结构中,除了用一组地址连续的存储单元依次存放从队头到队尾的元素之外,还需要两个指针进行管理。一个是队头指针(front)指向队头元素,另一个是队尾指针(rear)指向下一个入队元素的存储位置。
当我们需要向队列添加一个元素时,只要在队尾追加一个元素就可以了,不需要移动任何元素,因此时间复杂度为θ(1),如图3-1-1所示。

当我们需要队头的元素出队时,第一个位置就空出来了。那就意味着,队列中剩下的所有元素都得向前移动,以保证队列的队头有元素存放(如图3-1-2所示)。这就好比我们排队买票,前面的人买好了离开,后面的人就要向前不缺空位,这似乎很合理。但在队列中,若要完成这一行为,此时的时间复杂度为θ(n),显然这中行为是浪费的。

既然补缺空位的做法是浪费的,那何必出队列时一定要全部移动呢。如果不去限制队列的元素必须存储在数组的前n个单元这一条件,出队的性能就会大大增加。也就是说,队头(front)不需要一定在存储空间的最前面,如图3-1-3所示。

但是,当我们这样定义的时候,会出现一种比较的特殊的情况,如图3-1-4所示。

正如图3-1-4所示,A5入队之后,rear指针移动了数组之外,数组之外是哪里呢?是不是出错了?如果再有一个A6入队,因为队列添加元素只能在队尾进行操作,但数组末尾已经备注占用了,再向后加产生了错误。是不是就不能添加元素了呢?如果你上了俩公交车,发现前两排有个空座位,而后排所有座位都已经占满了,那你会说车上已经没有座位了,所以你要下车。显然这是不可能的。
在队列中若出现的这种情况,我们称为“假上溢”现象,除此之外,还有“真上溢”,“下溢”现象。
“假上溢”现象:由于入队和出队操作中,头尾指针只增加不减小,致使被删元素的空间永远无法重新利用。当队列中实际的元素个数远远小于向量空间的规模时,也可能由于尾指针已超越向量空间的上界而不能做入队操作。该现象称为”假上溢”现象。
“真上溢”现象:当队列满时,做进栈运算产生空间溢出的现象。“真上溢”是一种出错状态,应设法避免。
“下溢”现象:当队列为空时,做出队运算产生的溢出现象。“下溢”是正常现象,常用作程序控制转移的条件。
“假上溢”现象显然是很浪费的一种情况,我们肯定是要想办法解决这一问题的,随之而来的也就引入了循环队列。
循环队列
很简单,假上溢的时候,既然后面的满了,前面的还有空缺,那就从头在开始呗,也就是头尾想接的循环。我们把队列的这种头尾相接的顺序储存结构称为循环队列。在图3-1-4中,我们就可以根据循环队列的定义将rear改为下标为0的位置,如图3-2-1所示。

事实上,我们换一种方式更容易理解循环队列,如图3-2-1所示。

若我们这样定义循环队列时,我们会发现还有问题:
当队列没有元素,为空队列,此时rear=front,如图3-2-3所示;
当队列满时,此时rear=front,如图3-2-4所示。

也就是说当rear=front时,我们并不能确定此时队列究竟是空队列还是满队列。此时有两种方法解决这一问题:
- 设置一个标志变量flag,,当front=rear,且flag=0时为空队列,当front=rear。且flag=1是为满队列。
- 少用一个空间,也就是说当数组中还有一个空闲单元时,我们认为队列已经满了。如图3-2-5所示。而当队列空时,条件就是front=rear,即我们不允许图3-2-4所示的情况出现。

除了上面我们分析的一些情况,在具体的设计中,必须为循环队列设定一个最大队列的长度。具体的代码可参考下面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| #define MAX_QSIZE 5 typedef struct { QElemType *base; int front; int rear; }SqQueue;
void InitQueue(SqQueue *Q) { Q->base=malloc(MAX_QSIZE*sizeof(QElemType)); if(!Q->base) exit(OVERFLOW); Q->front=Q->rear=0; }
void DestroyQueue(SqQueue *Q) { if(Q->base) free(Q->base); Q->base=NULL; Q->front=Q->rear=0; }
void ClearQueue(SqQueue *Q) { Q->front=Q->rear=0; }
Status QueueEmpty(SqQueue Q) { if(Q.front==Q.rear) return TRUE; else return FALSE; }
int QueueLength(SqQueue Q) { return(Q.rear-Q.front+MAX_QSIZE)%MAX_QSIZE; }
Status GetHead(SqQueue Q,QElemType *e) { if(Q.front==Q.rear) return ERROR; *e=Q.base[Q.front]; return OK; }
Status EnQueue(SqQueue *Q,QElemType e) { if((Q->rear+1)%MAX_QSIZE==Q->front) return ERROR; Q->base[Q->rear]=e; Q->rear=(Q->rear+1)%MAX_QSIZE; return OK; }
Status DeQueue(SqQueue *Q,QElemType *e) { if(Q->front==Q->rear) return ERROR; *e=Q->base[Q->front]; Q->front=(Q->front+1)%MAX_QSIZE; return OK; }
void QueueTraverse(SqQueue Q,void(*vi)(QElemType)) { int i; i=Q.front; while(i!=Q.rear) { vi(Q.base[i]); i=(i+1)%MAX_QSIZE; } printf("\n"); }
|
4.链式队列
在队列的形成过程中,可以利用线性链表的原理,来生成一个队列。基于链表的队列,要动态动态创建和删除节点,效率较低,但是可以动态增长。一个链队列显然需要两个分别指示队头和队尾的指针才能唯一确定,,为了操作上的方便,我们将队头指针指向链队列的头结点,而队尾指针指向终端结点,如图4-1-1所示。

空队列时,front和rear都指向头结点,如图4-1-2所示。

入队操作就是在链表尾部插入结点,如图4-1-3所示。

出队操作时,就是头结点的后继结点出队,将头结点的后继改为它后面的结点如图4-1-4所示。

若链表除头结点只剩一个元素时,则需将rear指向头结点,如图4-1-5所示。

链式队列使用链表作为基本数据结果,所以不存在伪溢出的问题,队列长度也没有限制。但插入和读取的时间代价较高
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
| typedef struct QNode { QElemType data; struct QNode *next; }QNode,*QueuePtr;
typedef struct { QueuePtr front,rear; }LinkQueue;
void InitQueue(LinkQueue *Q) { Q->front=Q->rear=malloc(sizeof(QNode)); if(!Q->front) exit(OVERFLOW); Q->front->next=NULL; }
void DestroyQueue(LinkQueue *Q) { while(Q->front) { Q->rear=Q->front->next; free(Q->front); Q->front=Q->rear; } }
void ClearQueue(LinkQueue *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); } }
Status QueueEmpty(LinkQueue Q) { if(Q.front->next==NULL) 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_Q(LinkQueue Q,QElemType *e) { QueuePtr p; if(Q.front==Q.rear) return ERROR; p=Q.front->next; *e=p->data; return OK; }
void EnQueue(LinkQueue *Q,QElemType e) { QueuePtr p= (QueuePtr)malloc(sizeof(QNode)); if(!p) exit(OVERFLOW); p->data=e; p->next=NULL; Q->rear->next=p; Q->rear=p; }
Status DeQueue(LinkQueue *Q,QElemType *e) { QueuePtr p; if(Q->front==Q->rear) return ERROR; p=Q->front->next; *e=p->data; Q->front=p->next; if(Q->rear==p) Q->rear=Q->front; free(p); return OK; }
void QueueTraverse(LinkQueue Q,void(*vi)(QElemType)) { QueuePtr p; p=Q.front->next; while(p) { vi(p->data); p=p->next; } printf("\n"); }
|
对于循环队列与链队列的比较,可以从两方面来考虑:
从时间上,其实它们的基本操作都是常数时间,即都为0(1)的,不过循环队列是事先申请好空间,使用期间不释放,而对于链队列,每次申请和释放结点也会存在一些时间开销,如果入队出队频繁,则两者还是有细微差异。
对于空间上来说,循环队列必须有一个固定的长度,所以就有了存储元素个数和空间浪费的问题。而链队列不存在这个问题,尽管它需要一个指针域,会产生一些空间上的开销,但也可以接受。所以在空间上,链队列更加灵活。
- 总的来说,在可以确定队列长度最大值的情况下,建议用循环队列,如果你无法预估队列的长度时,则用链队列。
用数组实现队列时,如果不移动,随着数据的不断读写,会出现假满队列的情况。即尾数组已满但头数组还是空的。循环队列也是一种数组,只是它在逻辑上把数组的头和尾相连,形成循环队列,当数组尾满的时候,要判断数组头是否为空,不为空继续存放数据,可以有效的利用资源。但是用循环队列有个小麻烦,不好判断数列是为空还是为满;
链队列就不存在上面的问题。“循环队列”最大优点就是节省空间和少分配空间,而链队列多了一点点地址存储开销。