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:
6 into the middle of [1, 2, 3, 4, 5] results in [1, 2, 6, 3, 4, 5].[1, 2, 3, 4, 5, 6] returns 3 and results in [1, 2, 4, 5, 6].Example 1:
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 <= 1091000 calls will be made to pushFront, pushMiddle, pushBack, popFront, popMiddle, and popBack.When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:
The brute force approach is to store all the numbers in a single, simple line. Whenever we need to add or remove an item from the front or the middle, we manually shift all the other numbers around to make space or close a gap.
Here's how the algorithm would work step-by-step:
class FrontMiddleBackQueue:
def __init__(self):
# A single list is used to represent the queue, as described by the brute-force approach.
self.queue_data = []
def pushFront(self, value_to_add):
# Inserting at index 0 forces all other elements to shift right, creating space at the front.
self.queue_data.insert(0, value_to_add)
def pushMiddle(self, value_to_add):
# The insertion point is len // 2, which places the new element just before the original middle.
middle_index = len(self.queue_data) // 2
self.queue_data.insert(middle_index, value_to_add)
def pushBack(self, value_to_add):
self.queue_data.append(value_to_add)
def popFront(self):
if not self.queue_data:
return -1
return self.queue_data.pop(0)
def popMiddle(self):
if not self.queue_data:
return -1
# The middle element to pop is at index (n-1)//2 to correctly handle both even and odd lengths.
number_of_elements = len(self.queue_data)
middle_index = (number_of_elements - 1) // 2
return self.queue_data.pop(middle_index)
def popBack(self):
if not self.queue_data:
return -1
return self.queue_data.pop()The core strategy is to split the entire collection into two smaller, manageable halves: a front part and a back part. By keeping these two parts nearly the same size, we ensure that the front, back, and the 'middle' are always immediately accessible, avoiding the need to search through the whole collection.
Here's how the algorithm would work step-by-step:
from collections import deque
class FrontMiddleBackQueue:
def __init__(self):
self.left_half = deque()
self.right_half = deque()
def _rebalance(self):
# This ensures the left half has at most one more element than the right, our key invariant.
if len(self.left_half) > len(self.right_half) + 1:
element_to_move = self.left_half.pop()
self.right_half.appendleft(element_to_move)
elif len(self.left_half) < len(self.right_half):
element_to_move = self.right_half.popleft()
self.left_half.append(element_to_move)
def pushFront(self, new_value):
self.left_half.appendleft(new_value)
self._rebalance()
def pushMiddle(self, new_value):
# If the left side is larger, we must move an element to the right to make room for the new middle.
if len(self.left_half) > len(self.right_half):
element_to_move = self.left_half.pop()
self.right_half.appendleft(element_to_move)
self.left_half.append(new_value)
def pushBack(self, new_value):
self.right_half.append(new_value)
self._rebalance()
def popFront(self):
if not self.left_half:
return -1
popped_value = self.left_half.popleft()
# After any removal, we must rebalance to maintain quick access to the middle element.
self._rebalance()
return popped_value
def popMiddle(self):
if not self.left_half:
return -1
popped_value = self.left_half.pop()
self._rebalance()
return popped_value
def popBack(self):
# The last element could be in the left half if the queue is small, so we must check the right half first.
if not self.right_half:
if not self.left_half:
return -1
return self.popMiddle()
popped_value = self.right_half.pop()
self._rebalance()
return popped_value| Case | How to Handle |
|---|---|
| Calling any pop operation on a completely empty queue. | The implementation must check if the queue is empty at the beginning of each pop method and return -1 as required. |
| Calling `popBack` on a queue containing just one element. | The code must correctly retrieve the element from the left deque since the right deque will be empty in this specific scenario. |
| Using `pushMiddle` on a queue with an odd number of elements. | The implementation must shift the left deque's last element to the right deque before pushing the new middle element to the left deque. |
| A pop operation that unbalances the deques, for example making the right deque larger than the left. | A rebalancing function must be called to move an element from the right deque to the left deque to restore the size invariant. |
| A `popFront` operation that makes the left deque more than one element smaller than the right one. | The solution must rebalance by moving an element from the right deque's front to the left deque's back to maintain correct structure. |
| Popping the middle element from a queue with an even number of elements. | The solution must correctly pop the last element of the left deque, which corresponds to the frontmost middle position. |
| A sequence of operations that grows the queue and then shrinks it back to empty. | The state of the two internal deques must be managed correctly to handle transitions from empty, to non-empty, and back to empty again. |
| The number of operations is large, approaching the specified maximum of 1000. | The two-deque approach provides O(1) time complexity for all operations, ensuring the solution remains performant and avoids a timeout. |