[路飞]_每天刷leetcode_56( 设计前中后队列 Design Front Middle Back Queue)

830 阅读5分钟

「这是我参与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) Adds val to the middle of the queue.

  • void pushBack(int val) Adds val to the back of the queue.

  • int popFront() Removes the front element of the queue and returns it. If the queue is empty, return -1.

  • int popMiddle() Removes the middle element of the queue and returns it. If the queue is empty, return -1.

  • int popBack() Removes the back element 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] returns 3 and 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:

  • 1<=val<=1091 <= val <= 10^9
  • At most 1000 calls will be made to pushFront, pushMiddle, pushBack, popFront, popMiddle, and popBack.

思考线


解题思路

如果使用前端数组来实现,不考虑特定数据结构的话,我们可以很容易来实现。我下面放一下我用 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,这样防止篡改头部时的定位问题。

再看方法的实现

  1. 我们的pushFrontpopFront是比较简单的,只需要在dummyHead后删除或添加即可,我们不再赘述。

  2. 关于popBackpushBack的实现,我们只需要遍历链表就行了,这个也不再赘述,一会直接看代码。

  3. 最后是关于pushMiddlepopMiddle的实现。

    我们可以在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 是链表的长度。

这就是我对本题的解法,如果有疑问或者更好的解答方式,欢迎留言互动。