先进先出的线性表怎么实现增删查

163 阅读4分钟

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情

在需要严格遵守数据处理顺序的场景下,我们对线性表予以限制,那么就得到了后进先出的数据结构,栈。与之对应的还有一种限制的线性表,它遵循先进先出的性质,这就是队列。

队列是什么

与栈相似,队列也是一种特殊的线性表,与线性表的不同之处也是体现在对数据的增和删的操作上。

队列的特点是先进先出:

  • 先进,表示队列的数据新增操作只能在末端进行,不允许在队列的中间某个结点后新增数据;

  • 先出,队列的数据删除操作只能在始端进行,不允许在队列的中间某个结点后删除数据。也就是说队列的增和删的操作只能分别在这个队列的队尾和队头进行,如下图所示:

Ciqc1F7XiWKAYQ6uAABDmk0Wx98597.png

与线性表、栈一样,队列也存在这两种存储方式,即顺序队列和链式队列:

  • 顺序队列,依赖数组来实现,其中的数据在内存中也是顺序存储。

  • 链式队列,则依赖链表来实现,其中的数据依赖每个结点的指针互联,在内存中并不是顺序存储。链式队列,实际上就是只能尾进头出的线性表的单链表。

如下图所示,我们将队头指针指向链队列的头结点,队尾指针指向终端结点。不管是哪种实现方式,一个队列都依赖队头(front)和队尾(rear)两个指针进行唯一确定。

Ciqc1F7XiWqAdDfGAABpdzpnaic772.png

当队列为空时,front 和 rear 都指向头结点,如下图所示:

CgqCHl7XiXGADFxLAAAzCTHg63s024.png

队列对于数据的增删查处理

队列从队头(front)删除元素,从队尾(rear)插入元素。对于一个顺序队列的数组来说,会设置一个 front 指针来指向队头,并设置另一个 rear 指针指向队尾。当我们不断进行插入删除操作时,头尾两个指针都会不断向后移动。

为了实现一个有 k 个元素的顺序存储的队列,我们需要建立一个长度比 k 大的数组,以便把所有的队列元素存储在数组中。队列新增数据的操作,就是利用 rear 指针在队尾新增一个数据元素。这个过程不会影响其他数据,时间复杂度为 O(1),状态如下图所示:

Ciqc1F7XiX2AcswVAAB59JjuOyQ221.png

队列删除数据的操作与栈不同。队列元素出口在队列头部,即下标为 0 的位置。当利用 front 指针删除一个数据时,队列中剩余的元素都需要向前移动一个位置,以保证队列头部下标为 0 的位置不为空,此时时间复杂度就变成 O(n) 了,状态如下图所示:

Ciqc1F7XiYOAdl0WAACJFu_x0pg197.png

我们看到,front 指针删除数据的操作引发了时间复杂度过高的问题,那么我们该如何解决呢?我们可以通过移动指针的方式来删除数据,这样就不需要移动剩余的数据了。但是,这样的操作,也可能会产生数组越界的问题。接下来,我们来详细讨论一下。

我们一起来看一个利用顺序队列,持续新增数据和删除数据的例子。

初始时,定义了长度为 5 的数组,front 指针和 rear 指针相等,且都指向下标为 0 的位置,队列为空队列。如下图所示:

CgqCHl7XiYqAaxUUAACPoQw5EYI800.png

当 A、B、C、D 四条数据加入队列后,front 依然指向下标为 0 的位置,而 rear 则指向下标为 4 的位置。

当 A 出队列时,front 指针指向下标为 1 的位置,rear 保持不变。其后 E 加入队列,front 保持不变,rear 则移动到了数组以外,如下图所示:

Ciqc1F7XiZCAQFJlAACnK-UehMk790.png

总结

可以发现,队列与前一课时我们学习的栈的特性非常相似,队列也继承了线性表的优点与不足,是加了限制的线性表,队列的增和删的操作只能在这个线性表的头和尾进行。

在时间复杂度上,循环队列和链式队列的新增、删除操作都为 O(1)。而在查找操作中,队列和线性表一样只能通过全局遍历的方式进行,也就是需要 O(n) 的时间复杂度。在空间性能方面,循环队列必须有一个固定的长度,因此存在存储元素数量和空间的浪费问题,而链式队列不存在这种问题,所以在空间上,链式队列更为灵活一些。

通常情况下,在可以确定队列长度最大值时,建议使用循环队列。无法确定队列长度时,应考虑使用链式队列。队列具有先进先出的特点,很像现实中人们排队买票的场景。在面对数据处理顺序非常敏感的问题时,队列一定是个不错的技术选型。