一、项目背景详细介绍
在数据结构与算法的学习中,队列(Queue) 是最基础的一类结构,它采用 先进先出(FIFO) 的存取模式。相比之下,栈(Stack) 是另一种常用的数据结构,它采用 后进先出(LIFO) 的存取方式。
然而,在很多实际应用场景中,既需要从队首插入元素,也需要从队尾插入元素,同时还可能需要从两端删除元素。单纯的队列和栈都无法满足这种需求,于是我们就需要 双端队列(Deque,Double Ended Queue)。
双端队列的特性:
-
允许从 队首 和 队尾 插入元素;
-
允许从 队首 和 队尾 删除元素;
-
可以灵活地模拟 队列(只用一端插入,另一端删除)和 栈(两端操作相同)。
在现代计算机科学与工程中,Deque 有着广泛的应用:
-
任务调度:在操作系统中,任务可能会被插入到任务队列的不同位置,以实现优先级调度;
-
缓存实现:LRU 缓存算法中需要频繁操作队列两端,Deque 是理想的数据结构;
-
滑动窗口算法:在计算最大值/最小值的滑动窗口问题中,Deque 可以高效维护窗口中的候选元素;
-
表达式求值:中缀、后缀表达式的求解可以利用双端队列实现。
因此,实现一个通用的 Deque,不仅能够帮助我们加深对队列和栈的理解,还能直接应用在许多工程项目中。
二、项目需求详细介绍
本项目要求使用 Java 编写一个 通用双端队列(Deque)算法实现,主要需求如下:
-
数据通用化(泛型支持)
-
使用泛型
T
,保证队列可以存储任意数据类型(如Integer
、String
、自定义对象等)。
-
-
基本功能
-
addFirst(data)
:在队首插入元素; -
addLast(data)
:在队尾插入元素; -
removeFirst()
:删除并返回队首元素; -
removeLast()
:删除并返回队尾元素; -
peekFirst()
:查看队首元素但不删除; -
peekLast()
:查看队尾元素但不删除; -
isEmpty()
:判断队列是否为空; -
size()
:获取队列长度。
-
-
存储结构
-
内部使用 双向链表 来实现 Deque(也可以使用动态数组,但链表更直观)。
-
-
测试案例
-
插入一组元素并分别从队首和队尾删除;
-
验证 FIFO、LIFO 行为是否能够模拟;
-
验证不同数据类型存储是否正确。
-
三、相关技术详细介绍
在本项目中,需要掌握以下相关技术:
-
双向链表(Doubly Linked List)
-
每个节点包含三个部分:前驱指针
prev
、存储数据data
、后继指针next
; -
可以从任意方向遍历,方便支持从两端进行插入和删除。
-
-
Java 泛型(Generics)
-
通过定义
Deque<T>
,实现对任意类型的支持。
-
-
时间复杂度分析
-
addFirst()
和addLast()
:O(1); -
removeFirst()
和removeLast()
:O(1); -
peekFirst()
和peekLast()
:O(1)。
-
-
应用场景
-
任务调度(两端插入任务);
-
LRU 缓存(双端操作维护数据顺序);
-
广度优先搜索(BFS)和双端 BFS。
-
四、实现思路详细介绍
-
定义内部节点类 Node<T>
-
包含数据
T data
; -
包含前驱指针
Node<T> prev
和后继指针Node<T> next
。
-
-
定义 Deque 类
-
包含
head
(队首)和tail
(队尾)指针; -
维护一个
size
变量,记录当前元素个数。
-
-
实现基本方法
-
addFirst(data)
:创建新节点并插入到head
前; -
addLast(data)
:创建新节点并插入到tail
后; -
removeFirst()
:删除head
并返回其值; -
removeLast()
:删除tail
并返回其值; -
peekFirst()
:返回head.data
; -
peekLast()
:返回tail.data
。
-
-
辅助方法
-
isEmpty()
:判断size == 0
; -
size()
:返回元素个数。
-
-
测试用例
-
入队、出队操作验证;
-
模拟栈行为(只操作一端);
-
模拟队列行为(前删后加)。
-
五、完整实现代码
// 文件:DequeDemo.java
public class DequeDemo {
// 节点类(双向链表节点)
private static class Node<T> {
T data; // 数据
Node<T> prev; // 前驱指针
Node<T> next; // 后继指针
Node(T data) {
this.data = data;
}
}
// 双端队列类
public static class Deque<T> {
private Node<T> head; // 队首
private Node<T> tail; // 队尾
private int size; // 元素个数
// 构造函数
public Deque() {
head = null;
tail = null;
size = 0;
}
// 在队首插入元素
public void addFirst(T data) {
Node<T> newNode = new Node<>(data);
if (isEmpty()) {
head = tail = newNode;
} else {
newNode.next = head;
head.prev = newNode;
head = newNode;
}
size++;
}
// 在队尾插入元素
public void addLast(T data) {
Node<T> newNode = new Node<>(data);
if (isEmpty()) {
head = tail = newNode;
} else {
tail.next = newNode;
newNode.prev = tail;
tail = newNode;
}
size++;
}
// 删除队首元素
public T removeFirst() {
if (isEmpty()) {
throw new RuntimeException("队列为空,无法删除队首!");
}
T value = head.data;
head = head.next;
if (head != null) {
head.prev = null;
} else {
tail = null; // 队列为空
}
size--;
return value;
}
// 删除队尾元素
public T removeLast() {
if (isEmpty()) {
throw new RuntimeException("队列为空,无法删除队尾!");
}
T value = tail.data;
tail = tail.prev;
if (tail != null) {
tail.next = null;
} else {
head = null; // 队列为空
}
size--;
return value;
}
// 查看队首元素
public T peekFirst() {
if (isEmpty()) {
throw new RuntimeException("队列为空!");
}
return head.data;
}
// 查看队尾元素
public T peekLast() {
if (isEmpty()) {
throw new RuntimeException("队列为空!");
}
return tail.data;
}
// 判断是否为空
public boolean isEmpty() {
return size == 0;
}
// 获取队列长度
public int size() {
return size;
}
}
// 测试方法
public static void main(String[] args) {
Deque<Integer> deque = new Deque<>();
System.out.println("向队首插入: 1, 2");
deque.addFirst(1);
deque.addFirst(2);
System.out.println("向队尾插入: 3, 4");
deque.addLast(3);
deque.addLast(4);
System.out.println("当前队首元素: " + deque.peekFirst());
System.out.println("当前队尾元素: " + deque.peekLast());
System.out.println("队列长度: " + deque.size());
System.out.println("从队首删除: " + deque.removeFirst());
System.out.println("从队尾删除: " + deque.removeLast());
System.out.println("删除后的队首: " + deque.peekFirst());
System.out.println("删除后的队尾: " + deque.peekLast());
System.out.println("队列是否为空: " + deque.isEmpty());
}
}
六、代码详细解读
-
Node<T>
节点类-
保存数据
T data
; -
前驱指针
prev
,后继指针next
; -
用于构建双向链表。
-
-
Deque<T>
类-
head
指向队首节点; -
tail
指向队尾节点; -
size
记录元素个数。
-
-
插入操作
-
addFirst()
:新节点插入到head
前,并更新head
; -
addLast()
:新节点插入到tail
后,并更新tail
。
-
-
删除操作
-
removeFirst()
:删除head
,并将head
移动到下一个节点; -
removeLast()
:删除tail
,并将tail
移动到前一个节点。
-
-
查看操作
-
peekFirst()
返回head.data
; -
peekLast()
返回tail.data
。
-
-
测试结果
-
插入
2, 1, 3, 4
; -
删除后输出正确,符合 双端队列 特性。
-
七、项目详细总结
-
本项目实现了一个基于 双向链表 的 通用双端队列(Deque);
-
支持从队首和队尾进行插入和删除;
-
支持查看队首和队尾元素;
-
支持判空和获取长度;
-
验证了 Deque 能同时模拟栈和队列 的特性;
-
时间复杂度:所有基本操作均为 O(1),性能优秀。
八、项目常见问题及解答
问题1:Deque 和 Queue 有什么区别?
-
Queue 只能在尾部插入,在头部删除;
-
Deque 可以在两端插入和删除,更灵活。
问题2:为什么使用双向链表而不是 ArrayList?
-
如果用
ArrayList
实现,删除队首需要 O(n) 时间; -
双向链表删除和插入只需要 O(1),更高效。
问题3:Deque 可以用来模拟栈吗?
-
可以,只需始终在一端插入和删除即可,行为等同于栈。
问题4:Java 自带的 Deque 实现有哪些?
-
ArrayDeque
和LinkedList
都实现了Deque
接口。
九、扩展方向与性能优化
-
基于数组的循环双端队列
-
使用循环数组避免链表节点的内存开销;
-
插入和删除同样是 O(1)。
-
-
线程安全 Deque
-
在多线程环境中,可以使用
ConcurrentLinkedDeque
; -
适用于生产者-消费者模型。
-
-
应用扩展
-
LRU 缓存:Deque 可以高效维护访问顺序;
-
BFS 双向搜索:Deque 支持从两端扩展搜索;
-
滑动窗口最大值问题。
-