Design an Indexable Deque with O(1) Access
Hudson River TradingSoftware Engineer · Technical ScreenLast updated
Tests understanding of low-level data structure design, memory layout, and trade-offs required to build a double-ended queue supporting strict O(1) random indexing and amortized O(1) push/pop operations.
Problem Statement
Standard queue and deque implementations often prioritize push/pop performance at both ends, but not all provide constant-time random access by index. In high-performance trading environments, systems frequently require sliding windows or order queues that allow rapid insertion/removal at boundaries alongside instant lookups by relative position.
Design an IndexableDeque class that provides random access by 0-based index while supporting dynamic push and pop operations at both ends.
Implement the following API:
class IndexableDeque:
def __init__(self):
"""Initializes an empty deque."""
pass
def push_front(self, val: int) -> None:
"""Inserts val at the front of the deque in amortized O(1) time."""
pass
def push_back(self, val: int) -> None:
"""Inserts val at the back of the deque in amortized O(1) time."""
pass
def pop_front(self) -> int:
"""Removes and returns the front element. Raises IndexError if empty."""
pass
def pop_back(self) -> int:
"""Removes and returns the back element. Raises IndexError if empty."""
pass
def get(self, index: int) -> int:
"""Returns the element at 0-based index from the front in worst-case O(1) time."""
pass
def set(self, index: int, val: int) -> None:
"""Updates the element at 0-based index from the front in worst-case O(1) time."""
pass
def size(self) -> int:
"""Returns the number of elements currently stored in O(1) time."""
pass
Example
deque = IndexableDeque()
deque.push_back(10) # deque: [10]
deque.push_back(20) # deque: [10, 20]
deque.push_front(5) # deque: [5, 10, 20]
print(deque.get(0)) # Output: 5
print(deque.get(1)) # Output: 10
print(deque.get(2)) # Output: 20
deque.set(1, 15) # deque: [5, 15, 20]
print(deque.get(1)) # Output: 15
print(deque.pop_front()) # Output: 5, deque: [15, 20]
print(deque.pop_back()) # Output: 20, deque: [15]
print(deque.size()) # Output: 1
Constraints
-10^9 = size().- You cannot use built-in high-level deque classes (such as Python's
collections.dequeor C++std::deque) to back the primary storage; implement the underlying buffer mechanics using a fixed or dynamic contiguous array.
Hints
Hint 1
A circular buffer (ring buffer) using a fixed-capacity array with a head offset and a current size variable allows mapping logical index `i` to physical index `(head + i) % capacity` in O(1) time.
Hint 2
When moving `head` backwards for `push_front`, use modulo arithmetic `(head - 1 + capacity) % capacity` to wrap around properly.
Hint 3
When the buffer becomes full, allocate a new array of double the capacity and linearize the elements starting from physical index 0.
Follow-up questions
- How does a chunked-array layout (like C++ `std::deque`, which uses a map of fixed-size blocks) compare to a single resizing circular array in terms of memory fragmentation, cache locality, and re-allocation latency spikes?
- In a high-frequency trading application where dynamic resizing latency spikes (jitter) are unacceptable, how would you design this data structure?
- How could you adapt this structure into a bounded, wait-free Single-Producer Single-Consumer (SPSC) ring buffer?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.