《大话数据结构》读书笔记(四)--2

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

 

4.10 队列的抽象数据类型
同样是线性表,队列也有类似线性表的各种操作,不同的就是插入数据只能在队尾进行,删除数据只能在队头进行。
ADT 队列(Queue)
Data
同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
InitQueue( *Q ) :初始化操作,建立一个空队列Q。
DestoryQueue( *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的元素个数。
endADT

 

4.11 循环队列
4.11.1 队列顺序存储的不足
假设一个队列有 n 个元素,则顺序存储的队列需要建立一个大于n 的数组,并把队列的所有元素存储在数组的前 n 个单元,数组下标为0的一端即是队头。所谓入列操作,就是在队尾追加一个元素,不需要移动任何元素,因此时间复杂度O(1)。队列的出列是在队头,即下标为 0 的位置,那也就意味着队列中的所有元素都要向前移动,以保证队列的队头,下标为 0 的位置不为空,因此时间复杂度为O(n)。
为了避免元当只有一个元素时,队头和队尾重合使用处理变得麻烦,所以引入两个指针,front指针指向队头元素,rear指针指向队尾元素的下一个位置,这样当front等于rear时,此队列不是还剩一个元素,而是空队列。
 
假设这个队列的总个数不超过5个,但目前如果接着入队的话,因数组末尾元素已经占用,在向后加,就会产生数组越界,可实际上,队列在下标为0和1还有地方是空闲的,我们把这种现象叫“假溢出”
4.11.2 循环队列定义
解决假溢出的办法就是后面满了,就在从头开始,也就是头尾相接的循环。我们把这队列的这种头尾相接的顺序存储结构称为循环队列

 

rear 可以改为指向下标为0 的位置,这样就不会造成指针指向不的问题了。接着入队a6,将它放置于下标为0处,rear指针指向下标为1处

 

若a7再入队,则rear指针就与front 指针重合,同时指向下标为2的位置。
此时,又遇到问题,空队列时,front等rear,现在队列满时,也是front等于rear,那么如何判断队列是空还是满呢?
办法一:设置一个标志变量flag,当front = rear,且flag =0时,为空队列,当front=rear,且flag=1时,为队列满。
办法二,当队列空时,条件就是front= rear,当队列满时,我们修改其条件,保留一个元素空间。也就是说,队列满时,数组中还有一个空闲单元。
我们重点来讨论第二种方法,由于 rear 可能比 front 大,也可能比 front 小,所以尽管它们只相差一个位置时就是满的情况,但也可能是相整整所以若队列的最大尺寸为 QueueSize ,那么队列满的条件是 (rear+l) % QueueSlze == front (取模 "%" 的目的就是为了整合 rear 与 front 大小为一个问题)。
通用的计算队列长度公式为:
(rear- front + QueueSize) %QueueSize
实现循环队列的代码
typedef int QElemType ; /* QElemType 类型根据实际情况而定,这里假设为int */
/* 循环队列的顺序存储结构 */
typedef struct
{
QElemType data [ MAXSIZE ];
int front; /* 头指针 */
int rear ; /* 尾指针,若队列不空,指向队列尾元素的下一个位置 */
}SqQueue;
循环队列的初始化代码如下:
/* 初始化一个空队列Q */
Status、 IntitQueue ( 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; /* 将元素e赋值给队尾 */
Q -> rear = ( Q -> rear +1 ) %MAXSIZE; /* rear指针向后移一位置,若到最后转到数组头部 */
return OK;
}
循环队列的出队操作代码如下:
/* 若队列不空,则删除Q中队头元素,用e 返回其值 */
status DeQueue ( SqQueue *Q,QElemType *e ){
if( Q -> front == Q -> rear ){ /* 队列空的判断 */
return ERROR;
}
*e = Q ->data[ Q -> front ]; /* 将队头元素赋值给e */
Q -> front = (Q ->front +1 ) %MAXSIZE; /* front 指针向后移一位置,若到最后则转到数组的头部 */
return OK;
}
4.12 队列的链式存储结构及实现
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。为了操作上的方便,我们将队头指针指向链队列的头结点,而队尾指针指向终端结点。
链队列的结构为:
typedef int QElemType; /*QElemType类型根据实际情况而定,这里假设为int */
typedef struct QNode{ /* 结点结构 */
QElemType data;
struct QNode *next;
}QNode,*QueuePtr;
 
typedef struct /* 队列的链表结构 */
{
QueuePtr front ,rear; /* 队头、队尾指针 */
}LinkQueue
 
4.12.1 队列的链式存储结构 -- 入队操作

 

/* 插入元素 e 为Q 的新的队尾元素 */
status EnQueue( LinkQueue *Q,QElemType e ){
QueuePtr s = (QueuePtr) malloc(sizeod(Node));
if( ! s ){ /* 存储分配失败 */
exit(OVERFLOW);
}
s -> data =e;
s -> next = NULL;
Q -> rear -> next =s; /* 把拥有元素e新结点s赋值给原队尾结点的后继 */
Q -> rear = s; /* 把当前的s设置为队尾结点,rear指向s */
return OK;
}
4.12.2 队列的链式存储结构 -- 出队操作
出队操作时,就是头结点的后继结点出队,将头结点的后继改为它后面的结点,若链表除头结点外只剩一个元素时,则需将rear指向头结点

 

/* 若队列不空,删除Q的队头元素,用e返回其值,并返回ok,否则返回error */
status DeQueue( LinkQueue *Q , QElemType *e ){
QueuePtr p;
if ( Q -> front == Q -> rear ) {
return ERROR;
}
p = Q ->front ->next; /* 将欲删除的队头结点暂存给p */
*e = p ->data; /* 将欲删除的队头结点的值赋值给e */
Q -> front -> next = p -> next; /* 将原队头结点后继 p->next赋值给头结点后继 */
if ( Q -> rear == p ){ /* 若队头是队尾,则删除后将rear指向头结点 */
Q -> rear = Q -> front;
}
free (p);
return ok;
 
}
 
小结
循环队列与链队列的比较
从时间上,他们的基本操作都是常数时间,即都为O(1)
不过循环队列时事先申请好的空间,使用期间不能释放,而对于链队列,每次申请和释放结点也会存在一些时间开销,如果入队出队频繁,则两者还是有细微差异。
4.13 总结回顾
栈( stack )是限定仅在表尾进行插入和删除操作的线性表。
队列 ( queue) 是只允许在一端进行插入操作,而在另一端进行删除操作的线性表
他们均可以用线性表的顺序存储结构来实现,但都存在顺序存储的一些弊端。
 
对于栈来说,如果是两个相同数据类型的栈,则可以用数组的两端作栈底的方法来让两个栈共享数据,这就可以最大化地利用数组的空间。
对于队列来说,为了避免数组插入和删除时需要移动数据,于是就引入了循环队列 ,使得队头和队尾可以在数组中循环变化。解决了移动数据的时间损耗,使得本来插入和删除是 O(n)的时间复杂度变成了 O(1)。
他们也都可以通过链式存储结构来实现,实现原则上与线性表基本相同。

 

 

posted @ 2018-08-22 14:26  僵尸吃过跳跳糖  阅读(210)  评论(0编辑  收藏  举报