数据结构之-队列
队列 一种特殊的 线性表 ,也是常见的一种数据类型。特殊之处在于它只能在表的前端(front)进行删除,而在表的后端(rear)进行插入操作。进行插入操作的端称为 队尾 ,进行删除操作的端称为 队头 。
队列 又称为先进先出(FIFO—first in first out)线性表。
线性表 分为 顺序存储 和 链式存储 ,栈是线性表,所以也有这两种存储方式。同样,队列作为一种特殊的线性表,也同样存在这两种存储方式。
顾名思义,顺序存储采用数组的方式,而链式存储采用链表的方式。
顺序存储的队列 又分为 顺序队列 和 循环队列 。
首选说一下顺序队列的坏话,顺序队列在 入队列 的时候可以保持O(1)的时间复杂度,而在 出队列 的时候队头后边的元素需要依次往前移动,以保证队头不为空,时间复杂度就变成了O(n);
为什么出队列时一定要全部移动呢,那么我们是否可以解决 出队列 所带来的问题呢,当然可以。
如果不去限制队列的元素必须存储在数组的前n个单元这一条件,出队的性能就会大大增加。这也就意味着队头不一定位于index=0的位置了。
假设是长度为5的数组,初始状态,空队列如所示,front与 rear指针均指向下标为0的位置。然后入队a1、a2、a3、a4, front指针依然指向下标为0位置,而rear指针指向下标为4的位置。
出队a1、a2,则front指针指向下标为2的位置,rear不变,如下图所示,再入队a5,此时front指针不变,rear指针移动到数组之外。嗯?数组之外,那将是哪里?
如上图所示,添加a5元素之后,rear已经无家可归了,于此同时0和1位置的空间却被拜拜浪费了,这个中情况被称之为 假溢出 。
为了解决 假溢出 的情况也就引出了我们接下来要讲的 循环队列 。
接着上图的来讲,为了把无家可归的rear有个好的落脚点,我们就把它放置在index=0的位置。
但是如果继续进行入队操作的话,比如继续插入a6、a7,则rear指针就与front指针重合,同时指向下标为2的位置。
此时问题又出来了,我们刚才说,空队列时,等于rear,现在当队列满时,也是from等于rear,那么如何判断此时的队列究竟是空还是满呢?
办法一 是设置一个标志变量flag,当front == rear,且flag = 0时为队列空,当front == rear,且flag= 1时为队列满。
办法二 是当队列空时,条件就是from = rear,当队列满时,我们修改其条件,保留一个元素空间。也就是说,队列满时,数组中还有一个空闲单元。 如下图所示,我们就认为此队列已经满了,也就是说,我们不允许上图情况出现。
在第二种情况下,由于rear可能比front大,也可能比front小,所以尽管它们只相差一个位置时就是满的情况,但也可能是相差整整一圈。所以若队列的最大尺寸为QueueSize,那么队列满的条件是(rear+1) %QueueSize == front (取模“%的目的就是为了整合rear与front大小为一个问题)。
比如上面这个例子, QueueSize = 5,当 front=0,而 rear=4, (4+1) %5 = 0,所以此时队列满。再比如,front = 2而rear =1。(1 + 1) %5 = 2,所以此时 队列也是满的。而对于下图, front = 2而rear= 0, (0+1) %5 = 1,1!=2,所以此时队列并没有满。
另外,当rear > front时,此时队列的长度为rear—front。但当rear < front时,队列长度分为两段,一段是QueueSize-front,另一段是0 + rear,加在一起,队列长度为rear-front + QueueSize,因此通用的计算队列长度公式为:
(rear—front + QueueSize) % QueueSize
从上面的图我们不难看出顺序存储存在着数组可能会溢出的问题,所以也就引出了链式存储结构。
在链队列中,队头指针指向头结点,队尾指针指向终端结点,一个普通的链队列如下图所示:
当队列为空时,front和rear都指向头结点。