一、前言

栈和队列作为两种比较常见的线性结构,在数据结构中有着广泛的应用。栈的结构特点为后进先出,即只能在栈的一端插入数据,插入数据和出数据的一端为栈顶,另一端为栈底。队列的结构特点为先进先出,队用于出数据的一端为队头,用于入数据的一端为队尾。本文将在栈和队列的结构基础上,进一步探究循环队列的设计以及如何利用栈来实现队列。循环队列的设计与栈实现队列,为栈和队列的进一步运用,可以加深我们对栈和队列的认识与应用能力,培养知识迁移与应用思维。

二、循环队列

1、循环队列实现方式

我们来看下面这道循环队列oj题,题干给出了循环队列设计的具体要求:

题目要求设计的循环队列,我们可以类比于循环链表,即循环队列的队尾连接在队首之后,从而形成一个循环,循环队列相比于普通队列的一个优点为循环队列可以重复利用队列的空间,而普通队列一旦队列满了,就不能再插入元素。循环队列与队列一样,同样有两种实现方式,一种是通过顺序表实现,另一种则是通过链表实现,这两种方式相比,用顺序表实现循环队列,内存开销较低,实现难度也较小,链表实现具有额外的指针开销,且往往较复杂,需处理指针链接的问题,考虑到内存开销与实现难度的情况,我们选择顺序表的方式来实现循环队列。

2、循环队列的结构

我们采用动态顺序表的方式来实现循环队列的结构,结构体中需包含一个可动态开辟内存空间的指针,以及用于标识队头位置的head,用于标识队尾的tail,队列有效的数据个数k。

3、循环队列的初始化

在循环队列结构的基础上,我们就可以对循环队列进行初始化,这里需考虑的一个问题是动态开辟内存空间的大小问题,循环队列中有k个有效数据,循环队列是否只需开辟k个有效数据空间就可以呢?在这里,我们就需考虑一个特殊情况,当我们开辟k个有效数据空间时,循环队列会存在假溢出现象,即当循环队列为空时,head与tail位置相同,两者是相等的,而循环队列满时,tail会回绕至循环队列的队头位置,此时head与tail二者也是相等的,可参考下图所示:

通过上图,我们就可以发现当循环队列为空或者为满时,循环队列的head与tail是相等的,即假溢出现象,即head与tail相等时,我们就无法判断循环队列此时为空还是为满,因此我们就不能采取开辟k个有效数据空间的方式来初始化循环队列,而需要额外开辟一个空间来解决循环队列的假溢出问题,即开辟k+1个数据空间,多开辟的一个空间不存放数据,这时的tail所指向的即为循环队列队尾的下一个位置,此时tail与head二者就不会相等,这时循环队列就不会产生假溢出问题。

如上图所示,此时的tail指向队尾元素4的下一个位置,tail与head二者不相等,解决了循环队列的假溢出问题。

顺序表开辟k+1个数据空间,前k个数据空间用于存放数据,额外一个数据空间用于防止循环队列的假溢出,head与tail初始化为0,循环队列有效数据个数为k,从而完成了循环队列的初始化。

4、判断循环队列是否为空

有了前面的认识,判断循环队列是否为空,就迎刃而解了。即通过判断循环队列的head与tail是否相等来判断循环队列是否为空,当head与tail相等时,循环队列为空,二者不相等时,循环队列不为空。

5、判断循环队列是否为满

我们先讨论一般情况下循环队列为满的情况

由上图可知,当循环队列为满的情况下,此时head与tail满足的关系为tail+1=head。存在一种特殊情况,当循环队列为满时,tail指向循环队列的最后一个位置,如下图所示:

此时并不满足tail+1=head,判断循环队列是否为满,与判断循环队列是否为空不同在于,判断循环队列是否为满需解决tail的回绕问题,我们可以采取模运算来解决这个问题,即(tail+1)%(k+1)是否等于head,%(k+1)恰恰解决了tail的回绕问题,当tail指向循环队列的最后一个位置时,tail为4,(tail+1)%(k+1)为0,与head相等,易知一般情况下也同样成立。

6、循环队列插入数据

向循环队列中插入数据,即向循环队尾插入数据,先讨论特殊情况,当循环队列满时,这时就不能向循环队列中插入数据,我们可以调用前面所写的myCircularQueueIsFull函数判断循环队列是否为满,当循环队列不为满时,此时只需改变tail的位置。先向tail放置数据,tail向后移动一个位置,同时也需考虑tail的回绕问题,即当tail指向循环队列的最后一个位置时,此时再插入数据,tail就会回绕至循环队列的队头位置,因此需对tail进行模运算,tail=tail%(k+1),很好地解决了tail的回绕问题。

7、循环队列删除数据

与循环队列插入数据类似,先考虑特殊情况,当循环队列为空时,队列不能进行删除操作,可以调用前面所写的myCircularQueueIsEmpty函数判断循环队列是否为空。当循环队列不为空时,只需改变head的位置,即删除队头数据,head向后移动一个位置,同样也需考虑head的回绕问题,当head指向循环队列的最后一个位置时,此时删除数据,head就会回绕至循环队列的队头位置,因此需对head取模,head=head%(k+1),从而解决head的回绕问题。

8、访问循环队列的队头

访问循环队列的队头,先考虑特殊情况,当循环队列为空时,这时不能访问队头数据,可调用前面的myCircularQueueIsEmpty函数判断循环队列是否为空,当循环队列不为空时,只需返回循环队列head位置的数据即可。

9、访问循环队列的队尾

