Solving Leetcode Interviews in Seconds with AI: Design Front Middle Back Queue
Introduction
In this blog post, we will explore how to solve the LeetCode problem "1670" using AI. LeetCode is a popular platform for preparing for coding interviews, and with the help of AI tools like Chatmagic, we can generate solutions quickly and efficiently - helping you pass the interviews and get the job offer without having to study for months.
Problem Statement
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 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 <= 109 At most 1000 calls will be made to pushFront, pushMiddle, pushBack, popFront, popMiddle, and popBack.
Explanation
Here's a breakdown of the solution and the Python code:
Approach:
- Maintain two doubly linked lists:
leftandright. Theleftlist stores the first half (or slightly more) elements of the queue, and therightlist stores the remaining elements. - After each operation, balance the sizes of
leftandrightto ensure the "middle" is correctly positioned forpushMiddleandpopMiddle. Specifically,len(left)should either be equal tolen(right)orlen(right) + 1. - Doubly linked lists facilitate O(1) insertion and deletion at any point, essential for efficiency.
- Maintain two doubly linked lists:
Complexity:
- Runtime: O(1) for all operations.
- Space: O(N), where N is the number of elements in the queue.
Code
class Node:
def __init__(self, val):
self.val = val
self.next = None
self.prev = None
class DoublyLinkedList:
def __init__(self):
self.head = None
self.tail = None
self.size = 0
def append(self, val):
new_node = Node(val)
if not self.head:
self.head = new_node
self.tail = new_node
else:
new_node.prev = self.tail
self.tail.next = new_node
self.tail = new_node
self.size += 1
def prepend(self, val):
new_node = Node(val)
if not self.head:
self.head = new_node
self.tail = new_node
else:
new_node.next = self.head
self.head.prev = new_node
self.head = new_node
self.size += 1
def insert_middle(self, val):
if not self.head:
self.append(val)
return
mid_index = (self.size + 1) // 2 # Integer division gives the correct middle
if mid_index == 1:
self.prepend(val)
return
current = self.head
for _ in range(mid_index - 1):
current = current.next
new_node = Node(val)
new_node.next = current
new_node.prev = current.prev
if current.prev:
current.prev.next = new_node
else:
self.head = new_node
current.prev = new_node
self.size += 1
def pop_front(self):
if not self.head:
return -1
val = self.head.val
if self.head == self.tail:
self.head = None
self.tail = None
else:
self.head = self.head.next
self.head.prev = None
self.size -= 1
return val
def pop_back(self):
if not self.head:
return -1
val = self.tail.val
if self.head == self.tail:
self.head = None
self.tail = None
else:
self.tail = self.tail.prev
self.tail.next = None
self.size -= 1
return val
def pop_middle(self):
if not self.head:
return -1
mid_index = (self.size + 1) // 2 #Integer division as required
if self.size == 1:
val = self.head.val
self.head = None
self.tail = None
self.size = 0
return val
current = self.head
for _ in range(mid_index-1):
current = current.next
val = current.val
if current.prev:
current.prev.next = current.next
else:
self.head = current.next
if current.next:
current.next.prev = current.prev
else:
self.tail = current.prev
self.size -= 1
return val
def get_size(self):
return self.size
class FrontMiddleBackQueue:
def __init__(self):
self.left = DoublyLinkedList()
self.right = DoublyLinkedList()
def pushFront(self, val: int) -> None:
self.left.prepend(val)
self.balance()
def pushMiddle(self, val: int) -> None:
if self.left.get_size() == self.right.get_size():
self.left.append(val)
else:
self.right.prepend(val)
self.balance()
def pushBack(self, val: int) -> None:
self.right.append(val)
self.balance()
def popFront(self) -> int:
if not self.left.head and not self.right.head:
return -1
val = self.left.pop_front()
if val == -1:
val = self.right.pop_front()
self.balance()
return val
def popMiddle(self) -> int:
if not self.left.head and not self.right.head:
return -1
if self.left.get_size() == self.right.get_size():
val = self.left.pop_back()
else:
val = self.left.pop_back()
self.balance()
return val
def popBack(self) -> int:
val = self.right.pop_back()
if val == -1:
val = self.left.pop_back()
self.balance()
return val
def balance(self):
while self.left.get_size() < self.right.get_size():
self.left.append(self.right.pop_front())
while self.left.get_size() > self.right.get_size() + 1:
self.right.prepend(self.left.pop_back())