Katkat

Implement a Circular Buffer

Jane StreetSoftware Engineer · Technical Screen

Last updated

Implement a fixed-capacity circular buffer (ring buffer) supporting constant-time FIFO operations using wraparound indexing.

Problem Statement

A circular buffer (or ring buffer) is a fixed-size, first-in-first-out (FIFO) data structure that connects the end of the buffer back to the beginning. It is widely used in high-frequency trading and low-latency systems for buffering streaming market data and order messages without dynamic memory allocations.

Implement the CircularBuffer class with the following methods:

  • __init__(capacity: int): Initializes the buffer with a fixed maximum capacity capacity.
  • enqueue(val: int) -> bool: Inserts val at the back of the buffer. Returns True if the item was added successfully, or False if the buffer is already full.
  • dequeue() -> Optional[int]: Removes and returns the value at the front of the buffer. Returns None if the buffer is empty.
  • peek() -> Optional[int]: Returns the value at the front of the buffer without removing it. Returns None if the buffer is empty.
  • is_empty() -> bool: Returns True if the buffer contains no elements, otherwise False.
  • is_full() -> bool: Returns True if the buffer has reached its maximum capacity, otherwise False.
  • size() -> int: Returns the current number of elements in the buffer.

All operations must execute in O(1) time and must not reallocate or resize the underlying fixed storage.

Class Interface

from typing import Optional

class CircularBuffer:
    def __init__(self, capacity: int):
        pass

    def enqueue(self, val: int) -> bool:
        pass

    def dequeue(self) -> Optional[int]:
        pass

    def peek(self) -> Optional[int]:
        pass

    def is_empty() -> bool:
        pass

    def is_full() -> bool:
        pass

    def size() -> int:
        pass

Examples

buf = CircularBuffer(3)
buf.enqueue(1)   # returns True
buf.enqueue(2)   # returns True
buf.enqueue(3)   # returns True
buf.enqueue(4)   # returns False (buffer is full)
buf.peek()       # returns 1
buf.dequeue()    # returns 1
buf.enqueue(4)   # returns True (wraps around)
buf.dequeue()    # returns 2
buf.size()       # returns 2

Constraints

  • 1 <= capacity <= 10^5
  • -10^9 <= val <= 10^9
  • At most 2 * 10^5 total calls will be made to any combination of methods.

Edge Cases

  • Calling dequeue or peek on an empty buffer.
  • Enqueueing when the buffer is completely full.
  • Multiple consecutive wrap-around cycles where tail wraps past index 0.

Hints

Hint 1

You can allocate a fixed-length list of size `capacity` upfront to prevent dynamic reallocations.

Hint 2

Tracking an explicit `size` counter along with `head` and `tail` pointers simplifies distinguishing between a full buffer and an empty buffer.

Hint 3

Use the modulo operator `(index + 1) % capacity` to wrap pointer indices back to the start of the underlying array.

Follow-up questions

  • How would you adapt this implementation for a single-producer, single-consumer (SPSC) lock-free concurrency model in C++ or Rust?
  • If the requirement changed such that enqueueing into a full buffer overwrites the oldest element instead of rejecting it, what changes would you make?
  • How can you optimize the modulo operation if capacity is guaranteed to be a power of two?