访问循环队列的队尾,先考虑特殊情况,当循环队列为空时,这时不能访问队尾数据,调用前面所写的myCircularQueueIsEmpty函数判断循环队列是否为空。一般情况下,tail-1即为队尾数据,如下图所示:

访问队尾数据,存在一种特殊情况,需考虑tail的回绕问题,如下图所示:

此时tail-1=-1,并不等于队尾数据所在的位置,tail需进行回绕,回绕至队尾数据4所在的位置,我们需对tail-1进行取模运算,(tail-1+(k+1))%(k+1)就很好解决了tail-1的回绕问题,这时tail-1就会回绕至队尾数据的位置,从而访问循环队列的队尾数据。

10、循环队列内存释放

对循环队列内存释放,即先释放顺序表,再将arr置为空,避免野指针,head、tail、k赋值为0,从而完成循环队列的释放。

11、循环队列小结

循环队列设计与普通队列不同在于需考虑head以及tail的回绕问题,需对head、tail进行取模运算,以及循环队列需多开辟一个内存空间来避免循环队列的假溢出。回绕问题与假溢出是循环队列设计的注意点,知道这两个注意点,就不难设计出循环队列了。

三、栈实现队列

1、队列实现方式

同样以一道栈实现队列oj题为例子:

题目要求使用两个栈来实现队列,我们先实现栈的结构与栈的相关操作。

2、栈的结构

这里我们采用顺序表的方式实现栈的结构,top指向栈顶元素的下一个位置,capacity表示栈的数据个数。

3、栈的初始化

对栈进行初始化,将arr初始化为NULL,由于top指向栈顶元素的下一个位置,因此需将top初始化为0,若top指向栈顶元素,则top应初始化为-1。如下图所示:

若top指向栈顶元素,则top不能初始化为0,因为当栈为空时,top=0指向栈底的第一个位置,向栈插入一个数据时,top也是在栈底的第一个位置,则不能区分栈是否为空,因此若想top指向栈顶元素,top需初始化为-1。top初始化为0,表示top指向栈顶元素的下一个位置,capacity初始化为0。

4、入栈

由于栈采取动态顺序表的方式实现,因此将数据入栈时需判断是否扩容,当top与capacity相等时,表示顺序表数据已满,需要扩容,顺序表扩容一般采取2倍方式,采取三目操作符判断是否栈为空,若为空,则选择4个数据空间大小,若栈不为空,则采取2倍扩容,realloc函数用于顺序表的扩容。扩容完成后,将数据x入栈,即放入top位置,数据入栈后,top向后移动一个位置,指向栈顶元素的下一个位置。

5、出栈

数据出栈,需确保栈不为空,即断言top>0,当top=0时,栈为空。出栈需将top向栈底移动一个位置,此时top指向栈顶元素,返回此时top指向的元素,即栈顶元素即可,完成数据的出栈。

6、判断栈是否为空

判断栈是否为空,只需判断top是否为0即可,当top=0时,栈为空,top不为0时,栈不为空。

7、双栈实现队列

采用两条栈来实现队列的先入先出结构,一个栈可用于表示向队列中插入数据,即输入栈,用s1表示,另一个栈可用于表示向队列中取出数据,即输出栈,用s2表示,这样两条栈的后进先出,正好互补,就形成了队列的先进先出结构。

8、队列的初始化

进行队列的初始化,malloc动态开辟队列结构体的空间,之后分别对队列结构体成员的输入输出栈s1,s2调用前面的STinit函数进行初始化,从而完成了队列的初始化。

9、入队列

数据入队列,只需将数据插入到输入栈s1即可,即将数据入栈s1,完成数据的入队列。

10、出队列

将数据出队列,先判断输出栈s2是否为空,若输出栈s2为空,则需先将输入栈s1的数据全部入栈到输出栈s2中,再将输出栈s2中的数据出栈,从而完成数据的出队列。

11、访问队头数据

访问队头数据,与出队列类似,需先判断输出栈s2是否为空,若输出栈s2为空,则需将输入栈s1的数据全部入栈到输出栈s2中,最后只需返回输出栈s2的栈顶数据即可,即输出栈s2的top-1位置的数据,就是队头数据。

12、判断队列是否为空

判断队列是否为空,只需判断队列输入栈s1与输出栈s2是否都为空即可,当输入栈与输出栈都为空时,队列为空,反之,队列就不为空。

13、队列内存释放

对队列内存进行释放,即释放队列的输入栈和输出栈,即依次释放输入栈s1和输出栈s2的顺序表,最后再释放队列指针obj指向的空间。

14、栈实现队列小结

栈实现队列,核心是利用双栈结构来实现队列,其中一个栈作为输入栈,用于向队列插入数据,另一个栈作为输出栈,用于从队列取出数据,输入栈与输出栈的互补,恰好实现了队列先进先出的结构特点,有了输入栈和输出栈,剩下对队列的操作就是对栈的操作了,问题就迎刃而解了。

四、结语

至此,关于循环队列的设计与栈实现队列的内容就全部介绍完了,循环队列的设计采取顺序表方式实现,能够很好地节省内存开销,且实现难度相比链表较为简单,需要注意的是,循环队列需要额外开辟一个数据空间来防止假溢出,以及需进行取模运算来解决head、tail的回绕问题。栈实现队列采取双栈结构来实现队列,其中一个栈作为输入栈,负责队列数据的插入,另一个栈作为输出栈,负责队列数据的输出,输入栈与输出栈恰好互补,从而实现队列先进先出的结构特点。循环队列的设计与栈实现队列为栈和队列的进一步应用,通过设计循环队列,以及利用栈实现队列,可以加深我们对栈和队列的认识,提高对栈和队列的应用能力。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