[Medium] 1670. Design Front Middle Back Queue
Design a queue that supports push and pop operations in the front, middle, and back.
Implement the FrontMiddleBackQueue class:
FrontMiddleBackQueue()Initializes the queue.void pushFront(int val)Addsvalto 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 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
6into 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].
Examples
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 <= 10^9- At most
1000calls will be made topushFront,pushMiddle,pushBack,popFront,popMiddle, andpopBack.
Thinking Process
- Two Deques Pattern: Split queue into two halves for efficient middle access
- Identify required operations and their frequency (get/put/insert).
- Combine data structures: hash map + list, heap + map, trie + DFS.
- Amortized O(1) often needs lazy cleanup or doubly-linked lists.
Common Approaches
Typical techniques for this pattern:
| Approach | Time | Space | Notes |
|---|---|---|---|
| Hash map + list (this problem) | O(1) avg | O(n) | LRU cache pattern |
| Heap + hash map | O(log n) | O(n) | LFU, time-based store |
| Trie (prefix tree) | O(m) | O(nm) | Word search, autocomplete |
| Deque / circular buffer | O(1) | O(n) | Queue with fixed capacity |
Solution
Solution: Two Deques with Rebalancing
class FrontMiddleBackQueue {
public:
FrontMiddleBackQueue() {
}
void pushFront(int val) {
front_cache.push_front(val);
rebalance();
}
void pushMiddle(int val) {
front_cache.push_back(val);
rebalance();
}
void pushBack(int val) {
back_cache.push_back(val);
rebalance();
}
int popFront() {
if(front_cache.empty() && back_cache.empty()) return -1;
int rtn;
if(front_cache.empty()) {
rtn = back_cache.front();
back_cache.pop_front();
} else {
rtn = front_cache.front();
front_cache.pop_front();
rebalance();
}
return rtn;
}
int popMiddle() {
if(front_cache.empty() && back_cache.empty()) return -1;
int rtn;
if(front_cache.size() == back_cache.size()) {
rtn = front_cache.back();
front_cache.pop_back();
} else {
rtn = back_cache.front();
back_cache.pop_front();
}
return rtn;
}
int popBack() {
if(front_cache.empty() && back_cache.empty()) return -1;
int rtn = back_cache.back();
back_cache.pop_back();
rebalance();
return rtn;
}
private:
deque<int> front_cache, back_cache;
void rebalance() {
while(front_cache.size() > back_cache.size()) {
back_cache.push_front(front_cache.back());
front_cache.pop_back();
}
while(back_cache.size() > front_cache.size() + 1) {
front_cache.push_back(back_cache.front());
back_cache.pop_front();
}
}
};
Solution Explanation
Approach: Hash map + list (this problem)
Key idea: 1. Two Deques Pattern: Split queue into two halves for efficient middle access
How the code works:
- Two Deques Pattern: Split queue into two halves for efficient middle access
- Identify required operations and their frequency (get/put/insert).
- Combine data structures: hash map + list, heap + map, trie + DFS.
- Amortized O(1) often needs lazy cleanup or doubly-linked lists.
Walkthrough — input ["FrontMiddleBackQueue", "pushFront", "pushBack", "pushMiddle", "pushMiddle", "popFront", "popMiddle", "popMiddle", "popBack", "popFront"], expected output [null, null, null, null, null, 1, 3, 4, 2, -1]:
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)
Algorithm Explanation:
- pushFront (Lines 7-10):
- Add
valto front offront_cache - Rebalance to maintain invariant
- Add
- pushMiddle (Lines 12-15):
- Add
valto back offront_cache(becomes new middle) - Rebalance to maintain invariant
- Add
- pushBack (Lines 17-20):
- Add
valto back ofback_cache - Rebalance to maintain invariant
- Add
- popFront (Lines 22-33):
- If both empty, return
-1 - If
front_cacheempty, pop fromback_cache - Otherwise, pop from
front_cacheand rebalance
- If both empty, return
- popMiddle (Lines 35-45):
- If both empty, return
-1 - If sizes equal: pop from
front_cache.back() - Otherwise: pop from
back_cache.front() - No rebalance needed (sizes become balanced after removal)
- If both empty, return
- popBack (Lines 47-53):
- If both empty, return
-1 - Pop from
back_cache.back() - Rebalance to maintain invariant
- If both empty, return
- rebalance (Lines 57-65):
- First loop: If
front_cache.size() > back_cache.size(), move last element fromfront_cacheto front ofback_cache - Second loop: If
back_cache.size() > front_cache.size() + 1, move first element fromback_cacheto back offront_cache - Maintains:
front_cache.size() <= back_cache.size() <= front_cache.size() + 1
- First loop: If
Why This Works:
- Two Deques: Split queue into two halves for efficient middle access
- Balance Invariant: Ensures middle element is always accessible in O(1)
- Rebalancing: Maintains invariant after each modification
- Middle Definition: When sizes equal, middle is
front_cache.back(); whenback_cacheis larger, middle isback_cache.front()
Example Walkthrough:
Operations: pushFront(1), pushBack(2), pushMiddle(3), pushMiddle(4), popFront(), popMiddle(), popMiddle(), popBack()
Initial: front_cache = [], back_cache = []
pushFront(1):
front_cache = [1], back_cache = []
Rebalance: sizes equal (1, 0) → move 1 to back_cache
front_cache = [], back_cache = [1]
pushBack(2):
front_cache = [], back_cache = [1, 2]
Rebalance: back_cache too large (0, 2) → move 1 to front_cache
front_cache = [1], back_cache = [2]
pushMiddle(3):
front_cache = [1, 3], back_cache = [2]
Rebalance: sizes equal (2, 1) → OK
front_cache = [1, 3], back_cache = [2]
pushMiddle(4):
front_cache = [1, 3, 4], back_cache = [2]
Rebalance: front_cache too large (3, 1) → move 4 to back_cache
front_cache = [1, 3], back_cache = [4, 2]
State: front_cache = [1, 3], back_cache = [4, 2]
Queue: [1, 3, 4, 2]
popFront():
front_cache not empty → pop 1
front_cache = [3], back_cache = [4, 2]
Rebalance: back_cache too large (1, 2) → move 4 to front_cache
front_cache = [3, 4], back_cache = [2]
Return: 1
popMiddle():
Sizes: front_cache.size()=2, back_cache.size()=1
Sizes equal → pop from front_cache.back() = 4
front_cache = [3], back_cache = [2]
Return: 4
popMiddle():
Sizes: front_cache.size()=1, back_cache.size()=1
Sizes equal → pop from front_cache.back() = 3
front_cache = [], back_cache = [2]
Return: 3
popBack():
Pop from back_cache.back() = 2
front_cache = [], back_cache = []
Rebalance: OK
Return: 2
Complexity Analysis:
- Time Complexity: O(1) amortized per operation
- Deque operations (push/pop front/back) are O(1)
- Rebalancing is O(1) amortized (each element moved at most once)
- Space Complexity: O(n) where n is number of elements in queue
- Two deques store all elements
Common Mistakes
- Two deques store all elements
- Empty queue: All pop operations return
-1 - Single element: After one push, one pop returns that element
- Two elements: Middle is the first element (frontmost middle)
- Many operations: Rebalancing maintains efficiency
-
Alternating operations: Balance maintained correctly
- Wrong middle calculation: Not handling the case when sizes are equal vs unequal
- Missing rebalance: Forgetting to rebalance after push/pop operations
- Wrong rebalance logic: Incorrectly moving elements between deques
- Empty check: Not checking if both deques are empty before popping
- Pop from wrong deque: Popping from
front_cachewhen it’s empty inpopFront()
Related Problems
- LC 641: Design Circular Deque - Design a circular deque
- LC 622: Design Circular Queue - Design a circular queue
- LC 232: Implement Queue using Stacks - Queue with stacks
- LC 225: Implement Stack using Queues - Stack with queues
Key Takeaways
- Two Deques Pattern: Split queue into two halves for efficient middle access
- Balance Invariant:
front_cache.size() <= back_cache.size() <= front_cache.size() + 1 - Middle Element: Always accessible in O(1) due to invariant
- Rebalancing: Maintains invariant after each modification
- Push Middle: Add to end of
front_cache(becomes new middle)
References
- LC 1670: Design Front Middle Back Queue on LeetCode
- LeetCode Discuss — LC 1670: Design Front Middle Back Queue
- LeetCode Editorial (may require premium)