Skip to content

Queue: Practice Problems with FIFO and Deque Solutions

Welcome to the practice problems for the Queue pattern. If you need a refresher on the code, the code templates have the FIFO, level-order, and monotonic deque shapes in all 6 languages. Each problem below includes a hint, a visual, and the full solution.

Recommended Study Order

The problems build on each other, so the order matters.

  1. Implement Queue using Stacks teaches the core FIFO semantics inside two LIFO containers. Start here if queues feel abstract.
  2. Number of Recent Calls shows the simplest possible queue usage, a time-based sliding window where the queue is the whole solution.
  3. Implement Stack using Queues flips the first problem around and forces you to reason about ordering from both sides.
  4. Design Circular Queue exercises the fixed-capacity variant, where empty and full must be distinguished correctly.
  5. Open the Lock uses BFS over a state space, the highest-frequency queue lesson in real interviews.
  6. Sliding Window Maximum is the monotonic deque, an
    O(N)
    result where a naive heap approach costs more.

The first three build the mental model, the middle one teaches the ring, and the last two are actual interview standards. Do them in that order.

The order above is designed to build intuition progressively. The app schedules your reviews so you don’t forget the FIFO fundamentals while grinding the hard tails. Set up your review schedule .

Easy Problems

1. Implement Queue using Stacks

LeetCode 232 | Difficulty: Easy

Brief: Implement a FIFO queue using only two stacks.

Why this pattern: A stack reverses order, so pushing into one stack and draining it into another reverses the reversal. That turns LIFO into FIFO.

Key Insight: Only transfer from the input stack to the output stack when the output stack is empty. Amortized work is one transfer per element.

Visual:

    graph LR
    A["push 1, 2, 3"] --> B["input: 3 2 1"]
    B --> C["first pop: fill output"]
    C --> D["output: 1 2 3"]
    D --> E["pop outputs 1"]
  

Code:

var MyQueue = function() {
  this.inStack = [];
  this.outStack = [];
};

MyQueue.prototype.push = function(x) {
  this.inStack.push(x);
};

MyQueue.prototype.pop = function() {
  this.transfer();
  return this.outStack.pop();
};

MyQueue.prototype.peek = function() {
  this.transfer();
  return this.outStack[this.outStack.length - 1];
};

MyQueue.prototype.empty = function() {
  return this.inStack.length === 0 && this.outStack.length === 0;
};

MyQueue.prototype.transfer = function() {
  if (this.outStack.length === 0) {
      while (this.inStack.length > 0) {
          this.outStack.push(this.inStack.pop());
      }
  }
};

The transfer happens only when the output stack is empty, so each element is moved at most once in its lifetime. All four operations are

O(1) amortized
.

2. Number of Recent Calls

LeetCode 933 | Difficulty: Easy

  • Prompt: return the number of pings received in the last 3000 milliseconds.
  • Why this pattern: a time series is a queue by nature. Old pings leave the window in the same order they entered.
  • Key Insight: the incoming timestamps are strictly increasing, so the queue is always ordered and only the head ever needs eviction.

Visual:

    sequenceDiagram
    participant Q as Queue
    participant W as Window 3000ms
    Q->>W: ping(1) -> 1
    Q->>W: ping(100) -> 2
    Q->>W: ping(3001) -> 3
    Q->>W: ping(3002) -> drops 1 -> 3
  

Code:

var RecentCounter = function() {
  this.queue = [];
};

RecentCounter.prototype.ping = function(t) {
  this.queue.push(t);
  while (this.queue.length > 0 && this.queue[0] < t - 3000) {
      this.queue.shift();
  }
  return this.queue.length;
};

Every ping is removed from the queue at most once, so the amortized cost per ping is constant, and the queue stores only pings inside the current window.

3. Implement Stack using Queues

LeetCode 225 | Difficulty: Easy

  • Prompt: implement a LIFO stack using only queues.
  • Why this pattern: the same transfer trick as problem 1, run in reverse. You need the newest element on top, which means the newest element must wait at the front of the queue.
  • Hint: push the new element into a helper queue, then move everything else after it. That places the newest element first.

Visual:

    graph LR
    A["push 1"] --> B["q1: 1"]
    B --> C["push 2 into helper"]
    C --> D["move 1 to helper: 2 1"]
    D --> E["top = q1[0] = 2"]
  

Code:

var MyStack = function() {
  this.q1 = [];
  this.q2 = [];
};

MyStack.prototype.push = function(x) {
  this.q2.push(x);
  while (this.q1.length > 0) {
      this.q2.push(this.q1.shift());
  }
  const tmp = this.q1;
  this.q1 = this.q2;
  this.q2 = tmp;
};

MyStack.prototype.pop = function() {
  return this.q1.shift();
};

MyStack.prototype.top = function() {
  return this.q1[0];
};

MyStack.prototype.empty = function() {
  return this.q1.length === 0;
};

The four methods use only queue primitives, no indexing. Every push rotates the whole stack into the secondary queue, which makes the push

O(N)
and every pop
O(1)
. Compare that with the stack-to-queue direction in problem 1, where pops transfer lazily.

Medium Problems

4. Design Circular Queue

LeetCode 622 | Difficulty: Medium

  • Prompt: design a queue with a fixed size that lets unused front slots be reused.
  • Why: the pointer arithmetic wraps around with the module, so the first slot becomes valid again once the front has moved. A circular buffer is the smallest full implementation of the pattern.
  • Hint: hold the head, the count, and the capacity. derive the tail slot as (head + count) % capacity.

