栈和队列是常见的基础数据结构,彼此间关系密切。之前我们已经介绍过栈,栈的操作遵循“后进先出”原则,简单来说就是“先进的排在后面,后来的要先走”。可以把栈想象成一个狭窄的隧道,只能从一侧进,另一侧出来,最新进入的物品必须先出来。与此不同的是,队列则遵循“先进先出”规则,类似于日常生活中的排队场景,前面的人先走,后面的人随后跟着。
从数据结构的角度来看,栈有些“自私”,它总是先处理新进来的元素,把老元素暂时放在一边。而队列则相对公平,谁先来谁先走,大家都得遵守排队规则。这使得队列在计算机程序设计中具有广泛应用,比如消息队列、磁盘调度、广度优先搜索等,都离不开队列这一结构。
队列的基本概念
队列可以被看作一种特殊的线性表。与一般的线性表不同,队列只能在一端进行插入操作,而只能在另一端进行删除操作。前端(front)是删除元素的位置,后端(rear)是插入元素的位置。常见的队列实现有数组和链表两种方式。
理解队列的前提是了解顺序表的基本操作和栈的原理。为了更好地理解队列的工作方式,我们可以参考力扣题目622中的循环队列设计方案作为例子。
队列的核心操作
队列的实现通常涉及以下几个基本操作:
初始化:构造一个队列,指定队列的大小。
入队:将一个元素插入队尾。
出队:删除队头的元素。
获取队头元素:返回队头元素的值,但不删除它。
获取队尾元素:返回队尾元素的值,但不删除它。
检查队列是否为空:队列为空时返回true,否则返回false。
检查队列是否已满:队列已满时返回true,否则返回false。
队列的实现方式
数组实现
对于数组实现的队列,通常会有两个指针,分别指向队列的前端和后端。数组的操作比链表略为简单,但需要注意处理队列满和队列空的情况。
在数组队列中,入队和出队操作的难点在于如何利用数组空间。一旦队列头部元素被删除,数组空间就被释放出来,通常我们会用循环队列的方式来优化空间的利用。循环队列的最大优点就是可以“重复利用”这些空间。
数组实现中的基本操作:
初始化:初始化时,队头(front)和队尾(rear)都指向数组的开始位置。此时队列为空。
入队操作:如果队列未满,先将新元素插入到队尾,然后将队尾指针移动一位。如果队尾指针到达数组末尾,则将其重新定位到数组的起始位置。
出队操作:如果队列不为空,将队头元素删除,并将队头指针向后移动一位。如果队头指针到达数组末尾,也需要回到数组的起始位置。
这种方式通过“求余”运算来实现循环,确保队列能够有效利用所有空间。
链表实现
链表实现的队列则稍显灵活。我们通常使用单链表来实现队列,链表有一个头节点和一个尾节点,队列的“前端”对应链表的头部,队列的“后端”对应链表的尾部。
使用链表的优势在于,插入和删除操作可以在两端高效进行,不会浪费空间。具体实现时,我们可以采用两种方案:
方案一:队头设在链表尾部,队尾设在链表头部。插入操作很方便,但删除操作需要遍历链表到队尾,效率较低。
方案二:队头设在链表头部,队尾设在链表尾部。插入和删除操作都比较高效,但需要维护一个指向尾节点的指针。
最终,通常会选择第二种方案,即在链表中增加头节点和尾指针,这样可以使得队列的插入和删除操作都变得非常简单。
链表实现中的基本操作:
初始化:我们创建一个头节点,并让队头和队尾都指向这个头节点。
入队操作:将新元素插入到队尾,并更新尾指针。
出队操作:将队头元素删除,并更新队头指针。如果队列只剩一个元素,需要特别处理尾指针。
是否为空:可以通过判断队头和队尾指针是否相等来判断队列是否为空。
循环队列的优势
循环队列的主要优势在于它避免了空间的浪费。在普通队列中,一旦队列满了,即使前面有空余空间,也不能插入新的元素。而循环队列则通过“首尾相接”来实现队列空间的循环使用,使得可以在任何时候都利用空闲的空间。
双端队列的实现
除了常见的单端队列,双端队列也是一种非常高效的数据结构。它允许在队列的两端都进行插入和删除操作,既可以当作队列使用,也可以当作栈使用。ArrayDeque就是基于数组实现的双端队列。
实现双端队列时,我们需要考虑队头插入、队尾插入、队头删除和队尾删除等操作。每个操作都需要处理指针的移动以及是否越界的检查。
队列是一种非常重要且实用的数据结构。无论是用于消息处理、任务调度还是广度优先搜索等算法中,都离不开队列的应用。理解队列的工作原理,并能够根据实际需要选择合适的实现方式,是每个程序员必须掌握的技能。