Leetcode 算法面试题之路(一)
队列 & 栈介绍
- 在数组中,我们可以通过索引访问随机元素。但是,在某些情况下,我们可能想要限制处理顺序。
- 比如我们可以选择两种不同的处理顺序,先入先出和后入先出,分别对应两个不同的线性数据结构,队列和栈。
- 我们将详细介绍每个数据结构的定义,实现和内置函数。 然后,我们将更多地关注这两种数据结构的实际应用。
- 学习的目标
- 了解 FIFO 和 LIFO 处理顺序的原理;
- 实现这两个数据结构;
- 熟悉内置的队列和栈结构;
- 解决基本的队列相关问题,尤其是 BFS;
- 解决基本的栈相关问题;
- 理解当你使用 DFS 和其他递归算法来解决问题时,系统栈是如何帮助你的。
队列:先入先出的数据结构
- 我们将首先介绍先入先出(FIFO)及其在队列中的工作方式。
- 学习的目标:
- 理解 FIFO 和队列的定义;
- 能够自己实现队列;
- 熟悉内置队列结构;
- 使用队列来解决简单的问题。
1. 先入先出的数据结构
- 在 FIFO 数据结构中,将首先处理添加到队列中的第一个元素。
- 如下图所示,队列是典型的 FIFO 数据结构。
- 插入(insert)操作也称作入队(enqueue),新元素始终被添加在队列的末尾。
- 删除(delete)操作也被称为出队(dequeue),你只能移除第一个元素。
2. 实现队列
-
为了实现队列,我们可以使用动态数组和指向队列头部的索引。
- 队列应支持两种操作:入队和出队。
- 入队会向队列追加一个新元素,而出队会删除第一个元素。 所以我们需要一个索引来指出起点。
-
使用数组和单个指针来实现队列:
package data_structure_queue_and_stack; import java.util.ArrayList; import java.util.List; class MyQueue { // 一个存储元素的数组 private List<Integer> data; // 一个指向起点的指针 private int p_start; public MyQueue() { data = new ArrayList<Integer>(); p_start = 0; } // 插入一个元素到队列中,如果成功,返回 true public boolean enQueue(int x) { data.add(x); return true; } // 删除队列中的一个元素,如果成功返回 true public boolean deQueue() { if (isEmpty() == true) { return false; } p_start++; return true; } // 获取队头元素 public int Front() { return data.get(p_start); } // 队列判空 public boolean isEmpty() { return p_start >= data.size(); } }; public class Queue { public static void main(String[] args) { MyQueue q = new MyQueue(); q.enQueue(5); q.enQueue(3); if (q.isEmpty() == false) { System.out.println(q.Front()); // 输出 5 } q.deQueue(); if (q.isEmpty() == false) { System.out.println(q.Front()); // 输出 3 } q.deQueue(); if (q.isEmpty() == false) { System.out.println(q.Front()); // 队空不执行输出 } } }
- 上面的实现很简单,但在某些情况下效率很低。
- 随着起始指针的移动,浪费了越来越多的空间。 当我们有空间限制时,这将是难以接受的。
- 让我们考虑一种情况,即我们只能分配一个最大长度为 5 的数组。
- 当我们只添加少于 5 个元素时,我们的解决方案很有效。
- 例如,如果我们只调用入队函数四次后还想要将元素 10 入队,那么我们可以成功。
- 但是我们不能接受更多的入队请求,这是合理的,因为现在队列已经满了。此时如果我们将一个元素出队呢?
- 实际上,在这种情况下,我们应该能够再接受一个元素。
3. 循环队列
-
此前,我们提供了一种简单但低效的队列实现。更有效的方法是使用循环队列。
-
具体来说,我们可以使用固定大小的数组和两个指针来指示起始位置和结束位置。目的是重用我们之前提到的被浪费的存储。
-
循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
-
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
-
在循环队列中,我们使用一个数组和两个指针(head 和 tail)。head 表示队列的起始位置,tail 表示队列的结束位置。
-
使用数组和两个指针来实现循环队列:
package data_structure_queue_and_stack; class MyCircularQueue { private int[] data; private int head; private int tail; private int size; // 初始化数据结构,设置队列大小为 k public MyCircularQueue(int k) { data = new int[k]; head = -1; tail = -1; size = k; } // 插入一个元素到队列中,如果成功则返回 true public boolean enQueue(int value) { if (isFull() == true) { return false; } if (isEmpty() == true) { head = 0; } tail = (tail + 1) % size; data[tail] = value; return true; } // 从队列中删除一个元素,如果成功则返回 true public boolean deQueue() { if (isEmpty() == true) { return false; } if (head == tail) { head = -1; tail = -1; return true; } head = (head + 1) % size; return true; } // 获取队头元素 public int Front() { if (isEmpty() == true) { return -1; } return data[head]; } // 获取队尾元素 public int Rear() { if (isEmpty() == true) { return -1; } return data[tail]; } // 队列判空 public boolean isEmpty() { return head == -1; } // 队列判满 public boolean isFull() { return ((tail + 1) % size) == head; } }; public class CircularQueue { public static void main(String[] args) { MyCircularQueue circularQueue = new MyCircularQueue(3); // 设置长度为 3 System.out.println(circularQueue.enQueue(1)); // 返回 true System.out.println(circularQueue.enQueue(2)); // 返回 true System.out.println(circularQueue.enQueue(3)); // 返回 true System.out.println(circularQueue.enQueue(4)); // 返回 false System.out.println(circularQueue.Rear()); // 返回 3 System.out.println(circularQueue.isFull()); // 返回 true System.out.println(circularQueue.deQueue()); // 返回 true System.out.println(circularQueue.enQueue(4)); // 返回 true System.out.println(circularQueue.Rear()); // 返回 4 } }
4. 队列的用法
-
大多数流行语言都提供内置的队列库,因此您无需重新发明轮子。如前所述,队列有两个重要的操作,入队 enqueue 和出队 dequeue。此外,我们应该能够获得队列中的第一个元素,因为应该首先处理它。
package data_structure_queue_and_stack; import java.util.LinkedList; import java.util.Queue; public class Common_Queue { public static void main(String[] args) { // 1. 初始化一个队列 Queue<Integer> res = new LinkedList(); // 2. 获取队头元素,如果队列为空则返回 null System.out.println("The first element is: " + res.peek()); // 3. 入队 res.offer(5); res.offer(13); res.offer(8); res.offer(6); // 4. 出队 res.poll(); // 5. 获取队头元素 System.out.println("The first element is: " + res.peek()); // 6. 获取队列长度 System.out.println("The size is: " + res.size()); } /** * 输出结果: * The first element is: null * The first element is: 13 * The size is: 3 */ }