栈和队列是常见的数据结构。栈的特点是后进先出,添加元素、删除元素和查看元素都在栈顶操作。队列的特点是先进先出,添加元素在队尾操作,删除元素和查看元素在队首操作。
双端队列比栈和队列更加灵活,可以在双端队列的两端添加元素、删除元素和查看元素。
栈、队列和双端队列都满足每次添加元素、删除元素和查看元素的时间复杂度是 O ( 1 ) O(1) O(1)。
栈、队列和双端队列都可以基于数组或链表实现。
栈是一种特殊的线性表,特点是后进先出,最先加入的元素最后取出,最后加入的元素最先取出。
栈的常见操作包括判断栈是否为空和获得栈内元素个数,以及对元素的操作。对元素的操作都位于栈顶,包括以下三种操作:
下图为栈的示意,左边为栈底,右边为栈顶。将数字 1 1 1 到 5 5 5 依次入栈之后,栈的状态如下图所示,此时栈顶元素是 5 5 5,第一个出栈的元素是 5 5 5。
队列是一种特殊的线性表,特点是先进先出,最先加入的元素最先取出,最后加入的元素最后取出。队列有头部和尾部,队列头部称为队首,队列尾部称为队尾,队列内的元素从队首到队尾的顺序符合加入队列的顺序。
队列的常见操作包括判断队列是否为空和获得队列内元素个数,以及对元素的操作。对元素的操作包括以下三种操作:
需要注意的是,队列的三种对元素的操作所在的位置不同,查看元素和删除元素位于队首,添加元素位于队尾。
下图为队列的示意,左边为队首,右边为队尾。将数字 1 1 1 到 5 5 5 依次入队之后,队列的状态如下图所示,此时队首元素是 1 1 1,队尾元素是 5 5 5,第一个出队的元素是 1 1 1。
双端队列是一种特殊的线性表,同时具有栈和队列的性质,特点是在队首和队尾都可以加入和取出元素。栈和队列都可以基于双端队列实现。
Java 提供了多种类和接口支持栈、队列和双端队列的实现。
Stack \texttt{Stack} Stack 类是早期版本的栈的实现类,继承自 Vector \texttt{Vector} Vector 类。在后续版本中,JDK 的官方文档不建议使用 Stack \texttt{Stack} Stack 类实现栈的功能,而是建议使用 Deque \texttt{Deque} Deque 接口及其实现类实现栈的功能。
Queue \texttt{Queue} Queue 接口是队列的接口,需要通过实现类完成实例化,常见的实现类包括 ArrayDeque \texttt{ArrayDeque} ArrayDeque 类和 LinkedList \texttt{LinkedList} LinkedList 类。
Deque \texttt{Deque} Deque 接口是双端队列的接口,需要通过实现类完成实例化,常见的实现类包括 ArrayDeque \texttt{ArrayDeque} ArrayDeque 类和 LinkedList \texttt{LinkedList} LinkedList 类。
ArrayDeque \texttt{ArrayDeque} ArrayDeque 类和 LinkedList \texttt{LinkedList} LinkedList 类都可以作为栈和队列的实现类,区别在于, ArrayDeque \texttt{ArrayDeque} ArrayDeque 类的底层实现是循环数组, LinkedList \texttt{LinkedList} LinkedList 类的底层实现是双向链表。根据 JDK 的官方文档, ArrayDeque \texttt{ArrayDeque} ArrayDeque 类作为栈使用时效率高于 Stack \texttt{Stack} Stack 类, ArrayDeque \texttt{ArrayDeque} ArrayDeque 类作为队列使用时效率高于 LinkedList \texttt{LinkedList} LinkedList 类。无论是栈的实现还是队列的实现,都推荐使用 ArrayDeque \texttt{ArrayDeque} ArrayDeque 类。
单调栈和单调队列是栈和队列的高级应用,可以解决一些复杂的问题。
单调栈和单调队列满足元素的单调性。具体而言,单调栈内从栈底到栈顶的元素依次递增或递减,单调队列内从队首到队尾的元素依次递增或递减。在单调栈和单调队列中添加元素时,必须维护元素的单调性。
向单调栈添加元素时,首先需要检查栈顶元素和待添加元素是否满足单调性,如果不满足单调性则将栈顶元素出栈,直到栈为空或者栈顶元素和待添加元素满足单调性,然后将待添加元素入栈。
向单调队列添加元素时,首先需要检查队尾元素和待添加元素是否满足单调性,如果不满足单调性则将队尾元素出队,直到队列为空或者队尾元素和待添加元素满足单调性,然后将待添加元素入队。
由于普通的队列不支持在队尾将元素出队,因此需要使用双端队列实现单调队列的功能。
单调栈和单调队列的应用需要考虑具体情况。有时需要维护下标信息,因此在单调栈和单调队列中存储的不是元素本身,而是元素下标,下标对应的元素满足单调性。
使用单调栈和单调队列实现通常需要两层循环,但是由于每个元素最多只会在单调栈或单调队列中被添加一次和删除一次,因此时间复杂度是线性复杂度,而不是平方复杂度。
优先队列是一种不同于栈、队列和双端队列的数据结构。栈、队列和双端队列的实现原理是线性表,而优先队列的实现原理是二叉堆。
在介绍二叉堆之前,首先需要介绍二叉树和完全二叉树。
二叉树是一个树的结构,每个结点最多有两个子结点,称为左子结点和右子结点。
完全二叉树的性质是,除了层数最大的层以外,其余各层的结点数都达到最大值,且层数最大的层的所有结点都连续集中在最左边。假设完全二叉树有 l l l 层,其中根结点位于第 0 0 0 层,则对于 0 ≤ i < l − 1 0 \le i < l - 1 0≤i<l−1,第 i i i 层有 2 i 2^i 2i 个结点。
二叉堆是一棵完全二叉树,其中的元素按照特定规则排列。常见的例子有小根堆和大根堆。
在二叉堆中添加元素和删除元素时,必须维护二叉堆的性质。以小根堆为例,添加元素和删除元素的操作如下:
考虑以下小根堆。
将 8 8 8 添加到小根堆中,首先将 8 8 8 添加到末尾。
由于 8 8 8 比父结点 15 15 15 小,因此交换,交换后 8 8 8 比父结点 10 10 10 小,因此再次交换,此时 8 8 8 不再比父结点 5 5 5 小,因此停止交换。添加元素操作结束。
在添加 8 8 8 之后,删除元素。删除堆顶元素 5 5 5,将最后一个元素 15 15 15 放置到根结点处。
由于 15 15 15 比两个子结点元素都大,因此和较小的子结点 8 8 8 交换,交换后 15 15 15 比较小的子结点 10 10 10 大,因此和较小的子结点 10 10 10 交换,此时 15 15 15 不再比子结点 30 30 30 大,因此停止交换。删除元素操作结束。
假设二叉堆中的元素个数是 n n n,二叉堆的层数是 l l l。根据上述例子可知,在二叉堆中添加元素和删除元素时,需要维护二叉堆的性质,时间复杂度是 O ( l ) O(l) O(l),由于 O ( l ) = O ( log n ) O(l) = O(\log n) O(l)=O(logn),因此二叉堆的添加元素和删除元素的时间复杂度是 O ( log n ) O(\log n) O(logn)。
不同于栈的后进先出和队列的先进先出,优先队列中的每个元素都有优先级,优先级最高的元素位于队首,也是最先取出的元素。
Java 的 PriorityQueue \texttt{PriorityQueue} PriorityQueue 类是优先队列类,继承自 AbstractQueue \texttt{AbstractQueue} AbstractQueue 抽象类。 PriorityQueue \texttt{PriorityQueue} PriorityQueue 类的实现原理是二叉堆,底层实现是数组,二叉堆满足堆顶元素为优先级最高的元素。创建优先队列的时候可以自定义优先队列中的元素的比较方法,从而自定义优先级。
由于 Java 的优先队列的实现原理是二叉堆,因此优先队列的添加元素和删除元素的时间复杂度是 O ( log n ) O(\log n) O(logn),查看元素的时间复杂度是 O ( 1 ) O(1) O(1),其中 n n n 是优先队列中的元素个数。