Design your implementation of the circular double-ended queue (deque).
Implement the MyCircularDeque
class:
MyCircularDeque(int k)
Initializes the deque with a maximum size of k
.boolean insertFront()
Adds an item at the front of Deque. Returns true
if the operation is successful, or false
otherwise.boolean insertLast()
Adds an item at the rear of Deque. Returns true
if the operation is successful, or false
otherwise.boolean deleteFront()
Deletes an item from the front of Deque. Returns true
if the operation is successful, or false
otherwise.boolean deleteLast()
Deletes an item from the rear of Deque. Returns true
if the operation is successful, or false
otherwise.int getFront()
Returns the front item from the Deque. Returns -1
if the deque is empty.int getRear()
Returns the last item from Deque. Returns -1
if the deque is empty.boolean isEmpty()
Returns true
if the deque is empty, or false
otherwise.boolean isFull()
Returns true
if the deque is full, or false
otherwise.For example:
MyCircularDeque myCircularDeque = new MyCircularDeque(3);
myCircularDeque.insertLast(1); // return True
myCircularDeque.insertLast(2); // return True
myCircularDeque.insertFront(3); // return True
myCircularDeque.insertFront(4); // return False, the queue is full.
myCircularDeque.getRear(); // return 2
myCircularDeque.isFull(); // return True
myCircularDeque.deleteLast(); // return True
myCircularDeque.insertFront(4); // return True
myCircularDeque.getFront(); // return 4
What would be an efficient way to implement this data structure and what are the time and space complexities?
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 method for building a circular deque involves directly simulating all operations. For each new element, we attempt to insert it at all possible positions, and for removals, we simply shift elements to fill the gap.
Here's how the algorithm would work step-by-step:
class MyCircularDeque:
def __init__(self, max_size):
self.deque = [None] * max_size
self.max_size = max_size
self.current_size = 0
def insertFront(self, value):
if self.isFull():
return False
# Shift elements to make space at the front.
for index in range(self.current_size, 0, -1):
self.deque[index] = self.deque[index - 1]
self.deque[0] = value
self.current_size += 1
return True
def insertLast(self, value):
if self.isFull():
return False
self.deque[self.current_size] = value
self.current_size += 1
return True
def deleteFront(self):
if self.isEmpty():
return False
# Shift elements to fill the gap at the front.
for index in range(0, self.current_size - 1):
self.deque[index] = self.deque[index + 1]
self.deque[self.current_size - 1] = None
self.current_size -= 1
return True
def deleteLast(self):
if self.isEmpty():
return False
# Removing the last element.
self.deque[self.current_size - 1] = None
self.current_size -= 1
return True
def getFront(self):
if self.isEmpty():
return -1
return self.deque[0]
def getRear(self):
if self.isEmpty():
return -1
# Return last element.
return self.deque[self.current_size - 1]
def isEmpty(self):
return self.current_size == 0
def isFull(self):
# Checking if the container is already full.
return self.current_size == self.max_size
We'll create a data structure that acts like a line of people waiting, where people can join at either end. The key is to use a fixed amount of space, and remember the location of the first and last person in line, adjusting these locations as people join and leave.
Here's how the algorithm would work step-by-step:
class CircularDeque:
def __init__(self, circular_deque_capacity):
self.circular_deque_capacity = circular_deque_capacity
self.circular_deque = [None] * circular_deque_capacity
self.front_index = 0
self.back_index = 0
self.current_size = 0
def insertFront(self, value):
if self.isFull():
return False
# Move front back, looping if necessary
self.front_index = (self.front_index - 1) % self.circular_deque_capacity
self.circular_deque[self.front_index] = value
self.current_size += 1
return True
def insertLast(self, value):
if self.isFull():
return False
self.circular_deque[self.back_index] = value
self.back_index = (self.back_index + 1) % self.circular_deque_capacity
self.current_size += 1
return True
def deleteFront(self):
if self.isEmpty():
return False
self.circular_deque[self.front_index] = None
# Move front forward, looping if necessary
self.front_index = (self.front_index + 1) % self.circular_deque_capacity
self.current_size -= 1
return True
def deleteLast(self):
if self.isEmpty():
return False
# Move back backward, looping if necessary
self.back_index = (self.back_index - 1) % self.circular_deque_capacity
self.circular_deque[self.back_index] = None
self.current_size -= 1
return True
def getFront(self):
if self.isEmpty():
return -1
return self.circular_deque[self.front_index]
def getRear(self):
if self.isEmpty():
return -1
rear_index = (self.back_index - 1) % self.circular_deque_capacity
return self.circular_deque[rear_index]
def isEmpty(self):
return self.current_size == 0
def isFull(self):
# Check if queue is full.
return self.current_size == self.circular_deque_capacity
Case | How to Handle |
---|---|
Deque capacity is 0 | The constructor should probably throw an exception or the operations return an error code as no elements can be stored. |
Adding elements beyond deque's capacity | Insertion operations should either return an error code, throw an exception, or silently do nothing, depending on the design requirements. |
Removing from an empty deque | Removal operations should return an error code, throw an exception, or return a specific value (e.g., null) to indicate an empty deque. |
Operations called in rapid succession (e.g., many inserts then many deletes) | Check for potential race conditions if the Deque is intended for multi-threaded use, and ensure proper locking mechanisms are in place. |
Integer overflow when calculating indices (e.g., head or tail) | Use appropriate data types (e.g., long) or modulo operations to prevent integer overflow when performing index calculations. |
Testing with very large capacity deque (close to max integer value) | Check memory allocation to ensure there are no out-of-memory errors when initializing a very large deque. |
Alternating addFront and addRear operations to fill the queue | This tests that both front and rear pointers are correctly incremented/decremented in the wrap-around fashion. |
Deleting all elements then adding elements again | This ensures the pointers are reset properly, and the insertion works fine after the Deque has been emptied out. |