10.双端队列
链表实现
使用Java内置双向链表实现。
代码和数组实现放在后边。
数组实现
从头部插入:
a:L==0,将加入的值放在末尾k-1位置,L= k-1;
b:L≠≠0,将加入的值放在L-1的位置,L = L–。
从头部弹出:
a:如果L==k-1(L当前在k-1位置),弹出当前L位置的值,L = 0;
b;如果L≠≠k-1,弹出当前L位置的值,L++,(往后走)。
从尾部插入:
a:当R==k-1,将新加入的元素放在0位置,R=0;
b:当R≠≠k-1,将新加入的元素放在R+1位置,R++。
从尾部弹出:
a:R==0,将当前R位置的元素弹出,R=k-1
b:R≠≠0,将当前位置的元素弹出,R–。
代码
// 定义一个名为 CircularDeque 的公共类
public class CircularDeque {
// 定义一个名为 MyCircularDeque1 的内部类,用于实现基于链表的循环双端队列
class MyCircularDeque1 {
// 使用 LinkedList 实现的双端队列
public Deque<Integer> deque = new LinkedList<>();
// 队列的当前大小
public int size;
// 队列的最大容量
public int limit;
// 构造方法,初始化队列的大小和容量
public MyCircularDeque1(int k) {
size = 0; // 初始化队列为空
limit = k; // 设置队列的最大容量
}
// 在队列前端插入一个元素
public boolean insertFront(int value) {
// 如果队列已满,返回 false
if (isFull()) {
return false;
} else {
// 否则,将元素插入队列前端,并增加队列大小
deque.offerFirst(value);
size++;
// 插入成功,返回 true
return true;
}
}
// 在队列后端插入一个元素
public boolean insertLast(int value) {
// 如果队列已满,返回 false
if (isFull()) {
return false;
} else {
// 否则,将元素插入队列后端,并增加队列大小
deque.offerLast(value);
size++;
// 插入成功,返回 true
return true;
}
}
// 删除队列前端的元素
public boolean deleteFront() {
// 如果队列为空,返回 false
if (isEmpty()) {
return false;
} else {
// 否则,删除队列前端的元素,并减少队列大小
size--;
deque.pollFirst(); // 移除并返回队列的第一个元素
// 删除成功,返回 true
return true;
}
}
// 删除队列后端的元素
public boolean deleteLast() {
// 如果队列为空,返回 false
if (isEmpty()) {
return false;
} else {
// 否则,删除队列后端的元素,并减少队列大小
size--;
deque.pollLast(); // 移除并返回队列的最后一个元素
// 删除成功,返回 true
return true;
}
}
// 获取队列前端的元素,不删除
public int getFront() {
// 如果队列为空,返回 -1
if (isEmpty()) {
return -1;
} else {
// 否则,返回队列前端的元素
return deque.peekFirst();
}
}
// 获取队列后端的元素,不删除
public int getRear() {
// 如果队列为空,返回 -1
if (isEmpty()) {
return -1;
} else {
// 否则,返回队列后端的元素
return deque.peekLast();
}
}
// 检查队列是否为空
public boolean isEmpty() {
return size == 0; // 如果队列大小为 0,则队列为空
}
// 检查队列是否已满
public boolean isFull() {
return size == limit; // 如果队列大小等于最大容量,则队列已满
}
}
// 定义一个名为 MyCircularDeque2 的内部类,用于实现基于数组的循环双端队列
class MyCircularDeque2 {
// 使用数组实现的循环双端队列
public int[] deque;
// 队列的左指针
public int l;
// 队列的右指针
public int r;
// 队列的当前大小
public int size;
// 队列的最大容量
public int limit;
// 构造方法,初始化队列的大小、容量和指针
public MyCircularDeque2(int k) {
deque = new int[k]; // 创建一个大小为 k 的数组用于存储队列元素
l = r = size = 0; // 初始化左指针、右指针和队列大小为 0
limit = k; // 设置队列的最大容量
}
// 在队列前端插入一个元素
public boolean insertFront(int value) {
// 如果队列已满,返回 false
if (isFull()) {
return false;
} else {
// 如果队列为空,初始化左右指针并插入元素
if (isEmpty()) {
l = r = 0;
deque[0] = value;
} else {
// 否则,更新左指针并插入元素
l = l == 0 ? (limit - 1) : (l - 1);
deque[l] = value;
}
// 插入成功,增加队列大小并返回 true
size++;
return true;
}
}
// 在队列后端插入一个元素
public boolean insertLast(int value) {
// 如果队列已满,返回 false
if (isFull()) {
return false;
} else {
// 如果队列为空,初始化左右指针并插入元素
if (isEmpty()) {
l = r = 0;
deque[0] = value;
} else {
// 否则,更新右指针并插入元素
r = r == limit - 1 ? 0 : (r + 1);
deque[r] = value;
}
// 插入成功,增加队列大小并返回 true
size++;
return true;
}
}
// 删除队列前端的元素并更新左指针
public boolean deleteFront() {
// 如果队列为空,返回 false
if (isEmpty()) {
return false;
} else {
// 否则,更新左指针
l = (l == limit - 1) ? 0 : (l + 1);
// 删除成功,减少队列大小并返回 true
size--;
// 注意:这里没有实际删除数组中的元素,因为数组的元素会在插入时覆盖
return true;
}
}
// 删除队列后端的元素并更新右指针
public boolean deleteLast() {
// 如果队列为空,返回 false
if (isEmpty()) {
return false;
} else {
// 否则,更新右指针
r = r == 0 ? (limit - 1) : (r - 1);
// 删除成功,减少队列大小并返回 true
size--;
// 注意:这里没有实际删除数组中的元素,因为数组的元素会在插入时覆盖
return true;
}
}
// 获取队列前端的元素,不删除
public int getFront() {
// 如果队列为空,返回 -1
if (isEmpty()) {
return -1;
} else {
// 否则,返回队列前端的元素
return deque[l];
}
}
// 获取队列后端的元素,不删除
public int getRear() {
// 如果队列为空,返回 -1
if (isEmpty()) {
return -1;
} else {
// 否则,返回队列后端的元素
return deque[r];
}
}
// 检查队列是否为空
public boolean isEmpty() {
return size == 0; // 如果队列大小为 0,则队列为空
}
// 检查队列是否已满
public boolean isFull() {
return size == limit; // 如果队列大小等于最大容量,则队列已满
}
}
}
二者区别:
底层数据结构:
MyCircularDeque1 使用 LinkedList 作为底层数据结构。LinkedList 是一个链表,它由节点组成,每个节点包含数据和指向下一个节点的引用。使用链表可以轻松地在任何位置添加或删除元素,因为这些操作不需要移动其他元素。
MyCircularDeque2 使用数组作为底层数据结构。数组是一个固定大小的连续内存块,可以存储相同类型的元素。在数组中插入和删除元素可能需要移动其他元素,但访问元素的速度快。
时间复杂度:
MyCircularDeque1 的插入和删除操作在链表中通常是 O(1) 时间复杂度,因为链表的节点可以在任何位置添加或删除,而不需要重新排列其他节点。
MyCircularDeque2 的插入和删除操作在数组中也是 O(1) 时间复杂度,但这是因为环形队列的特性允许在数组的两端进行操作,而不需要移动其他元素。然而,如果数组需要扩容,这将是一个 O(n)的操作,因为所有元素都需要移动到新的数组。
空间复杂度:
使用 LinkedList 的 MyCircularDeque1 可能需要更多的空间来存储节点和它们的引用,这可能导致更高的内存 开销。
使用数组的 MyCircularDeque2 通常具有更低的内存开销,因为它只需要存储连续的元素,没有额外的引用开销。但是,数组可能需要预留额外的空间以备将来扩容。
实现细节:
MyCircularDeque1 的实现较为简单,因为它依赖于 LinkedList 的内置方法来处理插入和删除操作。
MyCircularDeque2 的实现需要手动管理数组的索引,以及在插入和删除时正确更新这些索引。这需要额外的逻辑来处理环形队列的边界条件。
性能特点:
MyCircularDeque1 在插入和删除操作上具有很好的性能,但是在随机访问时可能比数组慢,因为链表需要遍历节点。
MyCircularDeque2 在随机访问时性能较好,因为可以直接通过索引访问数组中的元素。但是,如果队列经常需要扩容,性能可能会受到影响。
总的来说,两种实现各有优势和适用场景。MyCircularDeque1 更适合于频繁插入和删除的场景,而 MyCircularDeque2
更适合于固定大小或较少扩容需求的场景,并且需要快速随机访问元素。在实际应用中,应根据具体需求和性能要求选择合适的实现。