1. 概述
在计算机科学中,queue是以顺序的方式维护的一组数据集合,在一端添加数据,从另一端移除数据。习惯来说,添加的一端称为尾,移除的一端称为头,就如同生活中的排队买商品。
先定义一个简化的队列接口
public interface Queue<E> {
/**
* 向队列尾插入值
* @param value 待插入值
* @return 插入成功返回 true, 插入失败返回 false
*/
boolean offer(E value);
/**
* 从对列头获取值, 并移除
* @return 如果队列非空返回对头值, 否则返回 null
*/
E poll();
/**
* 从对列头获取值, 不移除
* @return 如果队列非空返回对头值, 否则返回 null
*/
E peek();
/**
* 检查队列是否为空
* @return 空返回 true, 否则返回 false
*/
boolean isEmpty();
/**
* 检查队列是否已满
* @return 满返回 true, 否则返回 false
*/
boolean isFull();
}
2. 链表实现
下面以单向环形带哨兵链表方式来实现队列
package com.itheima.datastructure.Queue;
import java.util.Iterator;
public class LinkedListQueue<E> implements Queue<E>, Iterable<E>{
private static class Node<E> {
E value;
Node<E> next;
public Node(E value, Node<E> next) {
this.value = value;
this.next = next;
}
}
private Node<E> head = new Node<>(null, null);
private Node<E> tail = head;
private int size = 0;
private int capacity = Integer.MAX_VALUE;
{
tail.next = head;
}
public LinkedListQueue() {
}
public LinkedListQueue(int capacity) {
this.capacity = capacity;
}
// 从队尾插入元素
@Override
public boolean offer(E value) {
if(isFull()) {
return false;
}
Node<E> added = new Node<>(value, head);
tail.next = added;
tail = added;
size++;
return true;
}
// 获取并移除队头元素
@Override
public E poll() {
if(isEmpty()) {
return null;
}
Node<E> first = head.next;
head.next = first.next;
if(first == tail) {
// 队列只有一个元素
tail = head;
}
size--;
return first.value;
}
// 获取队头元素
@Override
public E peek() {
if(isEmpty()) {
return null;
}
return head.next.value;
}
// 判断队列是否为空
@Override
public boolean isEmpty() {
return head == tail;
}
// 判断队列是否已满
@Override
public boolean isFull() {
return size == capacity;
}
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
Node<E> p = head.next;
@Override
public boolean hasNext() {
return p != head;
}
@Override
public E next() {
E value = p.value;
p = p.next;
return value;
}
};
}
}
3. 环形数组实现
好处
- 对比普通数组,起点和终点更为自由,不用考虑数据移动
- “环”意味着不会存在【越界】问题
- 数组性能更佳
- 环形数组比较适合实现有界队列、RingBuffer等
下标计算
例如,数组长度是5,当前位置是3,向前走2步,此时下标为(3 + 2)% 5 = 0
- cur当前指针位置
- step前进步数
- length数组长度
- 注意:如果step=1,也就是一次走一步,可以在 ≥length时重置为0即可
判空
- 判断head和tail是否相等即可
判满
- 判断 (tail指针+1) % length 是否等于head
队列满了之后的策略可以根据业务需求决定
- 例如我们要实现的环形队列,满了之后就拒绝入队
方法一:
package com.itheima.datastructure.Queue;
import java.util.Iterator;
public class ArrayQueue<E> implements Queue<E>, Iterable<E>{
private int head = 0;
private int tail = 0;
private final E[] array;
private final int length;
@SuppressWarnings("all")
public ArrayQueue(int capacity) {
length = capacity + 1;
array = (E[]) new Object[length];
}
@Override
public boolean offer(E value) {
if(isFull()) {
return false;
}
array[tail] = value;
tail = (tail + 1) % length;
return true;
}
@Override
public E poll() {
if(isEmpty()) {
return null;
}
E value = array[head];
head = (head + 1) % length;
return value;
}
@Override
public E peek() {
if(isEmpty()) {
return null;
}
return array[head];
}
@Override
public boolean isEmpty() {
return tail == head;
}
@Override
public boolean isFull() {
return (tail + 1) % length == head;
}
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
int p = head;
@Override
public boolean hasNext() {
return p != tail;
}
@Override
public E next() {
E value = array[p];
p = (p + 1) % array.length;
return value;
}
};
}
}
方法二:
package com.itheima.datastructure.Queue;
import java.util.Iterator;
public class ArrayQueue2<E> implements Queue<E>, Iterable<E>{
private int head = 0;
private int tail = 0;
private final E[] array;
private final int capacity;
private int size = 0;
@SuppressWarnings("all")
public ArrayQueue2(int capacity) {
this.capacity = capacity;
array = (E[]) new Object[capacity];
}
@Override
public boolean offer(E value) {
if(isFull()) {
return false;
}
array[tail] = value;
tail = (tail + 1) % capacity;
size++;
return true;
}
@Override
public E poll() {
if(isEmpty()) {
return null;
}
E value = array[head];
head = (head + 1) % capacity;
size--;
return value;
}
@Override
public E peek() {
if(isEmpty()) {
return null;
}
return array[head];
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public boolean isFull() {
return size == capacity;
}
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
int p = head;
int count = 0;
@Override
public boolean hasNext() {
return count < size;
}
@Override
public E next() {
E value = array[p];
p = (p + 1) % capacity;
count++;
return value;
}
};
}
}
方法三:
求模运算:如果除数是2的n次方,那么被除数的后n为即为余数(模)
如果要保留一个数字的二进制形式的后m位(如后4位),则将该数字与(2^m - 1)按位与即可。
按位与运算比求模运算性能更优
- head和tail不断递增,用到索引时,再用它们进行计算,两个问题
- 如何保证head和tail自增超过正整数最大值的正确性 -> Integet.toUnsignedLong()
- 如何让取模运算性能更高
- 让capacity为2的幂
package com.itheima.datastructure.Queue;
import java.util.Iterator;
public class ArrayQueue3<E> implements Queue<E>, Iterable<E>{
private int head = 0;
private int tail = 0;
private final E[] array;
private final int capacity;
public ArrayQueue3(int capacity) {
if((capacity & (capacity - 1)) != 0) { // 8(0000 1000) & 7(0000 0111)
throw new IllegalArgumentException("capacity 必须为2的幂");
}
this.capacity = capacity;
array = (E[]) new Object[this.capacity];
}
@Override
public boolean offer(E value) {
if(isFull()) {
return false;
}
array[tail & (capacity - 1)] = value;
tail++;
return true;
}
@Override
public E poll() {
if(isEmpty()) {
return null;
}
E value = array[head & (capacity - 1)];
head++;
return value;
}
@Override
public E peek() {
if(isEmpty()) {
return null;
}
return array[head & (capacity - 1)];
}
@Override
public boolean isEmpty() {
return tail - head == 0;
}
@Override
public boolean isFull() {
return tail - head == capacity;
}
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
int p = head;
@Override
public boolean hasNext() {
return p != tail;
}
@Override
public E next() {
E value = array[p & (capacity - 1)];
p++;
return value;
}
};
}
}
4. 习题
4.1 二叉树的层序遍历
给你二叉树的根节点 root
,返回其节点值的 层序遍历 。(即逐层地,从左到右访问所有节点)。
示例 1:
输入:root = [3,9,20,null,null,15,7] 输出:[[3],[9,20],[15,7]]
示例 2:
输入:root = [1] 输出:[[1]]
示例 3:
输入:root = [] 输出:[]
提示:
- 树中节点数目在范围
[0, 2000]
内 -1000 <= Node.val <= 1000
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if(root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root); // 根节点入堆
while(!queue.isEmpty()) {
int levelSize = queue.size(); // 当前层的节点个数
List<Integer> currentLevel = new ArrayList<>(); // 存储当前层的节点值
for(int i = 0; i < levelSize; i++) {
TreeNode currentNode = queue.poll(); // 队头出队
currentLevel.add(currentNode.val); // 添加到当前层的列表中
if(currentNode.left != null) { // 左子节点入队
queue.offer(currentNode.left);
}
if(currentNode.right != null) { // 右子节点入堆
queue.offer(currentNode.right);
}
}
result.add(currentLevel); // 将当前层的节点值添加到结果列表
}
return result;
}
}
4.2 设计循环队列
设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
你的实现应该支持如下操作:
MyCircularQueue(k)
: 构造器,设置队列长度为 k 。Front
: 从队首获取元素。如果队列为空,返回 -1 。Rear
: 获取队尾元素。如果队列为空,返回 -1 。enQueue(value)
: 向循环队列插入一个元素。如果成功插入则返回真。deQueue()
: 从循环队列中删除一个元素。如果成功删除则返回真。isEmpty()
: 检查循环队列是否为空。isFull()
: 检查循环队列是否已满。
示例:
MyCircularQueue circularQueue = new MyCircularQueue(3); // 设置长度为 3 circularQueue.enQueue(1); // 返回 true circularQueue.enQueue(2); // 返回 true circularQueue.enQueue(3); // 返回 true circularQueue.enQueue(4); // 返回 false,队列已满 circularQueue.Rear(); // 返回 3 circularQueue.isFull(); // 返回 true circularQueue.deQueue(); // 返回 true circularQueue.enQueue(4); // 返回 true circularQueue.Rear(); // 返回 4
提示:
- 所有的值都在 0 至 1000 的范围内;
- 操作数将在 1 至 1000 的范围内;
- 请不要使用内置的队列库。
解法一:
class MyCircularQueue {
private static class Node {
int value;
Node next;
Node(int value, Node next) {
this.value = value;
this.next = next;
}
}
private final Node head = new Node(-1, null);
private Node tail = head;
private int size = 0;
private int capacity = 0;
{
tail.next = head;
}
public MyCircularQueue(int k) {
this.capacity = k;
}
public boolean enQueue(int value) {
if(isFull()) {
return false;
}
Node added = new Node(value, tail);
tail.next = added;
tail = added;
size++;
return true;
}
public boolean deQueue() {
if(isEmpty()) {
return false;
}
Node first = head.next;
head.next = first.next;
if(first == tail) {
tail = head;
}
size--;
return true;
}
public int Front() {
if(isEmpty()) {
return -1;
}
return head.next.value;
}
public int Rear() {
if(isEmpty()) {
return -1;
}
return tail.value;
}
public boolean isEmpty() {
// return head == tail;
return size == 0;
}
public boolean isFull() {
return size == capacity;
}
}
/**
* Your MyCircularQueue object will be instantiated and called as such:
* MyCircularQueue obj = new MyCircularQueue(k);
* boolean param_1 = obj.enQueue(value);
* boolean param_2 = obj.deQueue();
* int param_3 = obj.Front();
* int param_4 = obj.Rear();
* boolean param_5 = obj.isEmpty();
* boolean param_6 = obj.isFull();
*/