Design Circular Deque
Description
Design a double-ended queue, a deque, that holds at most k values and can add and remove at both ends. Implement MyCircularDeque:
MyCircularDeque(int k)starts an empty deque with room forkvalues.insertFront(int value)andinsertLast(int value)add a value at the front or the back. They returntrue, orfalseif the deque is already full.deleteFront()anddeleteLast()remove the front or the back value. They returntrue, orfalseif the deque is empty.getFront()andgetRear()return the front or the back value, or-1if the deque is empty.isEmpty()andisFull()say whether the deque holds no values, orkof them.
Examples
Example 1
Input:
["MyCircularDeque", "insertLast", "insertLast", "insertFront", "insertFront", "getRear", "isFull", "deleteLast", "insertFront", "getFront"]
[[3], [1], [2], [3], [4], [], [], [], [4], []]
Output: [null, true, true, true, false, 2, true, true, true, 4]
Explanation: Room for 3: 1 and 2 go in at the back and 3 at the front, so the deque is 3, 1, 2 and full; inserting 4 is refused. The rear is 2. Deleting it leaves room, and 4 goes in at the front: 4, 3, 1.
Example 2
Input:
["MyCircularDeque", "insertFront", "getFront", "getRear", "insertLast", "deleteFront", "isEmpty", "getRear"]
[[1], [7], [], [], [8], [], [], []]
Output: [null, true, 7, 7, false, true, true, -1]
Explanation: With room for one value, the front and the rear are the same value. Once it's removed, the deque is empty and the rear reads −1.
Example 3
Input:
["MyCircularDeque", "deleteFront", "deleteLast", "getFront", "isEmpty", "insertLast", "isFull"]
[[2], [], [], [], [], [5], []]
Output: [null, false, false, -1, true, true, false]
Explanation: Deleting from an empty deque returns false, and reading it returns −1.
Constraints
- 1 ≤ k ≤ 1000
- 0 ≤ value ≤ 1000
- At most 2000 calls will be made to
insertFront,insertLast,deleteFront,deleteLast,getFront,getRear,isEmptyandisFull
Editorial
Brute Force
Intuition
Keep the values in an ordinary list, front first, and check its length against k. The back is easy: append and pop at the end. The front works too, by inserting at index 0 and removing index 0, but each of those moves every other value one place.
Step-by-Step Explanation
Let's trace Example 1, with room for 3:
insertLast(1): 1 goes on the end, and nothing else moves.insertLast(2): 2 goes on the end, and nothing else moves.insertFront(3): 3 goes in at index 0, and the 2 values after it each move one place.insertFront(4)returns false, because the list already holds 3.getRear()returns 2.isFull()returns true.deleteLast(): the last value comes out, and nothing else moves.insertFront(4): 4 goes in at index 0, and the 2 values after it each move one place.getFront()returns 4.
A List, Front First — Inserts and deletes at the front shift every value behind them.
Algorithm
- Keep a list
items, front first, and the capacityk. insertFrontandinsertLast: refuse iflen(items) == k; otherwise insert at index 0 or append.deleteFrontanddeleteLast: refuse if the list is empty; otherwise remove index 0 or the last value.getFrontandgetRearread the ends, or return −1;isEmptyandisFullcompare the length with 0 andk.
Code
class MyCircularDeque {
int k;
vector<int> items; // front first
public:
MyCircularDeque(int k) : k(k) {}
bool insertFront(int value) {
if ((int)items.size() == k) return false;
items.insert(items.begin(), value); // every value moves one place right
return true;
}
bool insertLast(int value) {
if ((int)items.size() == k) return false;
items.push_back(value);
return true;
}
bool deleteFront() {
if (items.empty()) return false;
items.erase(items.begin()); // every value moves one place left
return true;
}
bool deleteLast() {
if (items.empty()) return false;
items.pop_back();
return true;
}
int getFront() { return items.empty() ? -1 : items.front(); }
int getRear() { return items.empty() ? -1 : items.back(); }
bool isEmpty() { return items.empty(); }
bool isFull() { return (int)items.size() == k; }
};
class MyCircularDeque:
def __init__(self, k: int):
self.k = k
self.items = [] # front first
def insertFront(self, value: int) -> bool:
if len(self.items) == self.k:
return False
self.items.insert(0, value) # every value moves one place right
return True
def insertLast(self, value: int) -> bool:
if len(self.items) == self.k:
return False
self.items.append(value)
return True
def deleteFront(self) -> bool:
if not self.items:
return False
self.items.pop(0) # every value moves one place left
return True
def deleteLast(self) -> bool:
if not self.items:
return False
self.items.pop()
return True
def getFront(self) -> int:
return self.items[0] if self.items else -1
def getRear(self) -> int:
return self.items[-1] if self.items else -1
def isEmpty(self) -> bool:
return not self.items
def isFull(self) -> bool:
return len(self.items) == self.k
class MyCircularDeque {
private final int k;
private final List<Integer> items = new ArrayList<>(); // front first
public MyCircularDeque(int k) {
this.k = k;
}
public boolean insertFront(int value) {
if (items.size() == k) return false;
items.add(0, value); // every value moves one place right
return true;
}
public boolean insertLast(int value) {
if (items.size() == k) return false;
items.add(value);
return true;
}
public boolean deleteFront() {
if (items.isEmpty()) return false;
items.remove(0); // every value moves one place left
return true;
}
public boolean deleteLast() {
if (items.isEmpty()) return false;
items.remove(items.size() - 1);
return true;
}
public int getFront() { return items.isEmpty() ? -1 : items.get(0); }
public int getRear() { return items.isEmpty() ? -1 : items.get(items.size() - 1); }
public boolean isEmpty() { return items.isEmpty(); }
public boolean isFull() { return items.size() == k; }
}
var MyCircularDeque = function(k) {
this.k = k;
this.items = []; // front first
};
MyCircularDeque.prototype.insertFront = function(value) {
if (this.items.length === this.k) return false;
this.items.unshift(value); // every value moves one place right
return true;
};
MyCircularDeque.prototype.insertLast = function(value) {
if (this.items.length === this.k) return false;
this.items.push(value);
return true;
};
MyCircularDeque.prototype.deleteFront = function() {
if (this.items.length === 0) return false;
this.items.shift(); // every value moves one place left
return true;
};
MyCircularDeque.prototype.deleteLast = function() {
if (this.items.length === 0) return false;
this.items.pop();
return true;
};
MyCircularDeque.prototype.getFront = function() {
return this.items.length ? this.items[0] : -1;
};
MyCircularDeque.prototype.getRear = function() {
return this.items.length ? this.items[this.items.length - 1] : -1;
};
MyCircularDeque.prototype.isEmpty = function() {
return this.items.length === 0;
};
MyCircularDeque.prototype.isFull = function() {
return this.items.length === this.k;
};
Complexity Analysis
Time Complexity: O(k) for each insert or delete at the front, O(1) for everything else
Inserting or deleting at index 0 moves up to k − 1 values.
Space Complexity: O(k)
At most k values.
Why This Approach Is Not Efficient
At these limits it still fits: 2,000 calls that each move up to 999 values is about 2 × 10^6 moves. But a deque is supposed to be cheap at both ends, and this one isn't: the cost of a front operation grows with the capacity.
The values never need to move. Fix them in k slots used as a circle and move only the ends: a head index for the front, and a count, so the back is (head + count - 1) % k. Adding at the front steps head back one slot, wrapping from slot 0 to slot k − 1.
Optimal Approach - A Ring Buffer
Intuition
Use an array of k slots, the index head of the front value, and count, the number of values. The count tells full (count == k) from empty (count == 0), which head alone can't.
insertLastwrites the slot after the back,(head + count) % k, and adds one tocount.insertFrontstepsheadback,(head - 1 + k) % k, writes there, and adds one tocount. The+ kkeeps the index from going negative in languages whose%keeps the sign.deleteFrontmovesheadforward;deleteLastonly lowerscount.- The back value is at
(head + count - 1) % k.
Step-by-Step Explanation
Let's trace Example 1, with room for 3:
insertLast(1): the slot after the back is(0 + 0) % 3, slot 0.insertLast(2): the slot after the back is(0 + 1) % 3, slot 1.insertFront(3):headsteps back from slot 0,(0 - 1 + 3) % 3, to slot 2, and 3 goes there.insertFront(4)returns false, becausecountis 3 and every slot is in use.getRear()reads the back,(2 + 3 - 1) % 3, slot 1: 2.isFull()returns true, sincecountis 3 of 3.deleteLast():countdrops to 2, so slot 1 is free again. Nothing moves.insertFront(4):headsteps back from slot 2,(2 - 1 + 3) % 3, to slot 1, and 4 goes there.getFront()reads slot 1: 4.
Three Slots in a Circle — Only `head` and the back move; the values stay in their slots.
Algorithm
- Keep
datawithkslots,head = 0andcount = 0. insertFront: refuse ifcount == k; elsehead = (head - 1 + k) % k,data[head] = value,count += 1.insertLast: refuse ifcount == k; elsedata[(head + count) % k] = value,count += 1.deleteFront: refuse ifcount == 0; elsehead = (head + 1) % k,count -= 1.deleteLast: refuse ifcount == 0; elsecount -= 1.getFrontreadsdata[head]andgetRearreadsdata[(head + count - 1) % k], or −1 whencount == 0.
Code
class MyCircularDeque {
vector<int> data; // the slots, reused in a circle
int head = 0; // index of the front value
int count = 0; // how many values are stored
public:
MyCircularDeque(int k) : data(k) {}
bool insertFront(int value) {
if (count == (int)data.size()) return false;
head = (head - 1 + data.size()) % data.size(); // step back, wrapping
data[head] = value;
count++;
return true;
}
bool insertLast(int value) {
if (count == (int)data.size()) return false;
data[(head + count) % data.size()] = value; // the slot after the back
count++;
return true;
}
bool deleteFront() {
if (count == 0) return false;
head = (head + 1) % data.size();
count--;
return true;
}
bool deleteLast() {
if (count == 0) return false;
count--; // the back slot simply stops being counted
return true;
}
int getFront() {
return count == 0 ? -1 : data[head];
}
int getRear() {
return count == 0 ? -1 : data[(head + count - 1) % data.size()];
}
bool isEmpty() {
return count == 0;
}
bool isFull() {
return count == (int)data.size();
}
};
class MyCircularDeque:
def __init__(self, k: int):
self.data = [0] * k # the slots, reused in a circle
self.head = 0 # index of the front value
self.count = 0 # how many values are stored
def insertFront(self, value: int) -> bool:
if self.count == len(self.data):
return False
self.head = (self.head - 1 + len(self.data)) % len(self.data) # step back, wrapping
self.data[self.head] = value
self.count += 1
return True
def insertLast(self, value: int) -> bool:
if self.count == len(self.data):
return False
self.data[(self.head + self.count) % len(self.data)] = value # the slot after the back
self.count += 1
return True
def deleteFront(self) -> bool:
if self.count == 0:
return False
self.head = (self.head + 1) % len(self.data)
self.count -= 1
return True
def deleteLast(self) -> bool:
if self.count == 0:
return False
self.count -= 1 # the back slot simply stops being counted
return True
def getFront(self) -> int:
return self.data[self.head] if self.count else -1
def getRear(self) -> int:
return self.data[(self.head + self.count - 1) % len(self.data)] if self.count else -1
def isEmpty(self) -> bool:
return self.count == 0
def isFull(self) -> bool:
return self.count == len(self.data)
class MyCircularDeque {
private final int[] data; // the slots, reused in a circle
private int head = 0; // index of the front value
private int count = 0; // how many values are stored
public MyCircularDeque(int k) {
data = new int[k];
}
public boolean insertFront(int value) {
if (count == data.length) return false;
head = (head - 1 + data.length) % data.length; // step back, wrapping
data[head] = value;
count++;
return true;
}
public boolean insertLast(int value) {
if (count == data.length) return false;
data[(head + count) % data.length] = value; // the slot after the back
count++;
return true;
}
public boolean deleteFront() {
if (count == 0) return false;
head = (head + 1) % data.length;
count--;
return true;
}
public boolean deleteLast() {
if (count == 0) return false;
count--; // the back slot simply stops being counted
return true;
}
public int getFront() {
return count == 0 ? -1 : data[head];
}
public int getRear() {
return count == 0 ? -1 : data[(head + count - 1) % data.length];
}
public boolean isEmpty() {
return count == 0;
}
public boolean isFull() {
return count == data.length;
}
}
var MyCircularDeque = function(k) {
this.data = new Array(k).fill(0); // the slots, reused in a circle
this.head = 0; // index of the front value
this.count = 0; // how many values are stored
};
MyCircularDeque.prototype.insertFront = function(value) {
if (this.count === this.data.length) return false;
this.head = (this.head - 1 + this.data.length) % this.data.length; // step back, wrapping
this.data[this.head] = value;
this.count++;
return true;
};
MyCircularDeque.prototype.insertLast = function(value) {
if (this.count === this.data.length) return false;
this.data[(this.head + this.count) % this.data.length] = value; // the slot after the back
this.count++;
return true;
};
MyCircularDeque.prototype.deleteFront = function() {
if (this.count === 0) return false;
this.head = (this.head + 1) % this.data.length;
this.count--;
return true;
};
MyCircularDeque.prototype.deleteLast = function() {
if (this.count === 0) return false;
this.count--; // the back slot simply stops being counted
return true;
};
MyCircularDeque.prototype.getFront = function() {
return this.count ? this.data[this.head] : -1;
};
MyCircularDeque.prototype.getRear = function() {
return this.count ? this.data[(this.head + this.count - 1) % this.data.length] : -1;
};
MyCircularDeque.prototype.isEmpty = function() {
return this.count === 0;
};
MyCircularDeque.prototype.isFull = function() {
return this.count === this.data.length;
};
Complexity Analysis
Time Complexity: O(1) per call
Each call does a few index operations; nothing is shifted.
Space Complexity: O(k)
The k slots, allocated once.
Common Mistakes
Each of these looks right on a quick read, and the tests still reject it. The outputs below come from running the code.
Reading the rear at the next free slot. return self.data[(self.head + self.count) % len(self.data)] if self.count else -1
On Example 1 it returns [null, true, true, true, false, 3, true, true, true, 4]; the answer is [null, true, true, true, false, 2, true, true, true, 4]. (head + count) % k is the next free slot, one past the back. The rear value is in the slot before it: (head + count - 1) % k.
Stepping back without adding k. head = (head - 1) % data.length; (in Java)
On Example 1 it crashes with ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 3; the answer is [null, true, true, true, false, 2, true, true, true, 4]. In Java, C++ and JavaScript, % keeps the sign of the left side, so (0 - 1) % k is −1, not k − 1, and the first insert at the front writes outside the array. Add k before taking the remainder: (head - 1 + k) % k.
Keeping a slot unused in only k slots. return (self.tail + 1) % len(self.data) == self.head
On Example 1 it returns [null, true, true, false, false, 2, true, true, true, 4]; the answer is [null, true, true, true, false, 2, true, true, true, 4]. Without a count, head == tail means empty, so a full deque has to keep one slot unused to look different. That needs k + 1 slots; with only k, the deque refuses the k-th value. Keep a count, or allocate one slot more.
Where This Pattern Is Taught
This problem practises the ring buffer at both ends. It's taught in Introduction to Queues, along with why the count is kept and why stepping back needs the + k. Design Circular Queue is the same ring with one open end.