队列是一种典型的先进先出(FIFO)数据结构,就像现实生活中的排队场景。在Java中实现队列,我们需要关注几个核心操作:入队(offer)、出队(pop)、查看队首(peek)、判空(isEmpty)和获取大小(size)。
我选择使用双向链表来实现队列,主要基于以下考虑:
提示:虽然Java标准库已经提供了Queue接口和多种实现,但手动实现能帮助我们深入理解数据结构原理。
java复制public static class ListNode {
ListNode next;
ListNode prev;
int val;
public ListNode(int val) {
this.val = val;
}
}
这个内部类定义了队列节点的结构:
next指向下一个节点prev指向前一个节点val存储节点值双向链表的设计让节点可以向前和向后遍历,这在队列操作中非常有用。
java复制private ListNode first;
private ListNode last;
private int size;
这三个成员变量构成了队列的核心状态:
first始终指向队列头部(最早入队的元素)last始终指向队列尾部(最新入队的元素)size记录当前队列长度维护size变量可以快速获取队列长度,避免了遍历链表计算的开销。
java复制public boolean isEmpty() {
return first == null;
}
判空逻辑非常简单,只需检查first是否为null:
first和last都为null注意:不能通过
size==0来判断,因为size可能由于并发操作而不准确。
java复制public void offer(int val) {
ListNode newNode = new ListNode(val);
if (first == null) {
first = newNode;
last = newNode;
} else {
last.next = newNode;
newNode.prev = last;
}
last = newNode;
size++;
}
入队操作步骤解析:
关键点:
java复制public int peek() {
if (isEmpty()) {
return -1;
}
return first.val;
}
peek操作特点:
java复制public int pop() {
if (isEmpty()) {
return -1;
}
int x = first.val;
if (first.next == null) {
first = null;
last = null;
} else {
first = first.next;
first.prev = null;
}
size--;
return x;
}
出队操作详细步骤:
实操技巧:出队时要特别注意处理队列只剩一个元素的情况,避免出现指针混乱。
java复制public int size() {
return size;
}
size操作的实现非常简单:
在出队操作中,原作者提到first.prev.next = null可以省略。这是正确的,因为:
first = first.next执行后,原first节点已经不再被引用当前实现不是线程安全的,在多线程环境下可能出现问题。如果需要线程安全版本,可以考虑:
synchronized关键字修饰方法ReentrantLock进行更细粒度的控制ConcurrentLinkedQueue等现成的线程安全队列当前实现在空队列上执行pop或peek时返回-1,更好的做法是:
java复制public int pop() {
if (isEmpty()) {
throw new NoSuchElementException("Queue is empty");
}
// 其余逻辑不变
}
这样能更明确地表达错误情况,符合Java集合框架的惯例。
完整的队列实现应该包含测试用例。以下是简单的测试示例:
java复制public static void main(String[] args) {
MyQueue queue = new MyQueue();
// 测试入队
queue.offer(1);
queue.offer(2);
queue.offer(3);
System.out.println(queue.size()); // 输出3
// 测试peek
System.out.println(queue.peek()); // 输出1
// 测试出队
System.out.println(queue.pop()); // 输出1
System.out.println(queue.pop()); // 输出2
// 测试判空
System.out.println(queue.isEmpty()); // 输出false
// 清空队列
System.out.println(queue.pop()); // 输出3
System.out.println(queue.isEmpty()); // 输出true
}
| 操作 | 链表实现 | 数组实现 |
|---|---|---|
| offer | O(1) | 平均O(1),最坏O(n)(扩容时) |
| pop | O(1) | O(1) |
| peek | O(1) | O(1) |
| size | O(1) | O(1) |
链表实现的内存开销略高于数组实现,因为:
链表实现的队列更适合:
数组实现的队列更适合:
基础的队列实现可以进一步扩展:
java复制public class MyQueue<T> {
private static class ListNode<T> {
ListNode<T> next;
ListNode<T> prev;
T val;
// 构造方法...
}
// 其余实现...
}
java复制public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
private ListNode current = first;
public boolean hasNext() {
return current != null;
}
public Integer next() {
int val = current.val;
current = current.next;
return val;
}
};
}
java复制public void offerAll(int... values) {
for (int val : values) {
offer(val);
}
}
常见于:
调试方法:
在Java中不太常见,但要注意:
多线程环境下可能出现:
Java提供了多种队列实现:
LinkedList:基于链表的双向队列ArrayDeque:基于数组的双端队列PriorityQueue:优先级队列我们的实现与LinkedList类似,但更简单,适合学习目的。生产环境建议使用标准库实现。
队列在编程中有广泛应用:
理解队列的实现原理有助于在这些场景中做出更好的设计决策。