Visual:

    graph LR
    subgraph Buffer[Capacity = 3]
        A0["Slot 0"]
        A1["Slot 1"]
        A2["Slot 2"]
    end
    B["enqueue 7"] --> B0["data[(head+size)%cap]"]
    B0 --> C["dequeue moves head forward"]
  

Code:

var MyCircularQueue = function(k) {
  this.data = new Array(k);
  this.cap = k;
  this.head = 0;
  this.size = 0;
};

MyCircularQueue.prototype.enQueue = function(value) {
  if (this.isFull()) return false;
  this.data[(this.head + this.size) % this.cap] = value;
  this.size++;
  return true;
};

MyCircularQueue.prototype.deQueue = function() {
  if (this.isEmpty()) return false;
  this.head = (this.head + 1) % this.cap;
  this.size--;
  return true;
};

MyCircularQueue.prototype.Front = function() {
  return this.isEmpty() ? -1 : this.data[this.head];
};

MyCircularQueue.prototype.Rear = function() {
  if (this.isEmpty()) return -1;
  return this.data[(this.head + this.size - 1) % this.cap];
};

MyCircularQueue.prototype.isEmpty = function() {
  return this.size === 0;
};

MyCircularQueue.prototype.isFull = function() {
  return this.size === this.cap;
};

The rear index derivation is the only tricky part. The head plus the size minus one, modulo capacity, is where the newest element lives. After a dequeue the head moves forward and the slot behind it becomes reusable, which is the entire point of a circular buffer.

5. Open the Lock

LeetCode 752 | Difficulty: Medium

  • Prompt: minimum number of moves to turn a 4-digit lock from “0000” to a target, where unused combinations are forbidden, one digit changes per move.
  • Why this pattern: every combination is a node and every turn is an edge of weight 1, so the shortest path in an unweighted graph is a BFS job.
  • Hint: start from “0000” and expand the frontier level by level, skipping dead combinations and anything already visited.

Visual:

    graph LR
    A["0000"] --> B["1000"]
    A --> C["0001"]
    A --> d["0900"]
    subgraph Level1
        B --> E["1100, 1900, 0000..."]
    end
    subgraph Level2["...up to target"]
        E2["dist appears at the shortest turn count"]
    end
  

Code:

var openLock = function(deadends, target) {
  const dead = new Set(deadends);
  if (dead.has("0000")) return -1;

  const queue = ["0000"];
  const visited = new Set(["0000"]);
  let level = 0;

  while (queue.length > 0) {
      const levelSize = queue.length;
      for (let i = 0; i < levelSize; i++) {
          const comb = queue.shift();
          if (comb === target) return level;

          for (let wheel = 0; wheel < 4; wheel++) {
              for (const delta of [-1, 1]) {
                  const digit = (Number(comb[wheel]) + delta + 10) % 10;
                  const next = comb.slice(0, wheel) + digit + comb.slice(wheel + 1);
                  if (visited.has(next) || dead.has(next)) continue;
                  visited.add(next);
                  queue.push(next);
              }
          }
      }
      level++;
  }
  return -1;
};

The BFS visits all 4-digit combinations in order of distance. The visited set is what stops the search from exploring 1000 -> 0000 -> 1000 forever, and the level counter is the answer. The % 10 wrap means turning a 0 up gives 9 and turning 9 down gives 0.

This problem is BFS practice in its purest form, and the same skeleton solves Word Ladder and Rotting Oranges when the state space is a grid or another representation.

Hard problems are where patterns separate candidates who pass from candidates who get offers. Track your misses and review them on a spaced schedule so you do not repeat them in the real interview.

Hard Problems

6. Sliding Window Maximum

LeetCode 239 | Difficulty: Hard

  • Prompt: return an array of the maximum in each contiguous window of size k.
  • Why this pattern: this is the monotonic deque. A plain queue cannot drop the smaller elements, and a max-heap wants to remove them lazily anyway. The deque keeps the candidates sorted descending and evicts based on the index.
  • Hint: store indices, not values. The index handles two evictions: values that fall out of the window, and values that are smaller than the incoming one.

Visual:

    graph LR
    A["nums=1 3 -1 -3 5 3 6 7"]
    B["3 3 5 5 6 7"]
  

Code:

var maxSlidingWindow = function(nums, k) {
  const result = [];
  const deque = []; // indices of candidate maxima

  for (let i = 0; i < nums.length; i++) {
      // 1. Expire indices that slid out of the window.
      while (deque.length > 0 && deque[0] < i - k + 1) {
          deque.shift();
      }
      // 2. Drop anything smaller or equal to the new value,
      //    it can never be the window max if it loses now.
      while (deque.length > 0 && nums[deque[deque.length - 1]] <= nums[i]) {
          deque.pop();
      }
      deque.push(i);

      // 3. Window is full from index k-1 onwards.
      if (i >= k - 1) {
          result.push(nums[deque[0]]);
      }
  }
  return result;
};

The deque stores indices, and the two eviction rules run in order: expire old, then trim weak. Each index enters and leaves the deque exactly once, so the whole pass is

O(N)
time with the window size of extra space.

That is the difference between the naive O(N*k) and the accepted O(N). The naive approach compares against all k candidates; the deque throws individuals away as soon as they cannot win, before they ever reach the front.

Final Word

This set covers the pattern in both practical directions. The queue for ordering, the deque for extremes, plus their two construction problems in each direction. When a problem involves “oldest leaving first”, reach for a queue. When “largest/smallest in every window”, reach for a deque. When “next turn count” appears as a number of moves, that’s BFS and your queue template.

After you internalize the ordering and the eviction, this pattern shows up everywhere, from cache design to tree traversal. The next step is to build the review habit that brings it back when you need it.

Done with these problems? The app has more, plus a review system that brings problems back right before you would forget them. Continue your prep .