「这是我参与2022首次更文挑战的第15天,活动详情查看:2022首次更文挑战」
设计前中后队列 Design Front Middle Back Queue
LeetCode传送门1670. 设计前中后队列
题目
请你设计一个队列,支持在前,中,后三个位置的 push 和 pop 操作。
请你完成 FrontMiddleBack 类:
FrontMiddleBack() 初始化队列。 void pushFront(int val) 将 val 添加到队列的 最前面 。 void pushMiddle(int val) 将 val 添加到队列的 正中间 。 void pushBack(int val) 将 val 添加到队里的 最后面 。 int popFront() 将 最前面 的元素从队列中删除并返回值,如果删除之前队列为空,那么返回 -1 。 int popMiddle() 将 正中间 的元素从队列中删除并返回值,如果删除之前队列为空,那么返回 -1 。 int popBack() 将 最后面 的元素从队列中删除并返回值,如果删除之前队列为空,那么返回 -1 。 请注意当有 两个 中间位置的时候,选择靠前面的位置进行操作。比方说:
将 6 添加到 [1, 2, 3, 4, 5] 的中间位置,结果数组为 [1, 2, 6, 3, 4, 5] 。 从 [1, 2, 3, 4, 5, 6] 的中间位置弹出元素,返回 3 ,数组变为 [1, 2, 4, 5, 6] 。
Design a queue that supports push and pop operations in the front, middle, and back.
Implement the FrontMiddleBack class:
-
FrontMiddleBack()Initializes the queue. -
void
pushFront(int val)Adds val to the front of the queue. -
void
pushMiddle(int val)Addsvalto the middle of the queue. -
void
pushBack(int val)Addsvalto the back of the queue. -
int
popFront()Removes thefrontelement of the queue and returns it. If the queue is empty, return-1. -
int
popMiddle()Removes themiddleelement of the queue and returns it. If the queue is empty, return-1. -
int
popBack()Removes thebackelement of the queue and returns it. If the queue is empty, return-1.
Notice that when there are two middle position choices, the operation is performed on the frontmost middle position choice. For example:
- Pushing 6 into the middle of
[1, 2, 3, 4, 5]results in[1, 2, 6, 3, 4, 5]. - Popping the middle from
[1, 2, 3, 4, 5, 6]returns3and results in[1, 2, 4, 5, 6].
Example:
Input:
["FrontMiddleBackQueue", "pushFront", "pushBack", "pushMiddle", "pushMiddle", "popFront", "popMiddle", "popMiddle", "popBack", "popFront"]
[[], [1], [2], [3], [4], [], [], [], [], []]
Output:
[null, null, null, null, null, 1, 3, 4, 2, -1]
Explanation:
FrontMiddleBackQueue q = new FrontMiddleBackQueue();
q.pushFront(1); // [1]
q.pushBack(2); // [1, 2]
q.pushMiddle(3); // [1, 3, 2]
q.pushMiddle(4); // [1, 4, 3, 2]
q.popFront(); // return 1 -> [4, 3, 2]
q.popMiddle(); // return 3 -> [4, 2]
q.popMiddle(); // return 4 -> [2]
q.popBack(); // return 2 -> []
q.popFront(); // return -1 -> [] (The queue is empty)
Constraints:
- At most 1000 calls will be made to
pushFront,pushMiddle,pushBack,popFront,popMiddle, andpopBack.
思考线
解题思路
如果使用前端数组来实现,不考虑特定数据结构的话,我们可以很容易来实现。我下面放一下我用 JS 数组的实现。
class FrontMiddleBackQueue {
head: number;
size: number;
len: number;
queue: number[];
constructor() {
this.queue = [];
}
pushFront(val: number): void {
this.queue.unshift(val)
}
pushMiddle(val: number): void {
if(this.queue.length === 0) {
this.queue.push(val);
return
}
const middleInd = (this.queue.length) >> 1;
this.queue.splice(middleInd, 0, val)
}
pushBack(val: number): void {
this.queue.push(val)
}
popFront(): number {
console.log(this.queue.length, this.queue)
if (this.queue.length === 0) return -1;
return this.queue.shift() || -1;
}
popMiddle(): number {
if (this.queue.length === 0) return -1;
const middleInd = (this.queue.length - 1) >> 1;
console.log(middleInd, this.queue.length)
return this.queue.splice(middleInd, 1)[0] || -1;
}
popBack(): number {
return this.queue.pop() || -1;
}
}
/**
* Your FrontMiddleBackQueue object will be instantiated and called as such:
* var obj = new FrontMiddleBackQueue()
* obj.pushFront(val)
* obj.pushMiddle(val)
* obj.pushBack(val)
* var param_4 = obj.popFront()
* var param_5 = obj.popMiddle()
* var param_6 = obj.popBack()
*/
我们使用上面的方法虽然能实现,但是觉得索然无味,没有得到任何锻炼。我们在之前的做题中也并没有用链表来实现过队列,那我们这次就用链表来实现该功能。
首先我们要构造出链表类,当然LeetCode是帮我们构造好了的。在这里我直接把单链表代码贴上来
/**
* Definition for singly-linked list.
* class ListNode {
* val: number
* next: ListNode | null
* constructor(val?: number, next?: ListNode | null) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
* }
*/
接下来我们要基于这个ListNode来实现前中后队列
首先为了代码的一致性,我们使用一个dummyHead,这样防止篡改头部时的定位问题。
再看方法的实现
-
我们的
pushFront和popFront是比较简单的,只需要在dummyHead后删除或添加即可,我们不再赘述。 -
关于
popBack和pushBack的实现,我们只需要遍历链表就行了,这个也不再赘述,一会直接看代码。 -
最后是关于
pushMiddle和popMiddle的实现。我们可以在Class内部设计一个
size属性,来记录队列的长度,然后根据长度来判断中间位置即可。然后找到要push或者pop的位置,进行插入或者删除即可。在这里我列出
pushMiddle的代码作为示例pushMiddle(val: number): void { let times = (this.size) >> 1; // 定位中间位置 let cur = this.dummyHead; while (times--) { cur = cur.next; // 走到中间位置的前一个 } const next = cur.next; // 拿到要插入的下一个节点 cur.next = new ListNode(val, next) // 插入元素 this.size++ // 让列表长度记录下来 }
而我们整体的代码就实现如下
class FrontMiddleBackQueue {
size: number
dummyHead: ListNode;
constructor() {
this.size = 0;
this.dummyHead = new ListNode(0)
}
pushFront(val: number): void {
const next = this.dummyHead.next;
this.dummyHead.next = new ListNode(val, next);
this.size++;
}
pushMiddle(val: number): void {
let times = (this.size) >> 1; // 定位中间位置
let cur = this.dummyHead;
while (times--) {
cur = cur.next; // 走到中间位置的前一个
}
const next = cur.next; // 拿到要插入的下一个节点
cur.next = new ListNode(val, next) // 插入元素
this.size++ // 让列表长度记录下来
}
pushBack(val: number): void {
let cur = this.dummyHead;
while (cur.next) {
cur = cur.next;
}
cur.next = new ListNode(val)
this.size++;
}
popFront(): number {
if (this.size === 0) return -1;
const node = this.dummyHead.next;
this.dummyHead.next = this.dummyHead.next.next;
this.size--;
return node.val;
}
popMiddle(): number {
if (this.size === 0) return -1;
let times = this.size - 1 >> 1;
let cur = this.dummyHead;
while (times--) {
cur = cur.next;
}
const node = cur.next;
cur.next = cur.next.next;
this.size--;
return node.val;
}
popBack(): number {
if (this.size === 0) return -1;
let cur = this.dummyHead;
while (cur.next.next) {
cur = cur.next;
}
const node = cur.next;
cur.next = null;
this.size--;
return node.val;
}
}
最后,我发现,我们花了很多的精力来记录链表的长度,仔细思考,其实我们只是在Middle操作时使用了size,而我们可以 用快慢指针来代替size的操作。
在这里我就不演示全部代码了,同样只展示popMiddle作为参考
popMiddle(): number {
let slow = this.dummyHead
let fast = this.dummyHead.next
if (fast === null) return -1
while (fast && fast.next) {
fast = fast.next.next
if (fast) {
slow = slow.next
}
}
const node = slow.next
slow.next = node.next
this.size--;
return node.val
}
时间复杂度
O(n): 其中 n 是链表的长度。
这就是我对本题的解法,如果有疑问或者更好的解答方式,欢迎留言互动。